Стратегия обновления Docker: обновление контейнеров без простоя на VPS
Четыре метода обновления 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/1
✔ Container 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, и новая версия сломана.
- Проверь, что старый образ ещё доступен локально:
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
- Отредактируй compose-файл, закрепив предыдущую версию:
services:
app:
image: myapp:2.4.1
- Выполни откат:
docker compose up -d app
[+] Running 1/1
✔ Container app-1 Started 0.7s
- Убедись, что откат прошёл успешно:
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: При
trueDiun мониторит все запущенные контейнеры. Установи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 (ручной) | Нет | Высокая | Мгновенный (переключение конфиг-файла) | Да | Многосервисные стеки, регулируемые среды |
Порядок принятия решения:
- 2–10 секунд простоя допустимы? Используй
docker compose pull && docker compose up -d. - Хочешь знать об обновлениях до их применения? Добавь Diun.
- Нужен нулевой простой? Есть reverse proxy? Используй docker-rollout.
- Нужен явный контроль над переключением трафика? Реализуй 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
Готовы попробовать?
Разверните свой сервер за секунды. Linux, Windows или FreeBSD. →