Не забывайте, что обновление без даун-тайма доступно только если вы сперва проверили все изменения на тестовой среде. И только после того как убедились, что там всё работает можно обновлять приложение. Ну и конечно тестовая среда должна максимально соответствовать боевой среде, кроме данных в базе данных.
Конечно же если обновляемый сервис представляет из себя простое веб-приложение или обычный статическую веб-страницу, то как правило проблем с обновлением такого сервиса не будет. Но вот обновление таких сервисов как база данных или любое хранилище, где есть важные данные требует особого внимания. Некоторые продукты имеют свои ограничения на обновление, например, с какой версии на какую можно обновиться. Всегда проверяйте такие ограничения заранее, тот факт, что вы используете контейнеризацию, не освобождает вас от таких ограничений. И ещё раз повторюсь перед тем как делать обновление в боевой среде попробуйте сделать это на тестовой среде. Может так произойти что новый контейнер запустится, но работать будет неправильно, и что еще хуже повредит важные данные. Поэтому важно использовать правильный healthcheck.
Отдельной команды для обновления стека в Docker Swarm нету, тут также используется команда docker stack deploy, при выполнении которой демон Docker проверяет были ли изменения в файле конфигурации по сравнению с текущим состоянием сервиса. И если такие есть он их применяет.
swarm-update-best-practices.svg
Обновление стека
Как я уже говорил при обновлении стека всё начинается с изменения файла конфигурации этого стека. После изменения конфигурации выполняется команда docker stack deploy -c conf.yml mystack. Docker используя разные API проверяет необходимо ли что-то обновить в стеке для актуализации стека.
Допустим стек называется full-app, файл full-app.yml я поправил (новый image, env, ports, secrets и т.д.). Команда обновления стека будет выглядеть так: docker stack deploy -c full-app.yml full-app.
docker stack deploy -c full-app.yml full-app
docker service logs -f full-app_api
vim full-app.yml
ports:
- "8080:8000"
deploy:
replicas: 3
docker stack deploy --detach=false -c full-app.yml full-app
Updating service full-app_api (id: jkpbmz088nsgyvt0fdhyueqhj)
1/3: running [==================================================>]
2/3: running [==================================================>]
3/3: ready [======================================> ]
Updating service full-app_db (id: veg3bbimuapyqpj5y99s52bq3)
curl http://172.31.144.9:8080/items
[{"created_at":"Fri, 19 Dec 2025 06:19:48 GMT","id":1,"name":"adasdasd"},{"created_at":"Fri, 19 Dec 2025 06:19:50 GMT","id":2,"name":"fadsfasf"}]
swarm-stack-update-process.svg
Пример: обновить secret “правильно” (secrets immutable)
Для этого примера я буду использовать этот файл конфигурации, где по сути тоже самое что и выше, но только с использованием переменных (psql_user, psql_pwd). Моя задача тут поменять пароль, т.е. я создаю новый секрет psql_pwd_v2 и заменяю в файле старый psql_pwd. После чего делаю снова deploy.
printf "NEWPASS" | docker secret create psql_pwd_v2 -
docker secret ls
lbcx1yqjiuwcioorjbwjjdlyf psql_pwd_v2
docker stack deploy --detach=false -c full-app.yml full-app-2
vim full-app.yml
services:
api:
environment:
DB_PASSWORD_FILE: /run/secrets/psql_pwd_v2
secrets:
- psql_pwd_v2
db:
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/psql_pwd_v2
secrets:
- psql_pwd_v2
secrets:
psql_pwd_v2:
external: true
docker stack deploy -c stack.yml full-app1
Но нужно понимать что в самой базе данных postgresql пароль не поменяется т поэтому в логах сервиса full-app-2_db я буду видеть ошибки: FATAL: password authentication failed for user "app".
Т.е. мне необходимо было сперва сменить пароль пользователю app, ну или добавить другого пользователя и создать уже новый секрет для имени пользователя. Сейчас же я просто сменю пароль для пользователя app.
docker exec -it full-app-3_db.1.vk23xvu50sy4fgdzrtq6ngvg2 bash
psql -d app -U app
alter user app with password 'NEWPASS';
curl http://172.31.144.9:8000/items
[{"created_at":"Fri, 19 Dec 2025 10:24:46 GMT","id":1,"name":"adsd"},{"created_at":"Fri, 19 Dec 2025 10:24:48 GMT","id":2,"name":"fadfsdf"},{"created_at":"Fri, 19 Dec 2025 10:24:50 GMT","id":3,"name":"fsfsdf"}]
swarm-secret-update-process.svg
Обновление сервиса
В отличии от стека у сервиса есть отдельная команда для обновления, которая выглядит так docker service update. Этой команду в качестве параметров передаётся конфигурация сервиса, которую необходимо поменять. Параметров там немало, поэтому не лишним будет выполнить команду docker service update --help, чтобы получить весь их список и почитать что для сего нужно.
Ниже я рассмотрю пару наиболее часто используемых вариантов обновления сервиса.
Обычное обновление образа сервиса
Если пользователь при создании сервиса указывает конкретную версию образа (так и нужно делать), то со временем образ контейнера устаревает и приходит пора обновить этот образ.
docker service create --name web --replicas 4 -p 9090:80 nginx:1.28.0
docker service ps web
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
ffi9itlvqvgp web.1 nginx:1.28.0 c-stream-9-vm8 Running Running 13 seconds ago
vsmrr3xqisjn web.2 nginx:1.28.0 c-stream-9-vm9 Running Running 12 seconds ago
7wo9yat2er2s web.3 nginx:1.28.0 c-stream-9-vm4 Running Running 12 seconds ago
t7cag7wk1gni web.4 nginx:1.28.0 c-stream-9-vm8 Running Running 13 seconds ago
docker service update --image nginx:1.29.0 web
docker service ps web
isos2i46pwz7 web.1 nginx:1.29.0 c-stream-9-vm8 Running Running 15 seconds ago
ffi9itlvqvgp \_ web.1 nginx:1.28.0 c-stream-9-vm8 Shutdown Shutdown 17 seconds ago
Изменить публикацию портов
Также бывает необходимо изменить номер порта, который используется на самом хосте. Причины у каждого свои, например, образ, который должен работать на конкретном порте, который сейчас занят этим сервисом.
Для этого придётся использовать два параметра:
--publish-rm- удаляет проброс порта--publish-add- добавляет проброс порта
docker service update --publish-rm 9090:80 --publish-add 9091:80 web
docker service inspect -f "" web | jq
[
{
"Protocol": "tcp",
"TargetPort": 80,
"PublishedPort": 9091,
"PublishMode": "ingress"
}
]
Если нужно добавить дополнительный проброс порта, то просто используем --publish-add.
docker service update --publish-add 9092:80 web
docker service inspect -f "" web | jq
[
{
"Protocol": "tcp",
"TargetPort": 80,
"PublishedPort": 9091,
"PublishMode": "ingress"
},
{
"Protocol": "tcp",
"TargetPort": 80,
"PublishedPort": 9092,
"PublishMode": "ingress"
}
]
Порядок замены старых контейнеров
По умолчанию при обновление сервиса Docker запускает новые контейнеры (задачи), после чего проверяет их работоспособность (healthcheck). Если всё нормально только после этого останавливается старый контейнер. Таким образом можно избежать простоев (zero-downtime). Но это поведение можно изменить добавив параметр –update-order, который может принимать следующие значения:
- start-first - Сначала запускается новый контейнер. Только после того, как он перейдет в состояние
runningи успешно пройдет проверку здоровья (healthcheck), Docker остановит и удалит старый контейнер. - stop-first - Сначала остановить старый, потом запустить новый. Используется, если нельзя запускать две копии приложения одновременно (например, из-за конфликта доступа к базе данных или портам).
docker service update --update-order start-first --update-parallelism 1 --update-delay 5s web
Если выполнить команду выше произойдёт следующее:
- Docker выбирает один старый контейнер.
- Запускает рядом новый контейнер с обновленным образом/конфигурацией (
start-first). - Ждет, пока новый контейнер станет «здоров» (healthcheck).
- Удаляет старый контейнер.
- Выдерживает паузу в 5 секунд (
update-delay). - Переходит к следующему контейнеру и повторяет процесс, пока не обновит все реплики по одной (
update-parallelism 1).
Откат обновления
Если вдруг после обновления что что-то пошло не так, есть шанс вернуть всё назад. Для этого используется команда docker service rollback, которая немедленно откатит сервис к его предыдущей конфигурации.
В качестве параметров можно использовать все те же параметры что и для команды update выше. Но тут есть один хороший параметр –rollback-monitor, который задаёт время наблюдения за контейнером после отката, чтобы убедиться, что он не помер.
Тут важно понимать что откат приведёт к отмене только последней операции обновления.
Т.е. выполняя все обновления для сервиса web выше у меня в итоге получилось:
- Состояние (1): Создание сервиса (nginx:1.28.0, порт 9090).
- Состояние (2): Обновление образа на nginx:1.29.0.
- Состояние (3): Изменение порта с 9090 на 9091.
- Состояние (4): Добавление еще одного порта (9092). Это текущее состояние.
Т.е. выполнив команду docker service rollback web сервис вернётся к Состоянию (3), т.е. отменится только проброс еще одного порта (9092).
docker service rollback web
docker service inspect -f "" web | jq
[
{
"Protocol": "tcp",
"TargetPort": 80,
"PublishedPort": 9091,
"PublishMode": "ingress"
}
]
docker service update –force
Команда docker service update --force web сделает принудительное обновление сервиса web, даже если нет никаких обновлений. Да, кажется, что выполнении этой команды бессмысленно, но бывают всё же ситуации, когда это необходимо. Команда docker service update --force нужна для принудительно пересоздания всех тасков (контейнеров) сервиса, это “rolling restart” сервиса.
Что это даёт на практике:
- По очереди останавливает старые контейнеры и поднимает новые (по правилам update_config).
- VIP/имя сервиса остаются теми же.
И вот несколько примеров для чего это реально можно использовать:
- “Перезапуск” сервиса без простоя. В Swarm нет команды
docker service restart, поэтому используетсяdocker service update --force - Подтянуть новый образ. Если вы в качестве тэга образа указали latest (что не есть хорошо), то при пересоздании контейнера ноды кластера могут заново стянуть образ (если он изменился)
- Обновить конфиги, которые менялись. Если вдруг вы что-то изменили снаружи сервиса (файлы/конфиги)
--forceгарантирует пересоздание контейнеров, чтобы они заново прочитали состояние на старте. Контейнеры могут видеть новый файл, но процесс внутри не перечитает, пока не перезапустишь/не сделаешьreload. Самый, наверное, распространенный пример это когда необходимо обновитьTLS сертификаты, в идеале это происходит минимум раз в год. - Убить “залипшие” приложения. Утечка памяти, зависшие коннекты, забитый пул, deadlock — проще сделать rolling restart. Семь бед - один ресет.
- Переместить задачи (контейнеры). Если вдруг узлы кластера стали недоступны и задачи (контейнеры) пересоздались на других узлах может помочь вернуть нормальную балансировку.
Но не переживайте команда docker service update --force не удаляет volume (данные в томах остаются).
swarm-service-update-force.svg
тут надо показать что с 2 репликами всё отвалится из-за файлов
Для примера я создам сервис с образа nginx:1.28.0 с одной репликой и прокину туда файлы index.html и hello.conf, также проброшу порт 80.
Все файлы можно получить тут.
docker service create --name hello-service --publish published=80,target=80 - --mount type=bind,source=$(pwd)/index.html,target=/usr/share/nginx/html/index.html,readonly --mount type=bind,source=$(pwd)/hello.conf,target=/etc/nginx/conf.d/default.conf,readonly nginx:1.28.0
Теперь я проверю что находится внутри файла index.html в самом контейнере (задачи) сервиса, логично что тоже самое что и внутри файла на самом хосте.
docker exec hello-service.1.ov4e9s1k54w156zcm23nr1ies cat /usr/share/nginx/html/index.html
<h1>Hello</h1>
Поменяю строку <h1>Hello</h1> на <h1>Hello Again</h1> и проверю подтянулись ли изменения в сам контейнер сервиса.
vim index.html
<h1>Hello Again</h1>
docker exec hello-service.1.ov4e9s1k54w156zcm23nr1ies cat /usr/share/nginx/html/index.html
```json
<h1>Hello</h1>
Оказалось, что нет, и выходом здесь для меня будет выполнить команду docker service update --force
docker service update --force hello-service
docker exec hello-service.1.qmcb2mqu3mnvvf2siu9ubenna cat /usr/share/nginx/html/index.html
<h1>Hello Again</h1>
Если же говорить про секреты самого Swarm то тут по сути обновления нет, их нельзя нельзя “поменять на месте”. Единственный путь — это создать новый секрет прокинуть его в контейнер, но придётся и приложение переписывать для использования нового секрета.


Комментарии