Стратегия обновления 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

Авторское право 2026 Virtua.Cloud. Все права защищены. Данный контент является оригинальным произведением команды Virtua.Cloud. Воспроизведение, повторная публикация или распространение без письменного разрешения запрещены.

Готовы попробовать?

Разверните свой сервер за секунды. Linux, Windows или FreeBSD.

Смотреть тарифы VPS
Обновление Docker без простоя на VPS