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), в нём есть хотя бы одна заметка (сохраните, если нет).
- Переведите сайт на порт
8081, не трогаяcompose.yaml. - Убедитесь, что
127.0.0.1:8080больше не отвечает, а127.0.0.1:8081отдаёт ваши заметки. - Определите по выводу
up -d, какой сервис Compose пересоздал и почему не тронул другой. - Верните порт
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.
Проверка понимания
- Вы выполнили
docker compose down, потомdocker compose up -d. Заметки на месте? А послеdown -v? - Почему сервису
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, образы-заменители.
Проверка понимания
- После
down+up -d— на месте:downудаляет контейнеры и сеть, но не тома. Послеdown -vтомrpd-notes-dataудалён, заметок нет. - Публикация порта нужна для доступа с хоста. nginx обращается к приложению внутри сети проекта
rpd-netпо имениapp— напрямую к порту 8000 контейнера. К тому же без публикации приложение нельзя открыть в обход nginx.
Прочитали и разобрались? Отметьте урок — прогресс сохранится в этом браузере.