Перейти к содержимому
RootPoligon

Compose: два сервиса вместе

Ситуация

Чтобы запустить «Заметки» с nginx, нужно помнить шесть команд: собрать образ, создать сеть, создать том, запустить приложение с томом и сетью, запустить nginx с портом, сетью и настройками. Коллега пропустит --network — и nginx не найдёт приложение. Хочется записать всё это один раз.

Главная мысль

Compose читает файл compose.yaml с описанием сервисов и сам создаёт сеть, тома и контейнеры. Описание — желаемое состояние: docker compose up -d приводит машину к нему (создаёт недостающее, пересоздаёт изменённое, не трогает остальное), docker compose down убирает контейнеры и сеть. Тома down не удаляет — только down -v.

Новые слова

  • Сервис — описание одного контейнера в compose.yaml: образ или сборка, порты, тома, переменные.
  • Проект — все сервисы одного файла; имя проекта (name: rpd) становится префиксом имён контейнеров.
  • Подстановка переменных — ${WEB_PORT:-8080}: значение из окружения или файла .env, а если его нет — 8080.
  • .env — файл переменных рядом с compose.yaml; Compose читает его автоматически.

Файлы проекта

Папка ~/docker-course/rpd-notes. К файлам глав 3–5 добавятся три.

nginx/default.conf — nginx принимает запросы и передаёт их сервису app по имени (project/nginx/default.conf):

# nginx перед приложением «Заметки»: принимает запросы на порт 80 и передаёт их сервису app
server {
    listen 80;
    server_name _;

    location / {
        proxy_pass http://app:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

compose.yaml — версия этой главы; в главе 7 добавим проверку готовности и перезапуск:

# «Заметки»: приложение и nginx перед ним
name: rpd

services:
  app:
    build: .
    image: rpd-notes:compose
    environment:
      GREETING: ${GREETING:-Заметки}
    volumes:
      - notes-data:/data

  web:
    image: nginx:1.30-alpine
    ports:
      - "127.0.0.1:${WEB_PORT:-8080}:80"
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - app

volumes:
  notes-data:
    name: rpd-notes-data

networks:
  default:
    name: rpd-net

Что здесь записано:

  • name: rpd — имя проекта: контейнеры получат имена rpd-app-1, rpd-web-1.
  • app собирается из Dockerfile в этой папке (build: .), образ получит тег rpd-notes:compose. Порт не публикуется: снаружи к приложению можно попасть только через nginx.
  • web — готовый образ nginx. Порт опубликован только на 127.0.0.1; настройки подключены bind mount только для чтения (:ro).
  • depends_on — запускать web после app. Только после запуска, а не после готовности — об этом глава 7.
  • Том notes-data с явным именем rpd-notes-data, сеть по умолчанию — с именем rpd-net. Без явных имён Compose назвал бы их rpd_notes-data и rpd_default.

.env.example — пример настроек, который можно хранить в Git (project/.env.example):

# Пример настроек. Скопируйте: cp .env.example .env — и меняйте значения в .env.
# .env не попадает в Git и в образ (.gitignore, .dockerignore), но и не защищает пароли:
# это обычный текстовый файл, его прочитает любой, у кого есть доступ к папке.
GREETING=Заметки
WEB_PORT=8080

Рабочий .env создают копированием, и он не попадает ни в Git, ни в образ — для этого .gitignore и .dockerignore (глава 3):

cd ~/docker-course/rpd-notes
mkdir -p nginx
# создайте nginx/default.conf, compose.yaml и .env.example из листингов выше
cp .env.example .env
printf '.env\ndata/\n' > .gitignore

Важно про .env. Это обычный текстовый файл. Он не шифрует и не прячет значения: их прочитает любой, кто может читать папку, и увидит любой, кто выполнит docker compose config или docker inspect. Реальные пароли в .env учебного проекта не кладут; в рабочих системах для них есть отдельные хранилища секретов.

Разбор

Перед началом уберите учебные контейнеры и том главы 5, если остались: docker rm -f rpd-app rpd-bind, docker volume rm rpd-notes-data.

Проверить, что поймёт Compose

docker compose config

Команда печатает итоговое описание с подставленными переменными (фрагмент):

  web:
    depends_on:
      app:
        condition: service_started
…
    ports:
      - mode: ingress
        host_ip: 127.0.0.1
        target: 80
        published: "8080"

Опечатки в YAML и неизвестные ключи Compose покажет здесь, ничего не запуская.

Поднять

docker compose up -d --build

Значимые строки (в терминале Compose рисует их как обновляющийся список; здесь — без повторов):

 Volume rpd-notes-data Created
 Network rpd-net Created
 Container rpd-app-1 Created
 Container rpd-web-1 Created
 Container rpd-app-1 Started
 Container rpd-web-1 Started

--build — собрать образ app перед запуском (иначе Compose соберёт его только если образа ещё нет).

docker compose ps
NAME        IMAGE               COMMAND                  SERVICE   CREATED        STATUS                  PORTS
rpd-app-1   rpd-notes:compose   "python3 server.py"      app       1 second ago   Up Less than a second   8000/tcp
rpd-web-1   nginx:1.30-alpine   "nginx -g 'daemon of…"   web       1 second ago   Up Less than a second   127.0.0.1:8080->80/tcp

Проверить путь запроса

curl -sS -i http://127.0.0.1:8080/
HTTP/1.1 200 OK
Server: nginx/1.30.x
Content-Type: text/plain; charset=utf-8
X-App: rpd-notes

Заметки
Заметок: 0

Часть заголовков (Date, Content-Length, Connection) опущена. Два признака того, что запрос прошёл весь путь: Server: nginx — ответил nginx, X-App: rpd-notes — ответ сделало приложение. Версия nginx в заголовке зависит от образа.

curl -sS -d 'Заметка через nginx' http://127.0.0.1:8080/notes
docker compose logs app
Сохранено. Заметок: 1
app-1  | notes: слушаю 0.0.0.0:8000, данные в /data
app-1  | notes: GET / 200
app-1  | notes: POST /notes 201

Журналы всех сервисов — docker compose logs, одного — docker compose logs web, в реальном времени — -f. Команда в сервисе — docker compose exec app id (→ uid=10001 …).

Изменить настройку

Поменяйте заголовок в .env и снова выполните up -d:

sed -i 's/^GREETING=.*/GREETING=Заметки учебной группы/' .env
docker compose up -d
 Container rpd-web-1 Running
 Container rpd-app-1 Recreate
 Container rpd-app-1 Recreated
 Container rpd-app-1 Started

Compose пересоздал только app — у него изменилась переменная. web не тронут. Заметка на месте — она в томе: curl -sS http://127.0.0.1:8080/ → Заметки учебной группы, Заметок: 1.

down и down -v

docker compose down
docker volume ls --filter name=rpd-
 Container rpd-web-1 Removed
 Container rpd-app-1 Removed
 Network rpd-net Removed
DRIVER    VOLUME NAME
local     rpd-notes-data

Контейнеры и сеть удалены, том — нет. docker compose up -d поднимет всё снова, и заметка вернётся. А вот:

docker compose down -v
 …
 Volume rpd-notes-data Removed

-v удаляет тома проекта — учебные заметки пропали безвозвратно. На рабочем проекте down -v — то же, что удалить базу данных. Используйте его, только когда данные осознанно не нужны.

Практика: другой порт без потери данных

Исходное состояние: проект поднят (docker compose up -d), в нём есть хотя бы одна заметка (сохраните, если нет).

  1. Переведите сайт на порт 8081, не трогая compose.yaml.
  2. Убедитесь, что 127.0.0.1:8080 больше не отвечает, а 127.0.0.1:8081 отдаёт ваши заметки.
  3. Определите по выводу up -d, какой сервис Compose пересоздал и почему не тронул другой.
  4. Верните порт 8080 и остановите проект так, чтобы заметки сохранились.

Признаки успеха: curl -sS http://127.0.0.1:8081/notes показывает заметки; после остановки docker volume ls --filter name=rpd- показывает rpd-notes-data.

Подсказка 1

Порт в compose.yaml записан через переменную. Где Compose берёт её значение?

Подсказка 2

Поменяйте WEB_PORT в .env и выполните docker compose up -d. Порт — свойство сервиса web.

Подсказка 3

Остановить с сохранением данных — docker compose down без -v.

Решение — в answers/06-compose.md.

Если результат другой

  • docker: 'compose' is not a docker command. Не установлен плагин Compose v2. Старая отдельная программа docker-compose (через дефис) устарела; курс рассчитан на docker compose.
  • web сразу завершается, в docker compose logs web — host not found in upstream "app". nginx не нашёл сервис по имени: опечатка в proxy_pass или сервис называется иначе. Разбор — в главе 8.
  • curl к 8080 отвечает Connection reset/Empty reply. Посмотрите docker compose ps: работают ли оба сервиса, и куда опубликован порт.
  • Изменили nginx/default.conf, а nginx работает по-старому. Файл подключён в контейнер, но nginx читает его при запуске: docker compose restart web.

Проверка понимания

  1. Вы выполнили docker compose down, потом docker compose up -d. Заметки на месте? А после down -v?
  2. Почему сервису app не нужна публикация порта, хотя nginx к нему обращается?

Ответы — в answers/06-compose.md.

Дальше: 7. Запуск и готовность.

Разбор и проверка понимания

Лист ученика: 06-compose.md.

Практика

Папка ~/docker-course/rpd-notes.

1–2. Другой порт. Порт в compose.yaml — "127.0.0.1:${WEB_PORT:-8080}:80", значит, меняем .env:

sed -i 's/^WEB_PORT=.*/WEB_PORT=8081/' .env
docker compose up -d
curl -sS http://127.0.0.1:8080/
curl -sS http://127.0.0.1:8081/notes
 Container rpd-app-1 Running
 Container rpd-web-1 Recreate
 Container rpd-web-1 Recreated
 Container rpd-web-1 Started
curl: (7) Failed to connect to 127.0.0.1 port 8080 after 0 ms: Couldn't connect to server
Первая

3. Пересоздан web: изменилась его публикация порта. app — Running, его описание не менялось, данные он и так держит в томе.

4. Вернуть и остановить с сохранением данных:

sed -i 's/^WEB_PORT=.*/WEB_PORT=8080/' .env
docker compose up -d
docker compose down
docker volume ls --filter name=rpd-
DRIVER    VOLUME NAME
local     rpd-notes-data

Можно и сразу docker compose down после проверки — порт в .env всё равно вернуть для следующих глав.

Воспроизведено: Docker Engine 29.8.2, Compose 5.5.1, образы-заменители.

Проверка понимания

  1. После down + up -d — на месте: down удаляет контейнеры и сеть, но не тома. После down -v том rpd-notes-data удалён, заметок нет.
  2. Публикация порта нужна для доступа с хоста. nginx обращается к приложению внутри сети проекта rpd-net по имени app — напрямую к порту 8000 контейнера. К тому же без публикации приложение нельзя открыть в обход nginx.

Прочитали и разобрались? Отметьте урок — прогресс сохранится в этом браузере.