Как я подружил Docker Compose и перестал запускать контейнеры руками
Сколько можно писать docker run
Когда я только начал использовать Docker, каждый контейнер я запускал руками через docker run с кучей флагов. Для nginx это выглядело так:
docker run -d --name nginx -p 80:80 -v /data/nginx:/etc/nginx nginx:alpine
Вроде несложно. Но когда у тебя 10 контейнеров, а сервер перезагружается — вспоминать все команды и флаги тот ещё квест. Особенно если контейнер с MySQL требует проброса портов, путей, переменных окружения и паролей.
Docker Compose я долго игнорировал. Думал, это ещё одна прослойка, которая будет жрать ресурсы и усложнять жизнь. Как же я ошибался.
Первый Compose-файл
Начал с малого: nginx + PHP-FPM + MySQL. В докер-компоузе это выглядит как три сервиса в одном файле. Написал docker-compose.yml, запустил docker compose up -d — и всё поднялось само. Никаких длинных команд, ничего не надо вспоминать.
Я прямо почувствовал облегчение. Все настройки в одном файле, который можно закоммитить в git. Перезагрузил сервер — запустил docker compose up -d и все контейнеры на месте.
Как я строю Compose-файлы сейчас
Давай покажу, как выглядит мой типичный docker-compose.yml. Не какой-то абстрактный, а реальный, с которым я работаю каждый день:
version: '3.8'services:
app:
build: .
restart: unless-stopped
ports:
- "3000:3000"
depends_on:
db:
condition: service_healthy
env_file:
- .env
networks:
- app-netdb:
image: postgres:16-alpine
restart: unless-stopped
volumes:
- pgdata:/var/lib/postgresql/data
env_file:
- .env
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-netredis:
image: redis:7-alpine
restart: unless-stopped
networks:
- app-netvolumes:
pgdata:networks:
app-net:Пару моментов, до которых я дошёл своим горбом.
restart: unless-stopped— обязательно. Без этого контейнер после перезагрузки сервера не стартанёт, и ты узнаешь об этом только когда сайт упадёт. Было такое, да.
healthcheckна базе данных — спасение. Раньше приложение стартовало, пыталось коннектнуться к БД, а postgres ещё не поднялся. Падало с ошибкой. Docker Compose ждал depends_on, но не ждал, что БД готова принимать соединения. Healthcheck решает это — приложение стартует только когда postgres ответил на pg_isready.Сети — это магия, которую я не оценил сразу
Когда я запускал контейнеры через docker run, каждый контейнер был сам по себе. Чтобы nginx видел PHP-FPM, я вручную создавал сеть и подключал контейнеры. Забывал — и гадал, почему 502 Bad Gateway.
В Compose каждый проект получает свою сеть автоматически. Контейнеры видят друг друга по имени сервиса. Не по IP, который может измениться, а по имени. То есть из контейнера app я стучусь к БД как postgres://db:5432/myapp. Никаких IP, никаких --link, всё из коробки.
Если нужно несколько сетей — пожалуйста. Базу данных я прячу во внутреннюю сеть, а nginx выставляю наружу. Две сети в одном compose-файле, и микросервисы изолированы как надо.
Переменные окружения и секреты
Пароли и ключи я сначала писал прямо в docker-compose.yml. Потом закоммитил в git — и понял, что пароли утекли в репозиторий. Пришлось переезжать на .env файлы. Теперь все секреты в .env, а .env добавлен в .gitignore.
Но есть нюанс. .env файл подхватывается автоматически только если лежит рядом с docker-compose.yml. Если проект лежит в подпапке или ты запускаешь compose из другого каталога — переменные не подтянутся. Я обжёгся на этом, когда перенёс проект на сервер и полчаса тупил, почему пароль от БД не читается.
Теперь я в docker-compose.yml явно указываю
env_file: .envв каждом сервисе, которому нужны секреты. Или используюdocker compose --env-file .env.prod up -dдля разных окружений. Да, можно держать .env.dev, .env.prod и переключаться флагом. Очень удобно, когда на сервере пароли от БД настоящие, а локально — тестовые.Volumes — мусорка, которую надо контролировать
Если удалить контейнер через
docker compose downбез флага -v, volume остаются висеть мёртвым грузом. Через полгода я нашёл у себя десяток безымянных томов на 20 гигов — пришлось чистить вручную.Теперь я раз в месяц запускаю
docker system dfчтобы посмотреть, сколько места занято. Потомdocker volume prune— и гигиена наведена.Но тут тоже грабли. Если у тебя volume именованный (как pgdata в примере выше) — prune его не тронет, потому что на него ссылается compose-файл. А если безымянный — docker compose down -v его удалит вместе с контейнерами. Надо чётко понимать, какие volume тебе нужны после удаления проекта, а какие — нет.
Profiles — то, чего мне не хватало
У меня есть проект, где в одном compose-файле описаны сервисы для разработки и продакшена. Redis, БД, само приложение — и плюс adminer, mailhog, jaeger для локальной разработки. Запускать их на проде не нужно.
Раньше я держал два файла: docker-compose.yml и docker-compose.dev.yml. Запускал
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d. Работало, но неудобно.Потом открыл для себя profiles. Теперь adminer и mailhog помечены
profiles: ["dev"], и они запускаются только если я явно укажуdocker compose --profile dev up -d. На проде эти сервисы просто игнорируются, хотя всё лежит в одном файле. Красота.Что я теперь храню в Compose
Каждый проект у меня теперь начинается с docker-compose.yml. Там описаны все сервисы, их зависимости, сети, volumes. Если проект использует БД — она стартует до основного приложения, потому что depends_on.
Отдельный Compose-файл для мониторинга: Prometheus + Grafana + Node Exporter. Описал один раз — запускаю на любом сервере одной командой.
WordPress тоже через Compose. Сам WP, MySQL, phpMyAdmin. Три сервиса, один файл, одна команда для запуска.
Docker Compose v1 vs v2 — боль
На старом Docker Compose v1 команда была
docker-compose(с дефисом). На новом v2 —docker compose(без дефиса). Я долго тупил, почему на одном сервере команда работает, а на другом нет.На Ubuntu 22.04+ docker-compose v1 надо ставить отдельно, а v2 уже идёт вместе с Docker Engine. Но alias по умолчанию не стоит. Я прописал в ~/.bashrc:
alias docker-compose='docker compose'и забыл про проблему.Ещё из нового — docker compose v2 поддерживает
docker compose watch. Это как hot-reload для контейнеров. Меняешь код на хосте — compose пересобирает образ или копирует файлы в контейнер. Для разработки — имба.Мой вердикт
Я теперь не представляю работу с Docker без Compose. Даже один контейнер проще описать в YAML, чем каждый раз вспоминать флаги docker run. А если проект разрастается до 5-10 сервисов — Compose единственный адекватный способ не сойти с ума.
Один файл на проект. Закоммитил в git — и любой разработчик поднимет окружение одной командой. Сервер упал —
docker compose up -dи всё работает. Переезжаешь на другой хост — скопировал папку, запустил, готово.Сейчас я запускаю Docker Compose раз в неделю только чтобы накатить обновления. Не ввожу команды по 10 минут, не вспоминаю порты и пути. Просто
docker compose pull && docker compose up -d. Минута — и всё обновлено.