$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогDocker Compose для реальных проектов: конфиги, сети, volume и воспроизводимые окружения

Docker Compose для реальных проектов: конфиги, сети, volume и воспроизводимые окружения

$ sudo teach IT
·20 июля 2026 г.·9 мин·73
Docker Compose для реальных проектов: конфиги, сети, volume и воспроизводимые окружения

Научимся поднимать набор сервисов одним файлом, правильно разделять окружения и делать так, чтобы «у меня работает» исчезло.

Содержание
Базовая модель Compose и где обычно ломается реальная разработкаСтруктура репозитория: конфиги, compose-версии и единый контрактРекомендуемая схема файловОдин контракт для запускаКонфиги: переменные окружения, файлы конфигурации и секретыГде хранить env-переменныеКонфиги в виде файлов (не только env)Секреты: не класть в Git и не передавать как простые env в реальностиСети: сервисные имена, предсказуемая маршрутизация и отсутствие ловушек с localhostПример: приложение соединяется с БД по имени сервисаКогда нужна явная настройка сетейОчередная частая ошибка: порты наружу vs внутренняя связьVolume: как правильно управлять данными, чтобы они не «переезжали»Разделяйте dev и тестовое состояниеNamed volume vs bind mountИзоляция volume по окружениямКогда volume нужно очищатьВоспроизводимые окружения: фиксируем зависимости и избегаем скрытых допущенийПравило №1: фиксируйте образы (или тщательно описывайте build)Правило №2: не полагайтесь на локальные файлы, которых нет в образеПример рабочего docker-compose.yml для набора сервисов (dev)Что важно в этом примереCompose и окружения: один YAML или отдельные файлы?Пример docker-compose.dev.ymlЗдоровье сервисов: healthcheck и wait-for-dependenciesТипичные ошибки, которые ломают воспроизводимость1) “Внутри контейнера обратиться к localhost”

Docker Compose часто начинают использовать как удобную утилиту: «вот один файл — и поднимем БД и приложение». Но на практике почти каждый зрелый проект упирается в более сложные вопросы: как хранить конфиги по окружениям, как сделать сеть и сервисные имена стабильными, как правильно работать с volume, как воспроизводить окружение так, чтобы фраза «у меня работает» стала исключением, а не правилом.

Эта статья — практический разбор того, как собирать Compose-набор сервисов для реальных задач: с контрольной структурой конфигов, предсказуемыми сетями, аккуратным управлением данными и подходами к воспроизводимости.


Базовая модель Compose и где обычно ломается реальная разработка

Compose описывает набор сервисов и их зависимости в YAML. На практике важны четыре слоя:

  1. Образ/билд: откуда берётся код и как он собирается.
  2. Конфигурация: env, файлы конфигурации, секреты, флаги.
  3. Сети: как сервисы находят друг друга, какой DNS и какие порты наружу.
  4. Хранилище: volume/bind mount и стратегия персистентности.

Проблемы начинаются, когда:

  • конфиги размазаны по файлам и переменным без чёткой схемы;
  • используется один .env для всех сред (или вообще ручное редактирование YAML);
  • приложение обращается к localhost внутри контейнера;
  • персистентные данные живут «где-то там» и меняют поведение между запусками;
  • окружение нельзя воссоздать после чистой машины.

Чтобы «у меня работает» исчезло, нужно договориться о правилах: как задаём конфиги, как описываем сеть, где живут данные и как фиксируем зависимости окружения.


Структура репозитория: конфиги, compose-версии и единый контракт

Хорошая практика — разделить Compose-спецификацию и окруженческие параметры.

Рекомендуемая схема файлов

Пример структуры:

code
.
├── docker-compose.yml
├── docker-compose.override.yml
├── docker/
│   ├── env/
│   │   ├── dev.env
│   │   ├── test.env
│   │   └── prod.env
│   ├── config/
│   │   ├── nginx/
│   │   │   └── nginx.conf
│   │   └── scripts/
│   │       └── entrypoint.sh
├── .env.example
└── README.md

Идея такая:

  • базовый docker-compose.yml задаёт «что именно запускаем»;
  • окружение определяется отдельными *.env файлами;
  • опционально используем docker-compose.override.yml для локальной разработки (горячая перезагрузка, маппинг исходников и т.п.).

Один контракт для запуска

В README.md вы фиксируете команды, которые должны работать у каждого:

code
# Dev
docker compose --env-file docker/env/dev.env up -d --build

# Test (например, другая БД/фичи)
docker compose --env-file docker/env/test.env up -d --build

# Чистая остановка
docker compose down

На уровне команды важно использовать одинаковую схему передачи env, чтобы не было ситуации «у меня .env лежит в корне, а у тебя — в docker/env».


Конфиги: переменные окружения, файлы конфигурации и секреты

Где хранить env-переменные

Переменные окружения (environment, env_file, ${VAR}) — самый распространённый способ подставлять различия между dev/test/prod: адреса, названия баз, флаги логирования.

Compose поддерживает два подхода:

  1. env_file — указать файл с переменными.
  2. environment — задать переменные прямо в YAML (часто с подстановками из окружения).

В реальном проекте удобно сочетать: текущая среда — через --env-file, а внутри YAML — ссылаться на переменные.

Пример базового docker-compose.yml:

code
services:
  app:
    image: myorg/myapp:dev
    env_file:
      - ./docker/env/${APP_ENV}.env
    environment:
      # Пример добавления переменной поверх env_file
      LOG_LEVEL: ${LOG_LEVEL:-info}
    ports:
      - "${APP_PORT:-8080}:8080"

Но тут есть нюанс: ${APP_ENV} должен быть известен. Поэтому чаще делают так: вообще не хранить имя окружения в YAML, а передавать файл напрямую через --env-file:

code
docker compose --env-file docker/env/dev.env up -d --build

Тогда в YAML можно оперировать переменными, просто не добавляя env_file:

code
services:
  app:
    image: myorg/myapp:dev
    environment:
      APP_ENV: ${APP_ENV}
      LOG_LEVEL: ${LOG_LEVEL}
      DATABASE_URL: ${DATABASE_URL}
    ports:
      - "${APP_PORT}:${APP_PORT_CONTAINER}"

Так вы получаете один источник правды: файл dev.env полностью описывает среду.

Конфиги в виде файлов (не только env)

Иногда параметров слишком много, или нужны конфиги сложной структуры: nginx, php-fpm pool, сервисная конфигурация. В таких случаях лучше использовать bind mount или размещать файлы в образе.

Для dev удобно монтировать конфиги как файлы, для prod — избегать bind mount и класть конфиги в образ или использовать управляемые секреты.

Например, nginx:

code
services:
  nginx:
    image: nginx:1.27
    volumes:
      - ./docker/config/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - app
    ports:
      - "80:80"

Важно: :ro снижает риск случайной порчи конфигурации внутри контейнера.

Секреты: не класть в Git и не передавать как простые env в реальности

Compose в целом позволяет хранить секреты через механизм Compose Secrets (и интеграции с Docker Swarm), но если вы живёте в классическом Docker Compose без Swarm — всё равно важно не превращать env в «секреты». На практике подходы такие:

  • для dev: использовать тестовые значения;
  • для test/stage/prod: хранить секреты в CI/CD или в системах управления секретами (Vault, cloud secrets);
  • в Compose указывать их через переменные окружения, которые CI подставляет в рантайме.

Компромисс: Compose держим общим, а секреты — внешними. Тогда локально всё запускается со «слабой безопасностью», а в проде — с реальными секретами.


Сети: сервисные имена, предсказуемая маршрутизация и отсутствие ловушек с localhost

Compose по умолчанию создаёт сеть для проекта. Внутри этой сети сервисы доступны по имени сервиса (service name), например postgres или redis. Это фундамент: внутри контейнеров не используйте localhost для обращения к другому сервису.

Пример: приложение соединяется с БД по имени сервиса

Допустим, docker-compose.yml:

code
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - db_data:/var/lib/postgresql/data

  app:
    build: .
    environment:
      DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
    depends_on:
      - db
    ports:
      - "${APP_PORT}:${APP_PORT_CONTAINER}"

Здесь db — корректное hostname для контейнера app.

Когда нужна явная настройка сетей

Иногда требуется:

  • несколько сетей (front/back) для ограничения доступа;
  • статический выбор подсетей для интеграции с внешними системами;
  • подключение внешнего сервиса.

Пример с двумя сетями:

code
services:
  app:
    build: .
    networks:
      - internal
      - external
    environment:
      DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}

  nginx:
    image: nginx:1.27
    networks:
      - external
    ports:
      - "80:80"

  db:
    image: postgres:16
    networks:
      - internal
    volumes:
      - db_data:/var/lib/postgresql/data

networks:
  internal:
    driver: bridge
  external:
    driver: bridge

Подход полезен, когда вы хотите, чтобы БД была видна только приложению, а не внешнему миру.

Очередная частая ошибка: порты наружу vs внутренняя связь

Compose-порты (ports:) нужны для доступа извне хоста. Для связи сервисов внутри сети порты наружу не требуются — контейнеры могут общаться напрямую по внутреннему порту.

Поэтому, если вы в app ожидаете, что БД «доступна на 5432 через хост», это будет работать только локально и только при пробросе ports (а затем сломается в интеграции/CI).


Volume: как правильно управлять данными, чтобы они не «переезжали»

Разделяйте dev и тестовое состояние

Volume — это персистентность. Если вы смонтировали volume с БД, данные останутся между запусками, и это может:

  • ускорять dev,
  • но ломать тесты (из-за старых миграций/данных),
  • и создавать «призраки» в отладке.

Поэтому важно разделить:

  • dev: обычно оставляют volume, чтобы не пересоздавать данные каждый раз.
  • test: чаще используют либо отдельные named volumes на отдельную среду, либо очищают volume при запуске тестов, либо запускают БД с seed-скриптами.

Named volume vs bind mount

  • named volume создаётся Docker-ом и живёт как управляемый объём. Плюс — предсказуемое поведение.
  • bind mount монтирует каталог с хоста. Плюс — удобство для разработки, минус — зависимость от структуры ОС/путей и лишние риски.

Для БД почти всегда выбирают named volume:

code
volumes:
  db_data:

и в сервисе:

code
services:
  db:
    volumes:
      - db_data:/var/lib/postgresql/data

Изоляция volume по окружениям

Если вы используете один и тот же named volume для dev и test — данные будут смешиваться.

Варианты:

  1. Держать разные volume имена, подставляя суффиксы через переменные.
  2. Использовать разные проекты Compose (у Compose есть префикс проекта).

Рассмотрим второй подход: имя проекта можно менять опцией -p. Тогда тома и сети автоматически будут различаться.

Например:

  • docker compose -p myapp-dev up -d
  • docker compose -p myapp-test up -d

И тогда volume и сеть будут иметь префикс проекта.

Если хочется всё контролировать через YAML, можно подставлять суффикс:

code
volumes:
  db_data_${APP_ENV}:

но чаще удобнее держать это в командах CI/локально через -p.

Когда volume нужно очищать

Для тестов часто полезна команда:

code
docker compose -p myapp-test down -v

-v удалит volume. Да, это делает тесты более «дорогими», но вы платите за воспроизводимость.


Воспроизводимые окружения: фиксируем зависимости и избегаем скрытых допущений

Compose помогает стартовать, но воспроизводимость обеспечивают ещё дисциплина и фиксация.

Правило №1: фиксируйте образы (или тщательно описывайте build)

Для prod/test желательно использовать конкретные теги образов (иммутабельность). Для dev допустим build: из исходников.

Compose может выглядеть так:

code
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      NODE_ENV: ${NODE_ENV}

Но если вы в CI или тестовом контуре ожидаете «в точности то же окружение», сборка должна быть детерминированной:

  • фиксированные версии зависимостей в package-lock.json/poetry.lock/requirements.txt с хешами;
  • стабильные base images (конкретные digest’ы, если критично).

Правило №2: не полагайтесь на локальные файлы, которых нет в образе

Типичная ловушка — приложение читает конфиг или ключ из пути на хосте, потому что «у нас на машине так устроено». В Compose это приводит к тому, что контейнер в CI падает.

Решение:

  • либо кладите такие файлы в образ,
  • либо монтируйте их через volumes: так, чтобы путь существовал в репозитории.

Например, для локальной разработки можно добавить bind mount исходников:

code
services:
  app:
    build: .
    volumes:
      - ./:/app
    working_dir: /app

Но помните: это dev-паттерн. Для prod его лучше отключать.


Пример рабочего docker-compose.yml для набора сервисов (dev)

Ниже — пример «типового» стека: приложение + БД + кеш + фронт (nginx). Он демонстрирует:

  • env-переменные,
  • сеть по имени сервиса,
  • volume для БД,
  • разделение по портам.
code
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - db_data:/var/lib/postgresql/data
    networks:
      - internal

  redis:
    image: redis:7.2
    networks:
      - internal

  app:
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      APP_ENV: ${APP_ENV}
      DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
      REDIS_URL: redis://redis:6379/0
      PORT: ${APP_PORT_CONTAINER}
    depends_on:
      - db
      - redis
    ports:
      - "${APP_PORT}:${APP_PORT_CONTAINER}"
    networks:
      - internal
      - external

  nginx:
    image: nginx:1.27
    volumes:
      - ./docker/config/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - app
    ports:
      - "${HTTP_PORT:-80}:80"
    networks:
      - external

volumes:
  db_data:

networks:
  internal:
    driver: bridge
  external:
    driver: bridge

Что важно в этом примере

  • DATABASE_URL использует db как hostname — это правильная сетевая модель Compose.
  • redis://redis:6379/0 — тот же принцип.
  • db_data — named volume для персистентности БД.
  • nginx подключён к external, БД — к internal.
  • ports пробрасываются только тем сервисам, которым нужен доступ с хоста.

Compose и окружения: один YAML или отдельные файлы?

Есть два популярных подхода:

  1. Один docker-compose.yml, а окружение задаётся только .env.
  2. Отдельные compose-файлы: docker-compose.dev.yml, docker-compose.test.yml, docker-compose.prod.yml — и сборка через -f.

Первый подход проще для старта, но быстро усложняется, когда отличий становится много (разные volume, разные команды, разные healthcheck, разные маппинги).

Второй подход лучше масштабируется. Пример команды:

code
docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file docker/env/dev.env up -d --build

Где базовый файл содержит общую структуру, а dev-файл дополняет.

Пример docker-compose.dev.yml

code
services:
  app:
    volumes:
      - ./:/app
    command: ["npm", "run", "dev"]

В base файле command не задан, поэтому используется то, что определено в Dockerfile, либо дефолт в образе.

Так вы избегаете монструозных условных выражений внутри одного YAML.


Здоровье сервисов: healthcheck и wait-for-dependencies

Compose умеет управлять зависимостями через depends_on, но важно понимать: он не ждёт готовности приложения. Он лишь определяет порядок старта.

Чтобы избежать гонок (например, приложение стартует и пытается мигрировать БД, которая ещё не приняла соединения), используйте:

  • healthcheck для БД и зависимых сервисов,
  • либо «инициализацию» через отдельный job/скрипт,
  • либо retry logic в приложении.

Пример healthcheck для postgres:

code
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 20

Далее в приложении уже лучше делать retries на подключение или запускать миграции после того, как БД здорова.


Типичные ошибки, которые ломают воспроизводимость

1) “Внутри контейнера обратиться к localhost”

В контейнере localhost

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу
Простыми словами о защищенном соединении

Простыми словами о защищенном соединении

Базовый курс для обычных людей: что такое защищённое соединение, как оно устроено, что оно умеет и чего не умеет, как выбрать сервис и настроить его на своих устройствах.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Сервис, который не падает - как построить безопасную зашифрованную сеть

Сервис, который не падает - как построить безопасную зашифрованную сеть

Практический курс по безопасности сети: как собрать собственный сервис защищённого доступа - от пустого сервера до подписки с автовыбором - и, что важнее, как потом не уронить его своими руками.

Перейти к курсу

Другие статьи

Деплой без сюрпризов: как подготовить Docker-образ, переменные окружения и healthcheck
деплой

Деплой без сюрпризов: как подготовить Docker-образ, переменные окружения и healthcheck

Разберём практический чек-лист подготовки контейнера к продакшену: как собрать воспроизводимый образ, как правильно прокидывать конфиги/секреты через окружение и как настроить healthcheck, чтобы деплой не “успевал” раньше готовности сервиса.

18 сентября 2026 г.
1030
Пишем CLI для разработчика на Python: команды, конфиги и корректные exit codes по стандарту проекта
пишем

Пишем CLI для разработчика на Python: команды, конфиги и корректные exit codes по стандарту проекта

Построим удобный интерфейс командной утилиты: структура подкоманд, чтение конфигов, валидация входных параметров и единая логика ошибок с понятными exit code. Сфокусируемся на UX для тех, кто запускает CLI в CI и в скриптах.

15 сентября 2026 г.
1030
Создание Telegram бота в 2026 легко и просто! Полный курсы!
создание

Создание Telegram бота в 2026 легко и просто! Полный курсы!

19 июня 2026 г.
1431
Как выбрать, куда идти в IT: веб, боты, данные — без хаоса
выбрать

Как выбрать, куда идти в IT: веб, боты, данные — без хаоса

Разберём самые понятные направления для новичка (веб, боты, аналитика/данные) и чем они отличаются по шагам обучения. В конце вы получите простой план, как проверить, что вам подходит, и какой стартовый путь выбрать.

5 октября 2026 г.
580
Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть
путь

Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть

Разберём структуру репозитория, как упаковать проект в понятную историю (проблема → решение → запуск → метрики качества) и какие артефакты повышают доверие: скринкасты, архитектурная схема, чек-лист продакшн-готовности.

17 сентября 2026 г.
1090
Как понять, какой язык программирования учить первым: простой тест и выбор
понять

Как понять, какой язык программирования учить первым: простой тест и выбор

Помогу новичку выбрать первый язык без стресса: объясню варианты по целям (веб, боты, анализ данных), что учить в первую очередь и как не потратить месяцы впустую. В конце — краткий план старта на ближайшие недели.

2 октября 2026 г.
630

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

Базовая модель Compose и где обычно ломается реальная разработкаСтруктура репозитория: конфиги, compose-версии и единый контрактРекомендуемая схема файловОдин контракт для запускаКонфиги: переменные окружения, файлы конфигурации и секретыГде хранить env-переменныеКонфиги в виде файлов (не только env)Секреты: не класть в Git и не передавать как простые env в реальностиСети: сервисные имена, предсказуемая маршрутизация и отсутствие ловушек с localhostПример: приложение соединяется с БД по имени сервисаКогда нужна явная настройка сетейОчередная частая ошибка: порты наружу vs внутренняя связьVolume: как правильно управлять данными, чтобы они не «переезжали»Разделяйте dev и тестовое состояниеNamed volume vs bind mountИзоляция volume по окружениямКогда volume нужно очищатьВоспроизводимые окружения: фиксируем зависимости и избегаем скрытых допущенийПравило №1: фиксируйте образы (или тщательно описывайте build)Правило №2: не полагайтесь на локальные файлы, которых нет в образеПример рабочего docker-compose.yml для набора сервисов (dev)Что важно в этом примереCompose и окружения: один YAML или отдельные файлы?Пример docker-compose.dev.ymlЗдоровье сервисов: healthcheck и wait-for-dependenciesТипичные ошибки, которые ломают воспроизводимость1) “Внутри контейнера обратиться к localhost”