Стратегия обновления Docker: обновление контейнеров без простоя на VPS

9 мин чтения·Matthieu·containerstraefikzero-downtimedocker-composedocker|

Четыре метода обновления Docker-контейнеров на VPS по возрастанию сложности — от простого pull-and-replace до blue-green деплоя без простоя с Traefik. Пиннинг образов, откат, уведомления Diun и docker-rollout.

Обновление Docker-контейнеров на VPS не обязательно означает простой. Правильный подход зависит от того, что ты запускаешь и сколько времени простоя можешь себе позволить. Личный блог переживёт пару секунд даунтайма при docker compose up -d. SaaS-продукт с платящими клиентами — нет.

Это руководство охватывает четыре метода, от простейшего до самого надёжного. Каждый следующий строится на предыдущем. Начни с того, что подходит под твою ситуацию, и переходи на следующий уровень, когда понадобится.

Требования: VPS с Debian 12 или Ubuntu 24.04, Docker Engine 27+ и Docker Compose v2. Все команды используют синтаксис плагина docker compose (не устаревший бинарник docker-compose v1). Docker в продакшене на VPS: что ломается и как это починить

Как закрепить Docker-образы на конкретной версии?

Закрепи образы на конкретной minor- или patch-версии в compose-файле. Тег latest — движущаяся цель, которая может подтянуть ломающие изменения без предупреждения. Пиннинг даёт контроль над тем, когда происходят обновления, и позволяет откатиться, сохраняя предыдущий образ локально.

Разные стратегии тегов несут разные риски:

Формат тега Пример Уровень риска Поведение при обновлении
latest nginx:latest Высокий Любая версия в любой момент. Непонятно, что изменилось.
Только major nginx:1 Средне-высокий Может перескочить с 1.25 на 1.27. Minor-версии могут менять поведение.
Minor nginx:1.27 Низкий Получает patch-обновления (1.27.0 → 1.27.3). Безопасно для большинства рабочих нагрузок.
Patch nginx:1.27.3 Очень низкий Точная версия. Никаких сюрпризов. Обновляешь вручную.
Digest nginx:1.27.3@sha256:6f12... Минимальный Побайтово идентичный образ каждый раз. Защищён от мутации тегов.

Для большинства продакшн-сервисов закрепляй на minor-версии (image: postgres:16.6). Это правильный баланс между патчами безопасности и стабильностью. Для сервисов, где важна воспроизводимость (CI, регулируемые среды), закрепляй на полном digest.

services:
  app:
    image: myapp:2.4.1
    # Not: image: myapp:latest
  db:
    image: postgres:16.6

Запиши digest текущих образов перед обновлением. Они понадобятся для отката:

docker image inspect --format='{{index .RepoDigests 0}}' $(docker compose images app -q)
myapp@sha256:a1b2c3d4e5f6...

Как настроить health check в Docker Compose?

Health check сообщает Docker, действительно ли контейнер работает, а не просто запущен. Все паттерны обновления без простоя зависят от них. Без health check Docker не может узнать, готов ли новый контейнер, прежде чем удалить старый.

Добавь блок healthcheck к каждому сервису в compose-файле. Команда test выполняется внутри контейнера с заданным интервалом. Docker помечает контейнер как healthy только после прохождения теста.

services:
  app:
    image: myapp:2.4.1
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 15s
      timeout: 5s
      retries: 3
      start_period: 30s

Что делает каждое поле:

  • test: Команда для выполнения. CMD запускает её напрямую. Используй CMD-SHELL, если нужны возможности shell вроде пайпов.
  • interval: Время между проверками. 15 секунд — разумное значение для веб-сервисов.
  • timeout: Сколько ждать завершения команды, прежде чем считать её проваленной.
  • retries: Количество последовательных неудач, после которых Docker пометит контейнер как unhealthy.
  • start_period: Льготный период после запуска контейнера. Health check во время этого окна не учитываются в пороге отказов. Установи достаточно времени для загрузки приложения.

Для сервисов без установленного curl используй встроенную проверку самого сервиса:

  db:
    image: postgres:16.6
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myuser -d mydb"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 20s

  cache:
    image: redis:7.4
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3

После запуска сервисов проверь, что health check проходят:

docker compose ps
NAME       IMAGE           STATUS                    PORTS
app        myapp:2.4.1     Up 2 minutes (healthy)    0.0.0.0:8080->8080/tcp
db         postgres:16.6   Up 2 minutes (healthy)    5432/tcp

Статус (healthy) означает, что health check настроен и проходит. Если видишь (health: starting), контейнер ещё в start_period. Если (unhealthy), смотри логи health check:

docker inspect --format='{{json .State.Health}}' $(docker compose ps -q app) | python3 -m json.tool

Как обновить Docker-контейнер на VPS?

Выполни docker compose pull, чтобы скачать новый образ, затем docker compose up -d, чтобы заменить контейнер. Docker Compose останавливает старый контейнер, удаляет его и запускает новый из обновлённого образа. Это вызывает краткое прерывание (обычно 2–10 секунд), пока новый контейнер стартует и проходит health check.

Пошагово: простое обновление

Перед обновлением сделай бэкап volumes. Сломанное обновление с повреждёнными данными куда хуже пары секунд простоя.

Прочитай changelog новой версии. Проверь наличие ломающих изменений, устаревших опций конфигурации и необходимых шагов миграции. Это займёт пять минут, но сэкономит часы отладки.

# Pull the new image
docker compose pull app

# Check what changed
docker compose up -d --dry-run

Флаг --dry-run (Docker Compose v2.20+) показывает, что Compose собирается сделать, не выполняя это на самом деле. Ты увидишь, какие контейнеры будут пересозданы:

DRY RUN MODE - service "app" - Pull
DRY RUN MODE - Container app-1 - Recreate
DRY RUN MODE - Container app-1 - Started

Примени обновление:

docker compose up -d app
[+] Running 1/1Container app-1  Started    0.8s

Проверь, что новый контейнер healthy:

docker compose ps app
NAME       IMAGE           STATUS                    PORTS
app        myapp:2.5.0     Up 15 seconds (healthy)   0.0.0.0:8080->8080/tcp

Затем проверь снаружи сервера, что сервис доступен:

curl -s -o /dev/null -w "%{http_code}" https://app.example.com/health
200

Когда обновлять Docker-контейнеры?

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

  • Патчи безопасности (CVE): Применяй немедленно. Подпишись на уведомления о безопасности своих образов. Известная CVE в публично доступном контейнере эксплуатируется в течение часов после раскрытия, не дней.
  • Patch-версии (например, 2.4.1 → 2.4.2): Планируй еженедельно или раз в две недели. Это баг-фиксы. Читай changelog, обновляй, проверяй.
  • Minor-версии (например, 2.4 → 2.5): Планируй ежемесячно. Сначала протестируй в staging-окружении, если оно есть. Проверь changelog на изменения поведения.
  • Major-версии (например, 2.x → 3.x): Планируй и тестируй. Major-версии ломают вещи. Читай руководство по миграции. Тестируй на отдельном VPS или локально, прежде чем трогать продакшн.

Как откатить Docker-контейнер на предыдущий образ?

В Docker Compose нет встроенной команды отката. Чтобы вернуться: отредактируй compose-файл, закрепив предыдущий тег или digest образа, затем выполни docker compose up -d. Контейнер перезапустится со старым образом. Это работает, только если старый образ сохранился локально (не запускай docker image prune сразу после обновления).

Пошаговый откат

Допустим, ты обновил myapp с 2.4.1 до 2.5.0, и новая версия сломана.

  1. Проверь, что старый образ ещё доступен локально:
docker images myapp
REPOSITORY   TAG     IMAGE ID       CREATED        SIZE
myapp        2.5.0   abc123def456   2 hours ago    185MB
myapp        2.4.1   789fed654cba   2 weeks ago    182MB
  1. Отредактируй compose-файл, закрепив предыдущую версию:
services:
  app:
    image: myapp:2.4.1
  1. Выполни откат:
docker compose up -d app
[+] Running 1/1Container app-1  Started    0.7s
  1. Убедись, что откат прошёл успешно:
docker compose ps app
NAME       IMAGE           STATUS                   PORTS
app        myapp:2.4.1     Up 10 seconds (healthy)  0.0.0.0:8080->8080/tcp

Если старый образ уже удалён, Docker скачает его заново из реестра (при условии, что тег ещё существует). Для максимальной надёжности запиши полный digest (sha256:...) перед обновлением. Digest неизменяемы. Теги могут быть перезаписаны.

Автоматизация с помощью скрипта перед обновлением

Сохраняй текущее состояние перед каждым обновлением, чтобы откат всегда был в одной команде:

#!/bin/bash
# save-state.sh - Run before every update
COMPOSE_FILE="${1:-docker-compose.yml}"
DATE=$(date +%Y%m%d-%H%M%S)
BACKUP_DIR="./rollback/${DATE}"

mkdir -p "${BACKUP_DIR}"
cp "${COMPOSE_FILE}" "${BACKUP_DIR}/"
docker compose ps --format json > "${BACKUP_DIR}/containers.json"
docker compose images --format json > "${BACKUP_DIR}/images.json"

echo "State saved to ${BACKUP_DIR}"
chmod 700 save-state.sh

Watchtower ещё поддерживается в 2026?

Watchtower был архивирован 17 декабря 2025. Мейнтейнеры больше не используют Docker и прекратили разработку. Последний релиз — v1.7.1. Ещё важнее то, что Docker SDK в Watchtower использует API v1.25, а Docker Engine 29 поднял минимальную версию API до v1.44. Watchtower несовместим с текущими версиями Docker, если не понизить минимум API вручную через DOCKER_MIN_API_VERSION=1.25 в конфигурации daemon. Это костыль, а не решение.

Если ты используешь Watchtower сейчас, планируй миграцию. Для автоматических уведомлений об обновлениях без автоматического перезапуска используй Diun. Для автоматических обновлений без простоя используй docker-rollout за reverse proxy.

Как Diun уведомляет об обновлениях Docker-образов?

Diun (Docker Image Update Notifier) мониторит твои Docker-реестры и отправляет уведомления, когда доступны новые версии образов. Он не обновляет контейнеры. Он сообщает, что обновление существует, чтобы ты мог прочитать changelog и обновиться на своих условиях. Это подход «сначала узнай, потом действуй».

Добавь Diun в существующий compose-файл или создай отдельный:

services:
  diun:
    image: crazymax/diun:4
    command: serve
    volumes:
      - "diun-data:/data"
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
    environment:
      TZ: "Europe/Berlin"
      DIUN_WATCH_WORKERS: "10"
      DIUN_WATCH_SCHEDULE: "0 6 * * *"
      DIUN_PROVIDERS_DOCKER: "true"
      DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT: "true"
      DIUN_NOTIF_SLACK_WEBHOOKURL_FILE: "/run/secrets/slack_webhook"
    secrets:
      - slack_webhook
    restart: unless-stopped

secrets:
  slack_webhook:
    file: ./secrets/slack_webhook.txt

volumes:
  diun-data:

URL Slack-вебхука хранится в файле секретов, а не в переменной окружения, потому что Docker secrets скрывает его от вывода docker inspect и списков процессов. Создай файл секретов с ограниченными правами:

mkdir -p secrets
echo "https://hooks.slack.com/services/YOUR/WEBHOOK/URL" > secrets/slack_webhook.txt
chmod 600 secrets/slack_webhook.txt

Основные настройки:

  • DIUN_WATCH_SCHEDULE: Cron-выражение. 0 6 * * * проверяет ежедневно в 06:00. Подстрой под своё окно обслуживания.
  • DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT: При true Diun мониторит все запущенные контейнеры. Установи false и используй labels для выборочного мониторинга.
  • Монтирование Docker-сокета: Только для чтения (:ro), потому что Diun только читает метаданные контейнеров. Он никогда не запускает и не останавливает контейнеры.

Для выборочного мониторинга (рекомендуется для больших стеков) установи WATCHBYDEFAULT в false и добавь labels к контейнерам, которые хочешь мониторить:

services:
  app:
    image: myapp:2.4.1
    labels:
      - "diun.enable=true"
      - "diun.watch_repo=true"

Запусти Diun и проверь логи:

docker compose up -d diun
docker compose logs diun --tail 20
diun  | Thu, 19 Mar 2026 06:00:01 CET INF Starting Diun version=v4.31.0
diun  | Thu, 19 Mar 2026 06:00:01 CET INF Configuration loaded from 5 environment variable(s)
diun  | Thu, 19 Mar 2026 06:00:02 CET INF Cron triggered
diun  | Thu, 19 Mar 2026 06:00:03 CET INF New image found image=docker.io/myapp:2.5.0 provider=docker

Когда Diun находит новый образ, он отправляет сообщение в Slack с именем образа, текущим тегом и новым тегом. Ты сам решаешь, обновляться ли и когда.

Как docker-rollout обеспечивает обновления без простоя?

docker-rollout — это плагин Docker CLI, который выполняет blue-green деплой для Compose-сервисов. Он запускает новый контейнер из обновлённого образа, ждёт прохождения health check, затем удаляет старый контейнер. Трафик никогда не попадает на неработающий контейнер, потому что reverse proxy маршрутизирует только к healthy контейнерам.

Требования:

  • Reverse proxy (Traefik, Caddy или nginx-proxy), маршрутизирующий трафик к сервису
  • Health check определены в compose-файле
  • Нет директивы container_name у сервиса (docker-rollout управляет именами контейнеров)
  • Нет прямого маппинга ports у сервиса (reverse proxy управляет открытием портов)

Установка docker-rollout

mkdir -p /usr/local/lib/docker/cli-plugins
curl -fsSL https://raw.githubusercontent.com/wowu/docker-rollout/main/docker-rollout \
  -o /usr/local/lib/docker/cli-plugins/docker-rollout
chmod +x /usr/local/lib/docker/cli-plugins/docker-rollout

После установки должна отобразиться версия:

docker rollout --version
docker-rollout version v0.13

Пример: Traefik + docker-rollout

Минимальный compose-файл для веб-приложения за Traefik с health check. У приложения нет ports и container_name, потому что docker-rollout должен управлять масштабированием.

services:
  traefik:
    image: traefik:3.3
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
    ports:
      - "80:80"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
    restart: unless-stopped

  app:
    image: myapp:2.4.1
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.app.rule=Host(`app.example.com`)"
      - "traefik.http.routers.app.entrypoints=web"
      - "traefik.http.services.app.loadbalancer.server.port=8080"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 15s
      timeout: 5s
      retries: 3
      start_period: 30s
    restart: unless-stopped

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

Скачай новый образ и используй docker rollout вместо docker compose up -d:

docker compose pull app
docker rollout app
==> Scaling 'app' to '2' instances
 Container myproject-app-2 Creating
 Container myproject-app-2 Created
 Container myproject-app-2 Starting
 Container myproject-app-2 Started
==> Waiting for new containers to be healthy (timeout: 60 seconds)
==> Stopping and removing old containers

Во время этого процесса Traefik обнаруживает новый контейнер через Docker-сокет, направляет трафик на него после перехода в состояние healthy и прекращает маршрутизацию к старому контейнеру перед его удалением. Пользователи не замечают прерывания.

Если новый контейнер не проходит health check, docker-rollout отменяет операцию, и старый контейнер продолжает работать. Ручное вмешательство не требуется.

Что такое blue-green деплой с Docker и Traefik?

Blue-green деплой запускает две копии сервиса (blue и green). Одна обслуживает живой трафик, другая простаивает. Для деплоя обновляешь неактивную копию, проверяешь её работоспособность, затем переключаешь трафик. Это даёт мгновенный откат — достаточно переключить обратно на предыдущую копию.

Это концепция, стоящая за docker-rollout, но ты можешь реализовать её вручную для большего контроля. Минимальный пример с динамической конфигурацией Traefik:

services:
  traefik:
    image: traefik:3.3
    command:
      - "--providers.file.directory=/etc/traefik/dynamic"
      - "--providers.file.watch=true"
      - "--entrypoints.web.address=:80"
    ports:
      - "80:80"
    volumes:
      - "./traefik/dynamic:/etc/traefik/dynamic:ro"
    restart: unless-stopped

  app-blue:
    image: myapp:2.4.1
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 15s
      timeout: 5s
      retries: 3
      start_period: 30s

  app-green:
    image: myapp:2.4.1
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 15s
      timeout: 5s
      retries: 3
      start_period: 30s

Файл динамической конфигурации Traefik определяет, какая копия получает трафик:

# traefik/dynamic/app.yml
http:
  routers:
    app:
      rule: "Host(`app.example.com`)"
      service: app
      entryPoints:
        - web
  services:
    app:
      loadBalancer:
        servers:
          - url: "http://app-blue:8080"

Для деплоя: обнови образ app-green, запусти его, дождись перехода в healthy, затем отредактируй app.yml, чтобы он указывал на app-green. Traefik подхватит изменение автоматически, потому что watch=true. Для отката отредактируй файл, чтобы он снова указывал на app-blue.

Этот подход требует больше работы, чем docker-rollout. Используй его, когда нужен явный контроль над переключением, когда хочешь прогнать smoke-тесты на новой версии перед переключением трафика, или когда несколько сервисов должны переключиться одновременно.

Какой метод обновления выбрать?

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

Метод Простой Сложность Откат Нужен reverse proxy Подходит для
docker compose pull + up 2–10 секунд Низкая Ручной (редактирование compose-файла) Нет Личные проекты, внутренние инструменты
Diun + ручное обновление То же Низкая То же Нет Команды, которым нужна видимость перед обновлением
docker-rollout Нет Средняя Автоматический (отмена при сбое) Да Продакшн-сервисы на одном VPS
Blue-green (ручной) Нет Высокая Мгновенный (переключение конфиг-файла) Да Многосервисные стеки, регулируемые среды

Порядок принятия решения:

  1. 2–10 секунд простоя допустимы? Используй docker compose pull && docker compose up -d.
  2. Хочешь знать об обновлениях до их применения? Добавь Diun.
  3. Нужен нулевой простой? Есть reverse proxy? Используй docker-rollout.
  4. Нужен явный контроль над переключением трафика? Реализуй blue-green вручную.

Что-то пошло не так?

Контейнер запускается, но показывает (unhealthy)

Проверь команду health check. Выполни её вручную внутри контейнера:

docker compose exec app curl -f http://localhost:8080/health

Если не проходит, проблема в приложении, а не в Docker. Смотри логи приложения:

docker compose logs app --tail 50

Старый образ удалён, откат невозможен

Если тег ещё существует в реестре, docker compose pull скачает его. Если закреплял по digest, Docker скачает точный образ независимо от изменений тегов:

image: myapp:2.4.1@sha256:789fed654cba...

docker-rollout зависает во время деплоя

Health check не проходит в отведённое время. Проверь interval и retries health check. Увеличь timeout:

docker rollout -t 120 app

Watchtower перестал работать после обновления Docker

Docker Engine 29 требует минимум API v1.44. Watchtower использует API v1.25. Мигрируй на Diun для уведомлений или docker-rollout для автоматических обновлений без простоя.

Diun не обнаруживает новые образы

Проверь cron-расписание в DIUN_WATCH_SCHEDULE. Запусти сканирование вручную:

docker compose exec diun diun image list

Проверь логи Diun на ошибки аутентификации в реестре:

docker compose logs diun --tail 30