Docker Compose: как запустить приложение и базу данных

DevOps Автор: Среда и версия: Docker Engine 29.7.2, Compose 5.5.0, Linux amd64; Python 3.14.6, Flask 3.1.2, Psycopg 3.2.12, PostgreSQL 17.9; проверено 7 сентября 2026
содержание

Docker Compose запускает несколько связанных контейнеров по описанию в compose.yaml. В одном файле задаются приложение, база, сетевые адреса и хранилище данных. Соберём локальный счётчик: Python принимает HTTP-запрос, PostgreSQL сохраняет запись, а пересоздание контейнеров не обнуляет результат.

Команды ниже используют docker compose с пробелом. Нужны работающий Docker и плагин Compose; проверь их командами docker version и docker compose version. Пример проверен с Compose 5.5.0. Как устроена сборка одного образа, разобрано отдельно в статье про Dockerfile.

Docker Compose / 01

Приложение обращается к db, данные остаются в томе

HTTP-запрос проходит через приложение к базе данныхШаг 1: curl обращается к порту 8000 компьютера. Шаг 2: сервис app подключается к PostgreSQL по адресу db:5432. Шаг 3: PostgreSQL сохраняет записи в именованном томе pgdata. Контейнер базы можно пересоздать с тем же томом.01 / HTTP02 / SQL03 / хранениеКомпьютерcurl127.0.0.1:8000Сервис appPython + Flaskпорт 8000Сервис dbPostgreSQL 17адрес db:5432Том pgdataapp и db находятся в общей сети Compose.У каждого контейнера свой localhost.
Имя 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.

Источники