Как я подружил 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. Минута — и всё обновлено.Расширю статью о 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 я долго игнорировал. Думал, это ещё одна прослойка, которая будет жрать ресурсы и усложнять жизнь. Как же я ошибался. Ручной запуск контейнеров — это хаос. Десять docker run команд с флагами, портами, волями и паролями. После перезагрузки сервера всё забыто, и начинается восстановление по памяти. Пока контейнеров три — терпимо. Когда десять — вы вспоминаете: главное не команды, а то, что вся конфигурация должна быть описана и воспроизводима. Эта мысль привела меня к Compose. Первый Compose-файл. Начал с малого: nginx + PHP-FPM + MySQL. В докер-компоузе это выглядит как три сервиса в одном файле. Написал docker-compose.yml, запустил docker compose up -d — и всё поднялось само. Никаких длинных команд, ничего не надо вспоминать. Я прямо почувствовал облегчение. Все настройки в одном файле, который можно закоммитить в git. Перезагрузил сервер — запустил docker compose up -d и все контейнеры на месте. Первый опыт с Compose — это освобождение. Три сервиса в одном файле вместо трёх длинных команд. docker compose up -d поднял всё разом. Конфигурация — в git: если что-то сломается, это восстанавливается из истории. Перезагрузка сервера больше не страшит: одна команда возвращает всю инфраструктуру. С этого момента я начал переводить все проекты на Compose. Как я строю 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-net. db: 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-net. redis: image: redis:7-alpine, restart: unless-stopped, networks: - app-net. volumes: pgdata:, networks: app-net:. Это рабочий скелет проекта: приложение, база данных и кеш. Все настройки декларативны, всё версионируется. Пару моментов, до которых я дошёл своим горбом. restart: unless-stopped — обязательно. Без этого контейнер после перезагрузки сервера не стартанёт, и ты узнаешь об этом только когда сайт упадёт. Было такое, да. healthcheck на базе данных — спасение. Раньше приложение стартовало, пыталось коннектнуться к БД, а postgres ещё не поднялся. Падало с ошибкой. Docker Compose ждал depends_on, но не ждал, что БД готова принимать соединения. Healthcheck решает это — приложение стартует только когда postgres ответил на pg_isready. restart: unless-stopped — это первый рубеж надёжности. Контейнер автоматически возвращается после перезагрузки сервера. Без этого — тихий отказ: сайт лежит, а вы узнаёте об этом от посетителей. healthcheck с pg_isready — это второй рубеж: приложение не стартует, пока база не готова принимать соединения. depends_on ждёт запуска контейнера, healthcheck ждёт готовности. Комбинация решает классическую проблему порядка запуска. Сети — это магия, которую я не оценил сразу. Когда я запускал контейнеры через docker run, каждый контейнер был сам по себе. Чтобы nginx видел PHP-FPM, я вручную создавал сеть и подключал контейнеры. Забывал — и гадал, почему 502 Bad Gateway. В Compose каждый проект получает свою сеть автоматически. Контейнеры видят друг друга по имени сервиса. Не по IP, который может измениться, а по имени. То есть из контейнера app я стучусь к БД как postgres://db:5432/myapp. Никаких IP, никаких --link, всё из коробки. Если нужно несколько сетей — пожалуйста. Базу данных я прячу во внутреннюю сеть, а nginx выставляю наружу. Две сети в одном compose-файле, и микросервисы изолированы как надо. Сети в Compose — это автоматическая магия: каждый проект получает свою сеть, и контейнеры находят друг друга по имени сервиса, а не по IP. DNS внутри сети — это и есть то, что решает проблему 502 навсегда. Разделение сетей позволяет изолировать базу от внешнего мира: только приложение видит БД, а наружу выходит только nginx. Для микросервисной архитектуры — обязательная практика. Переменные окружения и секреты. Пароли и ключи я сначала писал прямо в 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 и переключаться флагом. Очень удобно, когда на сервере пароли от БД настоящие, а локально — тестовые. Секреты в git — это протекший пароль. Переезд на .env обязателен: секреты в отдельном файле, .env в .gitignore. Нюанс: .env подхватывается автоматически только из директории compose-файла. Для разных окружений — .env.dev и .env.prod с переключением флагом --env-file. Пароли настоящие только на сервере, локально — тестовые. Правило: секреты — в env-файлы, env-файлы — вне git. Volumes — мусорка, которую надо контролировать. Если удалить контейнер через docker compose down без флага -v, volume остаются висеть мёртвым грузом. Через полгода я нашёл у себя десяток безымянных томов на 20 гигов — пришлось чистить вручную. Теперь я раз в месяц запускаю docker system df чтобы посмотреть, сколько места занято. Потом docker volume prune — и гигиена наведена. Но тут тоже грабли. Если у тебя volume именованный (как pgdata в примере выше) — prune его не тронет, потому что на него ссылается compose-файл. А если безымянный — docker compose down -v его удалит вместе с контейнерами. Надо чётко понимать, какие volume тебе нужны после удаления проекта, а какие — нет. Volumes — это невидимый мусор, который копится. docker compose down без -v оставляет тома висеть. docker system df показывает реальную картину: сколько занято томами. docker volume prune чистит безымянные, но не трогает именованные, на которые ссылается compose. Понимание разницы между именованными и безымянными томами экономит диски и нервы. Гигиена volumes — часть рутины админа. 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. На проде эти сервисы просто игнорируются, хотя всё лежит в одном файле. Красота. Profiles решают проблему «один файл для разных окружений». Раньше — два файла и два флага. Теперь — один файл и пометки profiles. Сервисы для разработки (adminer, mailhog, jaeger) запускаются только с --profile dev. На проде они игнорируются. Одна конфигурация, чистое разделение сред. Этого мне не хватало долгие годы. Что я теперь храню в Compose. Каждый проект у меня теперь начинается с docker-compose.yml. Там описаны все сервисы, их зависимости, сети, volumes. Если проект использует БД — она стартует до основного приложения, потому что depends_on. Отдельный Compose-файл для мониторинга: Prometheus + Grafana + Node Exporter. Описал один раз — запускаю на любом сервере одной командой. WordPress тоже через Compose. Сам WP, MySQL, phpMyAdmin. Три сервиса, один файл, одна команда для запуска. Compose стал стартовой точкой каждого проекта: сервисы, зависимости, сети, тома — всё в одном файле. БД стартует до приложения через depends_on. Мониторинг — отдельный Compose: Prometheus, Grafana, Node Exporter, одна команда на любом сервере. WordPress — три сервиса в одном файле. Паттерн один: один проект — один Compose-файл — одна команда. 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 пересобирает образ или копирует файлы в контейнер. Для разработки — имба. Разница между v1 и v2 — это дефис в команде и удобство. На новых Ubuntu v2 идёт с Docker Engine, но alias не настраивается сам. Прописал alias docker-compose='docker compose' — и проблема ушла. Использовать alias для обратной совместимости — хороший приём. docker compose watch — это как hot-reload: меняете код, контейнер пересобирается или копирует файлы. Для разработки — незаменимо. Мой вердикт. Я теперь не представляю работу с Docker без Compose. Даже один контейнер проще описать в YAML, чем каждый раз вспоминать флаги docker run. А если проект разрастается до 5-10 сервисов — Compose единственный адекватный способ не сойти с ума. Один файл на проект. Закоммитил в git — и любой разработчик поднимет окружение одной командой. Сервер упал — docker compose up -d и всё работает. Переезжаешь на другой хост — скопировал папку, запустил, готово. Сейчас я запускаю Docker Compose раз в неделю только чтобы накатить обновления. Не ввожу команды по 10 минут, не вспоминаю порты и пути. Просто docker compose pull && docker compose up -d. Минута — и всё обновлено. Docker Compose — это стандарт де-факто для управления контейнерами на сервере. Один файл вместо десятков команд, декларативное описание вместо памяти, воспроизводимость вместо случайности. Начните с одного проекта — и вы поймёте, за что его любят миллионы разработчиков. Попробуйте Compose сегодня, если ещё не пробовали.