Docker Compose запускает несколько связанных контейнеров по описанию в compose.yaml. В одном файле задаются приложение, база, сетевые адреса и хранилище данных. Соберём локальный счётчик: Python принимает HTTP-запрос, PostgreSQL сохраняет запись, а пересоздание контейнеров не обнуляет результат.
Команды ниже используют docker compose с пробелом. Нужны работающий Docker и плагин Compose; проверь их командами docker version и docker compose version. Пример проверен с Compose 5.5.0. Как устроена сборка одного образа, разобрано отдельно в статье про Dockerfile.
Приложение обращается к db, данные остаются в томе
db находит контейнер базы в сети Compose. Том pgdata хранит данные отдельно от жизненного цикла этого контейнера.Создать небольшой проект
Создай отдельную пустую папку compose-demo. Все следующие файлы и команды относятся к ней. Если выполнял предыдущую статью, сначала останови её контейнер командой docker stop koddo-hello, чтобы освободить порт 8000.
В requirements.txt запиши:
Flask==3.1.2
psycopg[binary]==3.2.12
Сохрани в app.py весь код приложения:
import os
import psycopg
from flask import Flask
app = Flask(__name__)
database_url = os.environ["DATABASE_URL"]
with psycopg.connect(database_url) as connection:
connection.execute("""
CREATE TABLE IF NOT EXISTS visits (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY
)
""")
@app.get("/")
def count_visits():
with psycopg.connect(database_url) as connection:
total = connection.execute("SELECT count(*) FROM visits").fetchone()[0]
return {"visits": total}
@app.post("/visits")
def add_visit():
with psycopg.connect(database_url) as connection:
visit_id = connection.execute(
"INSERT INTO visits DEFAULT VALUES RETURNING id"
).fetchone()[0]
return {"id": visit_id}, 201
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
GET / читает число записей, POST /visits добавляет одну. База создаёт идентификатор сама. При успешном выходе из with psycopg.connect(...) Psycopg фиксирует транзакцию и закрывает соединение; при исключении откатывает изменения. Поэтому ответ об успехе возвращается после записи, а не до неё. Это поведение описано в документации Psycopg.
В Dockerfile помести:
FROM python:3.14-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
USER 10001
EXPOSE 8000
CMD ["python", "app.py"]
Добавь .dockerignore, чтобы лишние файлы не попадали в контекст сборки:
.git
.venv
__pycache__
*.pyc
.env
.env.*
Описать сервисы в compose.yaml
Создай рядом файл compose.yaml:
name: koddo-compose-demo
services:
app:
build: .
ports:
- "127.0.0.1:8000:8000"
environment:
DATABASE_URL: postgresql://demo:demo@db:5432/demo?connect_timeout=3
depends_on:
db:
condition: service_healthy
db:
image: postgres:17-alpine
environment:
POSTGRES_USER: demo
POSTGRES_PASSWORD: demo
POSTGRES_DB: demo
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U demo -d demo"]
interval: 2s
timeout: 3s
retries: 15
start_period: 5s
volumes:
pgdata:
app собирается из локального Dockerfile благодаря build: .. Для db используется готовый образ PostgreSQL. Compose создаёт общую сеть автоматически; отдельное поле networks для этой схемы не нужно. name задаёт имя проекта, к которому привязаны его контейнеры, сеть и том. Модель services, volumes и проекта описана в документации Compose.
Пароль demo здесь открытый учебный пароль для отдельной локальной базы. Для рабочего сервиса нужны отдельные учётные данные и управляемое хранилище секретов. HTTP-порт опубликован только на 127.0.0.1; порт базы на компьютер не публикуется.
Запустить и проверить запись
В папке compose-demo выполни:
docker compose config --quiet
docker compose up -d --build
docker compose ps
docker compose logs --tail=30 app
curl --fail http://127.0.0.1:8000/
curl --fail -X POST http://127.0.0.1:8000/visits
curl --fail http://127.0.0.1:8000/
config --quiet проверяет конфигурацию, up -d --build собирает приложение и запускает сервисы в фоне. Первый запуск скачивает образы и пакеты. Дождись сообщения Flask о запуске сервера в логах; если первый curl пришёл раньше него, повтори запрос.
На новом пустом томе три ответа будут такими:
{"visits":0}
{"id":1}
{"visits":1}
Это три отдельных JSON-ответа. Проверить результат прямо в базе можно без установки psql на компьютере:
docker compose exec db psql -U demo -d demo -c 'SELECT count(*) FROM visits;'
Запрос вернёт 1. Если проект уже запускался и том сохранился, начальное число может быть больше нуля.
Почему в DATABASE_URL стоит db, а не localhost
Внутри app адрес localhost указывает на сам контейнер приложения. PostgreSQL работает в другом контейнере, поэтому адрес соединения — db:5432: имя сервиса и внутренний порт базы.
С компьютера запрос идёт на 127.0.0.1:8000, а Compose перенаправляет его в порт 8000 приложения. Между контейнерами опубликованный порт компьютера не нужен. Не записывай IP контейнера в конфигурацию: после пересоздания он может измениться, имя сервиса останется прежним. Правила DNS и портов объяснены в руководстве по сети Compose.
depends_on: запущен ещё не значит готов
Короткое depends_on: [db] задаёт порядок старта, но не гарантирует, что PostgreSQL уже принимает соединения. В примере используется condition: service_healthy: Compose ждёт успешного healthcheck, прежде чем запустить app.
pg_isready проверяет, отвечает ли сервер; это не проверка всех прав пользователя или наличия таблиц приложения. Таблицу создаёт сам app после подключения. Условия ожидания описаны в документации запуска сервисов.
Ожидание готовности решает стартовую гонку. Если база упадёт позже, запрос приложения завершится ошибкой: depends_on не превращается в автоматическое восстановление соединений. В этом примере новое соединение открывается на каждый запрос и имеет connect_timeout=3; рабочему сервису понадобятся обработка временных отказов и ограниченные повторы там, где повтор операции безопасен.
Volumes: проверить сохранение данных после down
Том pgdata смонтирован в /var/lib/postgresql/data, каталог данных выбранного образа PostgreSQL 17. Монтирование для PostgreSQL 18 и новее отличается, поэтому не меняй только номер major-версии, сохраняя остальные настройки вслепую. Пути и правила обновления описаны на странице официального образа PostgreSQL.
Останови и пересоздай проект:
docker compose down
docker compose up -d
docker compose logs --tail=30 app
curl --fail http://127.0.0.1:8000/
После старта приложения ответ останется {"visits":1}, если до остановки была одна запись и ты не добавлял новые. Обычный down удаляет контейнеры и сеть проекта, но сохраняет именованный том.
docker compose down --volumes удалит и учебную базу вместе с томом. Выполняй эту команду только в данном учебном проекте, когда записи больше не нужны:
docker compose down --volumes
Поведение down и флага --volumes описано в справочнике команды. Том защищает от обычного пересоздания контейнера; резервной копией он не служит.
Переменные POSTGRES_USER, POSTGRES_PASSWORD и POSTGRES_DB применяются при первой инициализации пустого каталога данных. Смена пароля в YAML не меняет пароль пользователя в уже существующей базе. Для сохранённых данных пароль меняют в PostgreSQL; удалять том ради этого не нужно.
Где искать причину ошибки
| Симптом | Что проверить |
|---|---|
port is already allocated | Порт 8000 занят. Останови предыдущий учебный контейнер или поменяй левый порт в ports и адрес curl |
connection refused при старте | Адрес должен быть db:5432; посмотри docker compose logs db и статус healthcheck |
password authentication failed | Учётные данные приложения должны совпадать с реальной учётной записью PostgreSQL; старый том хранит прежний пароль |
После изменения app.py старый ответ | Повтори docker compose up -d --build; docker compose restart не собирает новый образ |
| Ответ HTTP 500 | Начни с docker compose logs --tail=50 app: там будет ошибка Python или базы |
Приложение использует встроенный сервер Flask и создаёт таблицу при запуске. Перед обслуживанием пользователей замени development server на сервер для публикации Flask, вынеси изменение схемы в миграции и настрой резервные копии. Сборку и проверку образа можно затем включить в CI/CD.