Всё приложение — один файл в репозитории и одна команда для запуска
compose.yaml.envhealthcheck и depends_ondown и down -v, и не терять данныеПриложение из веб-сервиса и Redis в голом Docker — это сеть, том, два docker run с десятком параметров каждый, и всё в правильном порядке. Через месяц никто не вспомнит, с какими параметрами запускали. Compose описывает то же самое в YAML-файле, который лежит в репозитории рядом с кодом, и выполняет одной командой.
Compose входит в Docker как подкоманда docker compose (пакет docker-compose в Debian, docker-compose-plugin в репозитории Docker). Старая отдельная программа docker-compose через дефис — первая версия на Python, она больше не развивается; в старых инструкциях встречается, команды почти те же.
Веб-сервис на Flask считает посещения в Redis. Адрес Redis и приветствие берутся из переменных окружения:
import os
import signal
import sys
from flask import Flask
from redis import Redis
app = Flask(__name__)
redis = Redis(host=os.environ.get("REDIS_HOST", "localhost"), port=6379)
@app.get("/")
def index():
hits = redis.incr("hits")
return f"{os.environ.get('GREETING', 'Привет')}! Счётчик посещений: {hits}\n"
if __name__ == "__main__":
signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
app.run(host="0.0.0.0", port=8000)
Dockerfile — как в модуле 03. Файл Compose:
name: counter
services:
web:
build: .
image: counter-web:1
ports:
- "127.0.0.1:8000:8000"
environment:
REDIS_HOST: redis
GREETING: ${GREETING:-Привет}
depends_on:
redis:
condition: service_healthy
restart: unless-stopped
redis:
image: redis:8-alpine
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
restart: unless-stopped
volumes:
redis-data:
| Ключ | Что задаёт | Аналог в docker run |
|---|---|---|
name | Имя проекта — приставка к именам контейнеров, сети, томов. Без него — имя каталога | — |
services | Сервисы; имя сервиса — его имя в сети проекта | --name, --network-alias |
build / image | Собрать из каталога / взять образ; вместе — собрать и назвать так | docker build -t |
ports | Публикация портов, в кавычках | -p |
environment | Переменные; ${GREETING:-Привет} — из окружения или .env, иначе значение по умолчанию | -e |
command | Заменить CMD образа | аргументы после образа |
volumes | Тома и bind mount; верхний volumes: объявляет именованные тома | -v |
restart | Политика перезапуска | --restart |
healthcheck | Команда проверки готовности | --health-cmd |
depends_on | Порядок запуска и условие | — |
Файл называется compose.yaml (старое имя docker-compose.yml тоже понимается). Отступы в YAML — только пробелами. Строку version: в начале, которую можно встретить в старых файлах, Compose v2 игнорирует — писать её не нужно.
Прежде чем запускать, полезно посмотреть, как Compose понял файл: docker compose config выводит итоговую конфигурацию с подставленными переменными и полной записью сокращений. Фрагмент для сервиса web:
$ docker compose config | sed -n "/^ web:/,/restart/p"
web:
build:
context: /tmp/dkpage.WqNV5j/ex
dockerfile: Dockerfile
depends_on:
redis:
condition: service_healthy
required: true
environment:
GREETING: Привет
REDIS_HOST: redis
image: counter-web:1
networks:
default: null
ports:
- mode: ingress
host_ip: 127.0.0.1
target: 8000
published: "8000"
protocol: tcp
restart: unless-stopped
docker compose up -d создаёт всё, чего нет, и запускает. --build — пересобрать образы сервисов с build (иначе соберёт только при первом запуске). Строки сборки из вывода убраны:
$ docker compose up -d --build 2>&1 | grep -v -E "^#|^ *$|=>|\[\+\] Building"
Network counter_default Creating
Network counter_default Created
Volume "counter_redis-data" Creating
Volume "counter_redis-data" Created
Container counter-redis-1 Creating
Container counter-redis-1 Created
Container counter-web-1 Creating
Container counter-web-1 Created
Container counter-redis-1 Starting
Container counter-redis-1 Started
Container counter-redis-1 Waiting
Container counter-redis-1 Healthy
Container counter-web-1 Starting
Container counter-web-1 Started
Порядок виден в выводе: сеть counter_default, том counter_redis-data, контейнеры. web не стартует, пока Redis не стал Healthy, — это условие depends_on. Состояние сервисов:
$ docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
counter-redis-1 redis:8-alpine "docker-entrypoint.s…" redis 8 seconds ago Up 7 seconds (healthy) 6379/tcp
counter-web-1 counter-web:1 "python app.py" web 8 seconds ago Up 2 seconds 127.0.0.1:8000->8000/tcp
$ curl -s http://127.0.0.1:8000/; curl -s http://127.0.0.1:8000/; curl -s http://127.0.0.1:8000/
Привет! Счётчик посещений: 1
Привет! Счётчик посещений: 2
Привет! Счётчик посещений: 3
Счётчик живёт в Redis, а не в веб-сервисе: перезапуск web его не обнулит.
Команды Compose принимают имя сервиса вместо имени контейнера. Журналы — всех сервисов сразу или выбранных; -f — следить; --no-log-prefix убирает приставку с именем контейнера:
$ docker compose logs --no-log-prefix web 2>&1 | grep -v WARNING | tail -3
172.24.0.1 - - [23/Oct/2026 17:17:54] "GET / HTTP/1.1" 200 -
172.24.0.1 - - [23/Oct/2026 17:17:54] "GET / HTTP/1.1" 200 -
172.24.0.1 - - [23/Oct/2026 17:17:54] "GET / HTTP/1.1" 200 -
$ docker compose logs --no-log-prefix redis | grep -m1 Ready
1:M 23 Oct 2026 17:17:47.000 * Ready to accept connections tcp
$ docker compose exec redis redis-cli get hits
3
$ docker compose exec web pip list 2>/dev/null | grep -i -E "^(flask|redis) "
Flask 3.1.2
redis 5.2.1
exec работает в уже запущенном контейнере сервиса. run создаёт новый одноразовый контейнер с настройками сервиса — так выполняют миграции базы, разовые скрипты, проверки:
$ docker compose run --rm web id
Container counter-redis-1 Running
uid=999(app) gid=999(app) groups=999(app)
Строка counter-redis-1 Running — run тоже соблюдает depends_on и запускает зависимости, если они не работают.
Compose сам создаёт для проекта сеть <проект>_default и подключает к ней все сервисы. Имя сервиса резолвится в адрес его контейнера — поэтому в REDIS_HOST записано просто redis:
$ docker network ls --filter name=counter
NETWORK ID NAME DRIVER SCOPE
418a72bed60d counter_default bridge local
$ docker volume ls --filter name=counter
DRIVER VOLUME NAME
local counter_redis-data
$ docker compose exec web python -c "import socket; print(socket.gethostbyname(\"redis\"))"
172.24.0.2
Сети можно описать и явно — верхний ключ networks: и networks: у каждого сервиса. Схему «прокси — приложение — база» из модуля 05 вы соберёте так в итоговом проекте.
Порт Redis в ps показан как 6379/tcp без стрелки — он доступен только внутри сети проекта. Публиковать порт базы или кэша, к которым ходит только приложение, не нужно.
Значения ${ИМЯ} в compose.yaml Compose берёт из окружения оболочки, а если там нет — из файла .env в каталоге проекта. Окружение важнее файла. Повторный up -d сравнивает конфигурацию с запущенной и пересоздаёт только изменившиеся сервисы — Redis продолжает работать (Running), счётчик не сбрасывается:
$ echo "GREETING=Здравствуйте" > .env
$ docker compose up -d
Container counter-redis-1 Running
Container counter-web-1 Recreate
Container counter-web-1 Recreated
Container counter-redis-1 Waiting
Container counter-redis-1 Healthy
Container counter-web-1 Starting
Container counter-web-1 Started
$ curl -s http://127.0.0.1:8000/
Здравствуйте! Счётчик посещений: 4
$ GREETING="Добрый день" docker compose up -d 2>&1 | grep web
Container counter-web-1 Recreate
Container counter-web-1 Recreated
Container counter-web-1 Starting
Container counter-web-1 Started
$ curl -s http://127.0.0.1:8000/
Добрый день! Счётчик посещений: 5
Обычно в репозиторий кладут .env.example с перечнем переменных без значений, а настоящий .env с паролями создают на сервере и добавляют в .gitignore и .dockerignore.
docker inspect и docker compose config любому, у кого есть доступ к Docker на сервере. Для паролей в Compose есть ключ secrets: — значение подключается файлом в /run/secrets/ и не попадает в окружение. Официальные образы баз понимают это через переменные с окончанием _FILE: POSTGRES_PASSWORD_FILE со значением /run/secrets/db_password. Подробнее — модуль 08.Без условия depends_on: [redis] лишь гарантирует, что контейнер Redis запущен раньше. Запущен — не значит готов: базе данных после старта нужно несколько секунд, а приложение, которое сразу пытается подключиться, падает. healthcheck описывает проверку готовности: Docker выполняет redis-cli ping каждые 5 секунд, и контейнер получает состояние starting, healthy или unhealthy. condition: service_healthy заставляет ждать healthy:
$ docker compose up -d 2>&1 | tail -3; docker compose ps --format "table {{.Service}}\t{{.Status}}"
Container counter-redis-1 Healthy
Container counter-web-1 Starting
Container counter-web-1 Started
SERVICE STATUS
redis Up 5 seconds (healthy)
web Up Less than a second
$ docker compose ps --format "table {{.Service}}\t{{.Status}}"
SERVICE STATUS
redis Up 13 seconds (healthy)
web Up 7 seconds
$ docker inspect -f "{{json .State.Health.Log}}" counter-redis-1 | head -c 160; echo
[{"Start":"2026-10-23T20:18:36.524678813+03:00","End":"2026-10-23T20:18:36.600954243+03:00","ExitCode":0,"Output":"PONG\n"},{"Start":"2026-10-23T20:18:41.602244
$ docker compose down -v
Container counter-web-1 Stopping
Container counter-web-1 Stopped
Container counter-web-1 Removing
Container counter-web-1 Removed
Container counter-redis-1 Stopping
Container counter-redis-1 Stopped
Container counter-redis-1 Removing
Container counter-redis-1 Removed
Volume counter_redis-data Removing
Network counter_default Removing
Volume counter_redis-data Removed
Network counter_default Removed
Каждую проверку Docker записывает: время, код, вывод. Для PostgreSQL проверка — pg_isready -U postgres, для веб-сервиса — запрос на адрес проверки (в образе должна быть утилита для запроса: curl, wget или сам Python).
Готовность нужна не только при запуске. Приложение должно переживать и временную недоступность базы — переподключаться, а не падать навсегда. restart: unless-stopped страхует, но не заменяет этого.
stop останавливает контейнеры, не удаляя их; start запускает снова:
$ docker compose stop
Container counter-web-1 Stopping
Container counter-web-1 Stopped
Container counter-redis-1 Stopping
Container counter-redis-1 Stopped
$ docker compose ps -a --format "table {{.Service}}\t{{.Status}}"
SERVICE STATUS
redis Exited (0) Less than a second ago
web Exited (0) Less than a second ago
$ docker compose start
Container counter-redis-1 Starting
Container counter-redis-1 Started
Container counter-redis-1 Waiting
Container counter-redis-1 Healthy
Container counter-web-1 Starting
Container counter-web-1 Started
down удаляет контейнеры и сеть проекта, но не именованные тома. Поднимаем заново — счётчик продолжается с того же числа. down -v удаляет и тома — данные пропали:
$ docker compose down
Container counter-web-1 Stopping
Container counter-web-1 Stopped
Container counter-web-1 Removing
Container counter-web-1 Removed
Container counter-redis-1 Stopping
Container counter-redis-1 Stopped
Container counter-redis-1 Removing
Container counter-redis-1 Removed
Network counter_default Removing
Network counter_default Removed
$ docker volume ls --filter name=counter
DRIVER VOLUME NAME
local counter_redis-data
$ docker compose up -d 2>&1 | tail -3
Container counter-redis-1 Healthy
Container counter-web-1 Starting
Container counter-web-1 Started
$ curl -s http://127.0.0.1:8000/
Здравствуйте! Счётчик посещений: 6
$ docker compose down -v
Container counter-web-1 Stopping
Container counter-web-1 Stopped
Container counter-web-1 Removing
Container counter-web-1 Removed
Container counter-redis-1 Stopping
Container counter-redis-1 Stopped
Container counter-redis-1 Removing
Container counter-redis-1 Removed
Volume counter_redis-data Removing
Network counter_default Removing
Volume counter_redis-data Removed
Network counter_default Removed
$ docker volume ls --filter name=counter
DRIVER VOLUME NAME
| Команда | Контейнеры | Сеть | Тома | Образы |
|---|---|---|---|---|
stop | останавливает | — | — | — |
down | удаляет | удаляет | — | — |
down -v | удаляет | удаляет | удаляет | — |
down --rmi local | удаляет | удаляет | — | собранные Compose |
down -v на сервере — то же, что удалить базу. Привычка «down -v, чтобы начать с чистого листа» из разработки на сервер не переносится.| Команда | Что делает |
|---|---|
docker compose up -d [--build] | Создать и запустить; пересоздать изменившееся |
docker compose ps [-a] | Состояние сервисов |
docker compose logs -f [сервис] | Журналы |
docker compose exec сервис команда | Команда в работающем контейнере |
docker compose run --rm сервис команда | Разовый контейнер с настройками сервиса |
docker compose pull | Скачать новые версии образов; применить — up -d |
docker compose restart сервис | Перезапустить без пересоздания (новые переменные не применятся) |
docker compose config | Проверить файл и посмотреть итоговую конфигурацию |
docker compose down [-v] | Удалить контейнеры и сеть [и тома] |
Команды работают с проектом в текущем каталоге. Из другого каталога — docker compose -f /opt/app/compose.yaml … или -p имя_проекта.
compose.yaml в репозитории заменяет инструкцию по запуску; docker compose config — проверка.env; .env с паролями не коммититьhealthcheck + depends_on: condition: service_healthy — запуск по готовностиup -d пересоздаёт только изменённые сервисы; restart не применяет новые настройкиdown тома сохраняет, down -v удаляет данные