Запустить приложение в Compose легко, а вот обновить его так, чтобы посетитель не увидел ошибку 502, — задача посложнее. Разберём полный стек: Nginx на внешних портах, приложение во внутренней сети, сертификаты Let’s Encrypt и деплой, при котором старая версия продолжает обслуживать запросы, пока новая не прошла healthcheck. Все конфигурации живут на хосте, контейнеры остаются одноразовыми.

Структура стека

Определим четыре сервиса: nginx, два варианта приложения — синий и зелёный, — и certbot. Два приложения нужны именно для переключения: пока новая версия прогревается, старая ещё отдаёт трафик.

services:
  nginx:
    image: nginx:1.27-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./nginx/backend.conf:/etc/nginx/backend.conf:ro
      - ./certbot/conf:/etc/letsencrypt:ro
      - ./certbot/www:/var/www/certbot:ro
  app_blue:
    build: .
    expose: ["8000"]
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8000/healthz"]
      interval: 5s
      timeout: 3s
      retries: 12
  app_green:
    build: .
    expose: ["8000"]
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8000/healthz"]
      interval: 5s
      timeout: 3s
      retries: 12
  certbot:
    image: certbot/certbot
    volumes:
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot

Приложение не публикует порты наружу — его видит только Nginx внутри общей сети. Здоровье контролирует healthcheck: если приложение не отвечает на /healthz, Docker пометит контейнер unhealthy, и вы увидите это в docker compose ps ещё до переключения трафика.

Секреты и данные, которые переживают перезапуск

Контейнеры должны быть одноразовыми, а состояние — на хосте. Базу, загруженные файлы и логи приложения выносите в именованные тома или bind-пути, иначе команда docker compose down -v заодно удалит пользовательские данные. Секреты не хардкодьте в docker-compose.yml: приложение читает переменные из файла .env рядом с ним, Compose подставит их автоматически, а сам .env добавьте в .gitignore. Пароль базы держите в отдельном файле с правами 600 и монтируйте его в контейнер, чем передавайте через аргументы сборки — аргументы остаются в истории образа и видны любому, кто сможет выполнить docker image inspect.

Конфигурация Nginx и подмена бэкенда

Ключевой приём — переменная в proxy_pass вместе с локальным DNS-резолвером Docker. Без этого Nginx резолвит имя контейнера один раз при старте и падает, если бэкенд ещё не запущен.

server {
    listen 80;
    server_name example.com;
    location /.well-known/acme-challenge/ { root /var/www/certbot; }
    location / { return 301 https://$host$request_uri; }
}
server {
    listen 443 ssl;
    http2 on;
    server_name example.com;
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    location / {
        resolver 127.0.0.11 valid=10s;
        include /etc/nginx/backend.conf;
        proxy_next_upstream error timeout http_502;
        proxy_connect_timeout 2s;
    }
}

Файл /etc/nginx/backend.conf на хосте содержит ровно одну строку: set $backend http://app_blue:8000;. Именно её мы будем менять при деплое.

Получение и продление сертификатов

Первый сертификат получаем webroot-методом: Nginx отдаёт файлы из каталога вызова, certbot кладёт токен и подтверждает владение доменом.

docker compose run --rm certbot certonly --webroot \
    -w /var/www/certbot -d example.com -d www.example.com \
    --agree-tos -m admin@example.com --non-interactive
openssl x509 -enddate -noout -in ./certbot/conf/live/example.com/fullchain.pem

Продление оформляем скриптом на хосте: он запускает renew и сразу перечитывает конфигурацию Nginx, иначе новые сертификаты подхватятся лишь после ручного перезапуска.

#!/bin/sh
set -e
cd /opt/site
docker compose run --rm certbot renew
docker compose exec -T nginx nginx -t
docker compose exec -T nginx nginx -s reload

Скрипт кладём в /etc/cron.d или планируем таймером на 03:17 ежедневно. Периодически проверяйте выпуск: openssl x509 -enddate читает дату окончания, а docker compose run —rm certbot renew —dry-run показывает, что продление сработает.

Деплой без простоя

Сценарий обновления: собрали образ, подняли контейнер второй версии, дождались здоровья, переключили Nginx, остановили старый контейнер. Пока не выполнен шаг переключения, трафик идёт на старую версию.

#!/bin/sh
set -e
cd /opt/site
NEW=$1
if [ "$NEW" = blue ]; then OLD=green; else OLD=blue; fi
docker compose build app_$NEW
docker compose up -d app_$NEW
CID=$(docker compose ps -q app_$NEW)
until [ "$(docker inspect -f '{{.State.Health.Status}}' "$CID")" = healthy ]; do sleep 2; done
printf 'set $backend http://app_%s:8000;\n' "$NEW" > nginx/backend.conf
docker compose exec -T nginx nginx -t
docker compose exec -T nginx nginx -s reload
sleep 30
docker compose stop app_$OLD

Почему это работает без обрыва соединений: nginx -s reload выполняется мягко, воркеры дорабатывают открытые запросы, а новые сразу идут на новый бэкенд. Старый контейнер останавливается только через паузу, которая перекрывает время жизни медленных запросов. Если новая версия оказалась битой, откат — это повторный запуск скрипта с указанием старого цвета, пока тот ещё не остановлен.

Диагностика и типичные проблемы

  • docker compose ps показывает состояние и healthcheck, логи конкретной версии — docker compose logs -f —since 10m app_green;
  • всегда проверяйте файл перед применением: docker compose config -q выдаст ошибку синтаксиса без перезапуска сервисов;
  • healthcheck вечно unhealthy почти всегда означает, что в образе нет curl или нет эндпоинта /healthz — проверьте wget или смените команду;
  • первая установка: docker compose up -d —wait поднимет стек и дождётся здоровых контейнеров;
  • ошибка host not found in upstream появляется при старте без resolver и переменной — вернитесь к блоку с 127.0.0.11;
  • не используйте docker compose restart для Nginx: это рвёт соединения, в отличие от nginx -s reload;
  • docker compose stats показывает в реальном времени, кто из сервисов съедает память, а docker compose top показывает, какие процессы живут внутри контейнера; это помогает отличить утечку приложения от проблем с диском хоста.

Итог

Нулевой простой в Compose достигается не магией оркестратора, а простой последовательностью: healthcheck новой версии, переключение бэкенда через переменную и мягкая перезагрузка Nginx, и лишь потом остановка старого контейнера. Тот же принцип работает с сертификатами — renew и reload в одном скрипте по таймеру. Такой стек из четырёх контейнеров остаётся полностью прозрачным: каждый шаг виден в docker compose ps и журнале, а откат укладывается в одну команду.

Насколько публикация полезна?

Нажмите на звезду, чтобы оценить!

Средняя оценка 0 / 5. Количество оценок: 0

Оценок пока нет. Поставьте оценку первым.

Поделиться: Telegram ВКонтакте