Міграція стеку Docker Compose: перенесення на новий хост без міфів про простій

Було корисно?

У вас є стек Docker Compose, який «працював роками», і тепер його потрібно перемістити на новий хост, бо старий помирає, вийшов з гарантії, закінчилося місце або вичерпалося терпіння команди.
Бізнес просить «без простою», а хтось каже: «Просто rsync томи й запусти там». Саме з цієї фрази народжуються інциденти.

Compose цілком можна мігрувати чисто. Але якщо ви хочете, щоб користувачі не відчули «простою», потрібно визначити, що саме це означає (хвилини? секунди? кілька помилок?) і вибрати техніку міграції, що відповідає вашим даним і шаблонам трафіку.
Ваші веб-контейнери — це легко. Бази даних — та частина, що штовхає вас до терапевта.

Міф про «без простою»: що ви можете і не можете обіцяти

«Без простою» — це не бінарне поняття. Це контракт. Потрібно визначити умови контракту, а потім збудувати міграцію, що їх виконує.
Для міграцій Docker Compose обмеження майже завжди пов’язані зі станом: бази даних, черги та будь-які записувані томи.
Якщо ваш стек повністю безстанний, ви можете зробити чистий blue/green swap з майже нульовим впливом на користувача.
Якщо стек записує на диск, потрібно або:

  • Реплікувати стан (реплікація бази даних, реплікація об’єктного сховища, запис у дві сторони), а потім переключитися, або
  • Заморозити запис (вікно обслуговування, режим лише для читання) і скопіювати консистентні дані, або
  • Прийняти простій і зробити його коротким і передбачуваним.

Чесна SRE-відповідь: якщо ви копіюєте томи, в які триває запис, у вас не «нема простою». У вас «неминуча корупція з ноткою заперечення».
Прийнятна версія «без простою» зазвичай виглядає як «немає запланованого вікна обслуговування», але це все одно може допустити кілька секунд сплесків під час змін DNS або балансувальника навантаження.

Ось практичне визначення, яке я люблю: немає простою для користувачів, але дозволений короткий витрата бюджету помилок.
Наприклад: 30-секундний сплеск 502 під час перемикання прийнятний, якщо ви попередили людей і клієнти роблять повторні спроби.
Дводенна мимовільна неконсистентність даних через rsync живої бази даних — ні.

Короткий жарт №1: «Ми зробимо це з нульовим простоям» часто означає «ми ще не дивилися базу даних».

Кілька фактів і трохи історії (щоб ви перестали вірити в магію)

Вам не треба ставати археологом контейнерів, щоб мігрувати стек Compose. Але трохи контексту допоможе передбачити режими відмов.
Ось конкретні факти, які важливі під час реальних міграцій:

  1. Docker томи спроектовані для локального збереження, а не для портативності. Іменовані томи живуть під Docker data root і не є «переносними» без копіювання підлеглої файлової системи.
  2. Docker Compose починався як зовнішній інструмент на Python. Згодом він став плагіном CLI Docker (Compose V2), що змінило поведінку та формати виводу і могло заплутати руктини.
  3. Overlay мережі — це для Swarm/Kubernetes, а не для Compose. Мережі Compose — це однозалізна bridge-мережа, якщо ви не підключаєте власну мережеву інфраструктуру.
  4. IP-адреси всередині Docker-мереж не є стабільними контрактами. Якщо додаток залежить від IP контейнера — це вже майбутній інцидент з нагадуванням у календарі.
  5. Rsync живої директорії бази даних — це не бекап. Більшість баз потребують снапшотів, зупинки файлової системи або нативних інструментів бекапу для отримання консистентної копії.
  6. Healthchecks існують тому, що «контейнер запущено» ≠ «сервіс готовий». Використовуйте їх при перемиканнях або будьте готові до таємничих 502.
  7. DNS TTL — це рекомендація, а не абсолют. Деякі резолвери кешують довше, ніж ви просили. Плануйте перемикання з урахуванням відсталих клієнтів.
  8. Compose не є планувальником. Він не перепланує ваш аварійний контейнер на інший хост. Якщо вам це потрібно — ви в території Swarm/Kubernetes (або ви будуєте власну автоматизацію, що є… хобі).
  9. Власність файлів і відображення UID/GID — мовчазні вбивці. Зміни образів і відмінності хостів ламають права на bind mount і томах так, що це виглядає як «помилка додатка».

Одна цитата, бо вона досі доречна: Надія — не стратегія. — Джеймс Кемерон

Моделі міграції, що реально працюють

Модель A: Безстанний blue/green (ідеальний випадок)

Якщо ваш стек не має персистентних записів на хості (або всі записи йдуть у керовані сервіси), робіть так:
розгорніть той самий проект Compose на новому хості, перевірте healthchecks, а потім перемкніть трафік через DNS, зворотний проксі або балансувальник навантаження.
Тримайте старий хост запущеним як запасний, поки не довіритеся новому.

Видимий для користувача вплив може бути близьким до нуля, якщо:
сесії не прив’язані до хосту або ви використовуєте спільне сховище сесій (Redis, сесії в БД, JWT),
і ваш метод cutover не залишає частину трафіку на застарілих кінцевих точках.

Модель B: Stateful з реплікацією (дорослий підхід)

Для PostgreSQL/MySQL/Redis реплікація — найчистіший шлях до «без простою».
Ви запускаєте нову базу як репліку, даєте їй наздогнати, а потім піднімаєте її у промоут.
Це добре працює, якщо ви можете потерпіти коротку заморозку записів під час промоушну або ваш додаток підтримує швидке відновлення.

Міграції на основі реплікації зсувають ризик від «коректності копіювання файлів» до «коректності реплікації та хореографії переключення».
Це вигідний обмін: інструменти реплікації створені для цього; ваш скрипт rsync — для впевненості.

Модель C: Stateful зі снапшотом + короткою заморозкою (реалістичний середній шлях)

Якщо реплікація надто складна (спадщинні додатки, немає часу, немає експертизи), орієнтуйтеся на консистентний снапшот:
зупиніть записи, зробіть снапшот (LVM/ZFS/btrfs або нативний бекап СУБД), перенесіть його, запустіть на новому хості, переключіть трафік.
Простій — це інтервал «зупинки записів» плюс час на cutover.

Модель D: «Просто скопіювати /var/lib/docker» (пастка)

Це може спрацювати в вузьких умовах: та сама версія Docker, той самий драйвер сховища, однакова семантика файлової системи, відсутність живих записів і готовність дебажити внутрішності Docker о 3:00 ранку.
Якщо ви мігруєте, бо старий хост крихкий, повторення крихких технік — це естетичний вибір.

Підготовка: що інвентаризувати перед тим, як торкатися системи

Невдачі під час міграцій рідко походять від очевидного. Вони виникають через невидимі звʼязки між вашим файлом Compose і хостом:
шляхи файлової системи, налаштування ядра, правила брандмауера та «тимчасові» cron-завдання, що стали продакшн.

Перед створенням нового хоста інвентаризуйте:

  • Версія Compose і версія Docker Engine на старому хості (звірити або свідомо оновити).
  • Томи: іменовані томи проти bind mount; які сервіси пишуть; скільки даних.
  • Секрети/конфіг: .env файли, змонтовані директорії конфігів, TLS-сертифікати, API-ключі.
  • Ingress: зворотний проксі, опубліковані порти, брандмауер, будь-які правила NAT.
  • Зовнішні залежності: DNS-записи, allowlist, зовнішні webhook-и, SMTP-реле.
  • Спостережуваність: розташування логів, кінцеві точки метрик, інтеграції алертингу.
  • Стратегія бекапу/відновлення, яку ви можете протестувати без гри з терпінням компанії.

Практичні завдання з командами (і як їх інтерпретувати)

Цей розділ навмисно практичний: команди, які можна виконати сьогодні, очікувані виводи і рішення, які потрібно прийняти на основі кожного.
Виконуйте їх спочатку на старому хості, потім повторіть на новому як валідацію.

Завдання 1: Визначити версії Docker і Compose (ризик сумісності)

cr0x@server:~$ docker version --format 'Engine={{.Server.Version}} StorageDriver={{.Server.Driver}}'
Engine=26.1.4 StorageDriver=overlay2

Значення: У вас Docker Engine 26.x з overlay2. Це добре і поширено.
Рішення: На новому хості відповідайте основній версії (або протестуйте оновлення). Також переконайтеся, що overlay2 підтримується (ядро + файловa система).

cr0x@server:~$ docker compose version
Docker Compose version v2.27.1

Значення: Плагін Compose V2, а не застарілий docker-compose V1.
Рішення: Використовуйте ту саму основну/мінорну версію, якщо хочете менше сюрпризів у іменах мереж, неймінгу проєктів і виводі CLI.

Завдання 2: Подивитися, що насправді запущено (і під яким імʼям проєкту)

cr0x@server:~$ docker compose ls
NAME            STATUS              CONFIG FILES
payments        running(7)          /srv/payments/compose.yaml

Значення: Ваш проєкт Compose називається payments, а не так, як ви здогадувалися.
Рішення: Тримайте імʼя проєкту стабільним між хостами (використовуйте --project-name або name: у Compose), щоб уникнути сюрпризів у іменах мереж/томів.

Завдання 3: Перелічити сервіси, образи та порти (вплив на ingress)

cr0x@server:~$ docker compose -p payments ps
NAME                 IMAGE                        COMMAND                  SERVICE   STATUS    PORTS
payments-web-1       nginx:1.27-alpine            "/docker-entrypoint.…"   web       running   0.0.0.0:443->443/tcp
payments-api-1       ghcr.io/acme/api:3.14.2      "/app/start"             api       running   0.0.0.0:8080->8080/tcp
payments-db-1        postgres:15                  "docker-entrypoint.s…"   db        running   5432/tcp

Значення: Лише web і api публікують порти на хості. База даних лише внутрішня (добре).
Рішення: Поверхня cutover — це 443 і 8080, а не 5432. Це змінює планування брандмауера та балансувальника навантаження.

Завдання 4: Перелічити томи і визначити, що є станом

cr0x@server:~$ docker volume ls
DRIVER    VOLUME NAME
local     payments_dbdata
local     payments_redisdata
local     payments_prometheus

Значення: Є іменовані томи. Очікуйте їх під Docker data root, якщо ви не використовували driver_opts.
Рішення: Розглядайте dbdata і redisdata як стан і вимагаючи гарантій консистентності.

Завдання 5: Визначити, де Docker зберігає дані (щоб не копіювати неправильний диск)

cr0x@server:~$ docker info --format 'DockerRootDir={{.DockerRootDir}}'
DockerRootDir=/var/lib/docker

Значення: За замовчуванням Docker root directory.
Рішення: Якщо плануєте снапшоти файлової системи, цей шлях має бути на файловій системі, що підтримує снапшоти, і ви маєте знати її підлеглий пристрій/пул.

Завдання 6: Проінспектувати монтовані шляхи контейнера (і правда: том чи bind mount)

cr0x@server:~$ docker inspect payments-db-1 --format '{{json .Mounts}}'
[{"Type":"volume","Name":"payments_dbdata","Source":"/var/lib/docker/volumes/payments_dbdata/_data","Destination":"/var/lib/postgresql/data","Driver":"local","Mode":"z","RW":true,"Propagation":""}]

Значення: База даних пише в іменований том під /var/lib/docker/volumes.
Рішення: Якщо копіюєте його, вам потрібен консистентний метод (снапшот або нативний бекап БД), а не випадкове копіювання файлів під час роботи.

Завдання 7: Перевірити розмір тома і зростання (час передачі і ризик)

cr0x@server:~$ sudo du -sh /var/lib/docker/volumes/payments_dbdata/_data
58G	/var/lib/docker/volumes/payments_dbdata/_data

Значення: Ви переносите ~58 GB даних бази, плюс WAL і накладні витрати.
Рішення: Плануйте час передачі з урахуванням реальної пропускної здатності. Якщо не можете реплікувати і не можете дозволити довгий час копіювання — потрібна інша модель.

Завдання 8: Знайти приховані bind mount, згадані в Compose

cr0x@server:~$ grep -nE '^\s*-\s*/' /srv/payments/compose.yaml
42:      - /srv/payments/nginx/conf.d:/etc/nginx/conf.d:ro
43:      - /srv/payments/tls:/etc/nginx/tls:ro
77:      - /srv/payments/uploads:/var/lib/app/uploads

Значення: Використовуються bind mount для конфігів, TLS і завантажень. Завантаження є станом і не в Docker-томах.
Рішення: Додайте ці директорії до плану міграції, включно з власністю та правами доступу.

Завдання 9: Перевірити доступність образів (щоб уникнути «pull denied» у день cutover)

cr0x@server:~$ docker compose -p payments config --images
ghcr.io/acme/api:3.14.2
nginx:1.27-alpine
postgres:15
redis:7-alpine

Значення: Саме ці теги образів ви будете запускати.
Рішення: На новому хості заздалегідь підвантажте образи. Якщо ви використовуєте приватний реєстр, переконайтеся, що авторизація працює до cutover.

Завдання 10: Перевірити health і readiness контейнерів (коректність cutover)

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Status}}' | head
NAMES             STATUS
payments-web-1    Up 5 days
payments-api-1    Up 5 days (healthy)
payments-db-1     Up 5 days

Значення: API повідомляє, що здоровий. Web і DB можуть не мати healthcheck (поширено).
Рішення: Додайте healthchecks для будь-якого сервісу, від якого залежите під час cutover (особливо зворотний проксі і API). Healthchecks — не прикраса.

Завдання 11: Підтвердити відкриті порти і хто їх займає (щоб уникнути конфліктів)

cr0x@server:~$ sudo ss -lntp | grep -E ':(443|8080)\s'
LISTEN 0      4096        0.0.0.0:443       0.0.0.0:*    users:(("docker-proxy",pid=15422,fd=4))
LISTEN 0      4096        0.0.0.0:8080      0.0.0.0:*    users:(("docker-proxy",pid=15457,fd=4))

Значення: Порти опубліковані через docker-proxy. На деяких системах це буде iptables DNAT, але результат той самий: ці порти мають бути вільні на новому хості.
Рішення: Забронюйте порти. Якщо щось інше (наприклад, старий Nginx на хості) їх займає — розгортання зазнає невдачі.

Завдання 12: Заміряти запас диску і інодів (тому міграції зриваються на банальній математиці)

cr0x@server:~$ df -h /var/lib/docker
Filesystem      Size  Used Avail Use% Mounted on
/dev/nvme0n1p3  500G  410G   65G  87% /

Значення: Місця вже мало.
Рішення: Новий хост має мати комфортний запас. Якщо ви мігруєте на «такий самий розмір, але новіший» — ви просто купуєте час, а не вирішуєте проблему.

Завдання 13: Перевірити тип файлової системи та опції монтовання (вплив на снапшоти і продуктивність)

cr0x@server:~$ findmnt -no FSTYPE,OPTIONS /var/lib/docker
ext4	rw,relatime,errors=remount-ro

Значення: ext4, без нативних снапшотів.
Рішення: Якщо хочете консистентних снапшотів файлової системи без зупинки сервісів, потрібен LVM під нею або перемістити Docker root на ZFS/btrfs на новому хості, або використовувати нативні бекапи СУБД.

Завдання 14: Протестувати поведінку DNS і TTL, яку ви насправді отримуєте

cr0x@server:~$ dig +noall +answer app.example.internal
app.example.internal.  300  IN  A  10.20.30.40

Значення: TTL 300 секунд (5 хвилин).
Рішення: Плануйте до кількох хвилин змішаного трафіку, якщо не використовуєте балансувальник/VIP. Знижуйте TTL заздалегідь, а не за пʼять хвилин до cutover.

Завдання 15: Протестувати готовність додатку до заморожування записів (якщо потрібно)

cr0x@server:~$ curl -sS -o /dev/null -w '%{http_code}\n' https://app.example.internal/health
200

Значення: Ендпоїнт здоровʼя доступний.
Рішення: Якщо вам потрібен режим обслуговування, реалізуйте і протестуйте його зараз. Міграція — поганий час, щоб дізнатися, що ваш додаток не вміє працювати в режимі лише для читання.

Сховище та стан: частина, яку не можна відмахнути

Міграції Compose зазвичай подаються як «перенесення контейнерів». Це мило. Ви переносите дані.
Контейнери — це велика рогата худоба; ваші томи — домашні тварини з юридичним статусом.

Іменовані томи проти bind mount: наслідки для міграції

Іменовані томи під контролем Docker. Це зручно, але робить міграцію непрозорою:
потрібно знайти шлях даних тома, скопіювати його безпечно і зберегти власність.
Bind mount — явні: ви бачите шлях у Compose, можете робити бекапи стандартними інструментами і застосовувати практики на рівні файлової системи.
Недолік: bind mount привʼязані до хосту. Структура директорій стає частиною API контракту.

Правила консистентності (в різкій формі)

  • Якщо це база даних: використовуйте реплікацію або нативні інструменти бекапу; ставтеся з підозрою до копій файлової системи, якщо у вас немає снапшотів або база не вимкнена чисто.
  • Якщо це директорія обʼєктного сховища (завантаження): rsync підходить, але потрібно врахувати одночасні записи (двофазний синхрон: початкове копіювання, потім фінальний синк після заморозки записів).
  • Якщо це Redis: вирішіть, чи це кеш (перебудувати), чи сховище даних (реплікація або снапшот).
  • Якщо це Prometheus: можна копіювати, але очікуйте інтенсивного WAL. Снапшотинг працює; живий rsync хиткий, якщо ви не зупиняєте його.

Два надійні шаблони для перенесення станних даних

Шаблон 1: Нативний бекап БД + відновлення (передбачувано, зазвичай швидше)

Для PostgreSQL: використовуйте pg_basebackup (реплікація) або pg_dump/pg_restore (логічний бекап).
Для MySQL: використовуйте реплікацію або консистентний дамп з відповідними прапорами.
Ключова перевага: ви переміщаєте дані в форматі, який розуміє база, а не файли, які база активно змінює.

Шаблон 2: Снапшот + передача (швидко для великих наборів даних)

Якщо Docker root або директорії томів знаходяться на ZFS або LVM, снапшоти дозволяють зробити точкову копію на рівні файлової системи.
Потім передаєте снапшот на новий хост і монтуєте його як джерело тома.
Це надзвичайно швидко для великих даних і зменшує простої. Потребує, щоб ви планували розміщення сховища свідомо.

Короткий жарт №2: Сховище — єдине місце, де «в мене працює на машині» означає «ваша машина тепер моя проблема».

Мережа та перемикання: DNS, VIP і зворотні проксі

Більшість стеків Compose мають одну з таких форм ingress:

  • Порти опубліковані безпосередньо (наприклад, 443 на хості) і клієнти підключаються до IP/DNS хоста.
  • Зворотний проксі в контейнері (nginx/Traefik/Caddy), що публікує 80/443 і маршрутизує до внутрішніх сервісів.
  • Зовнішній балансувальник навантаження попереду, що форвардить до хостів.

Для міграцій найкращий важіль cutover — той, що дозволяє найшвидше відкочування:
зміна пулу балансувальника, переміщення VIP або оновлення DNS, яке можна швидко скасувати.
Публікація портів прямо на одному хості з жорстко закодованим DNS — зручна, поки не настане час рухатися.

Перемикання через DNS: нормально, але плануйте відсталих клієнтів

DNS cutover поширений, бо доступний. Він також брудний:
кеші ігнорують TTL, клієнти повторно використовують TCP-зʼєднання, деяке ПЗ «фіксує» IP до перезапуску.
Якщо робите DNS, зменшіть TTL щонайменше за добу до цього (не за пʼять хвилин).
Потім тримайте старий сервіс запущеним, доки не впевнитеся, що він осушив трафік.

Перемикання VIP/keepalived: чисто, якщо можете запустити

Віртуальний IP, який переміщується між хостами, може бути майже миттєвим і легко відкатним.
Але це вимагає мережевої підтримки і готовності правильно запускати VRRP/keepalived.
У корпоративних мережах зі строгим контролем змін це може бути складніше, ніж має бути.

Перемикання через зворотний проксі: поставте перемикач туди, де він потрібен

Якщо у вас вже є зворотний проксі, подумайте про винесення його поза хости додатків.
Невеликий виділений шар проксі може маршрутизувати до старого або нового бекенду через зміну конфігурації.
Це ефективно blue/green без потреби використовувати Compose як планувальник.

Швидкий план діагностики

Коли міграція «здається» успішною, але продуктивність падає або зʼявляються помилки, потрібен короткий шлях до вузького місця.
Не обговорюйте архітектуру в каналі інциденту. Перевірте базові речі в порядку.

Перше: чи трафік доходить куди треба?

  • Перевірте резолюцію DNS з кількох мереж (корп мережа, VPN і з нового хоста).
  • Підтвердьте, що новий хост дійсно приймає зʼєднання на очікуваних портах (ss, лічильники брандмауера).
  • Перевірте upstream цілі зворотного проксі і статус їх здоровʼя.

Друге: чи додаток здоровий, чи просто «запущений»?

  • Перевіряйте статус health контейнерів, а не лише час роботи.
  • Хвостіть логи на краю (proxy) і в ядрі (API) одночасно; корелюйте за таймстемпами.
  • Шукайте вичерпання пулів зʼєднань, таймаути і «permission denied» на монтованих шляхах.

Третє: чи сховище — вузьке місце?

  • Перевірте затримки диска і насичення (iostat, nvme smart stats, dmesg на предмет ресетів).
  • Підтвердьте, що база даних знаходиться на очікуваному диску/пулі, а не на повільному завантажувальному розділі.
  • Перевірте опції файлової системи і вільне місце; майже повні диски поводяться погано.

Четверте: чи мережевий шлях відрізняється від очікуваного?

  • Перевірте невідповідність MTU (особливо через VPN і VLAN).
  • Підтвердьте правила брандмауера і ліміти conntrack на новому хості.
  • Подивіться на повторні SYN і TCP-resets (статистика ss, логи proxy).

Поширені помилки: симптом → корінь → виправлення

1) API повертає 502/504 відразу після перемикання

Симптом: Зворотний проксі запущений, але виклики до upstream таймаутяться.

Корінь: Відсутні або занадто слабкі healthchecks; проксі почав маршрутизувати раніше, ніж API прогрівся або завершилися міграції.

Виправлення: Додайте реальні healthchecks (перевірка підключення до БД, а не лише процесу). Блокуйте маршрутизацію в проксі за статусом здоровʼя. Розгляньте порядок запуску з depends_on плюс умови здоровʼя.

2) База даних запускається, а потім падає з помилками корупції

Симптом: Postgres скаржиться на недійсні контрольні точки або сегменти WAL; MySQL повідомляє помилки InnoDB.

Корінь: Копіювання на рівні файлової системи живої директорії бази даних (rsync під час роботи) створило неконсистентний набір даних.

Виправлення: Відновіть з консистентного бекапу або снапшоту. Перемігруйте заново з використанням реплікації або нативних інструментів бекапу СУБД. Якщо копіюєте файли — зупиніть БД.

3) Все «вгору», але запис не працює через permission denied

Симптом: Логи показують EACCES на змонтованих шляхах; завантаження не працюють; база не може писати.

Корінь: Невідповідність UID/GID між старим і новим хостом або зміни власності директорії для bind mount.

Виправлення: Вирівняйте власника під користувача контейнера, а не під ваш адміністративний акаунт. Підтвердіть за допомогою stat і користувачів рантайму контейнера. Використовуйте явний user: у Compose за потреби.

4) Продуктивність вдвічі гірша, ніж була

Симптом: Після міграції латентність зросла; CPU у нормі; база здається повільною.

Корінь: Сховище переміщено з SSD на повільніший диск (або неправильно налаштований RAID), опції файлової системи відрізняються або інший планувальник I/O.

Виправлення: Протестуйте диск на новому хості. Помістіть том БД на потрібний пристрій. Виправте опції монтовання. Перевірте, чи випадково немає шифрування/стиснення, що додає накладні витрати.

5) Клієнти час від часу потрапляють на старий хост годинами

Симптом: Змішані логи, змішана поведінка; деякі користувачі бачать стару версію.

Корінь: Кешування DNS понад TTL, жорстко задані резолвери, довгоживучі зʼєднання або зафіксовані IP у клієнтах.

Виправлення: Використовуйте балансувальник/VIP коли можливо. Якщо тільки DNS — зменшіть TTL заздалегідь і тримайте старий хост у режимі обслуговування, доки хвіст не вивітриться.

6) Compose up падає: «port is already allocated»

Симптом: Новий хост відмовляється запускати веб/проксі контейнер.

Корінь: Інший процес займає порт (часто хостовий nginx, apache або залишковий контейнер).

Виправлення: Знайдіть слухача за допомогою ss -lntp. Зупиніть/відключіть конфліктуючий сервіс або відкоригуйте опубліковані порти і маршрути upstream.

7) Несподівана втрата даних у завантаженнях чи файлах

Симптом: На новому хості відсутні останні завантаження.

Корінь: Однопрохідне копіювання; записи продовжувалися під час передачі; не зроблено фінального синку.

Виправлення: Двофазний rsync: початковий синк під час живого режиму, потім заморозка записів і фінальний синк, потім cutover.

Чеклісти / покрокові плани

План 1: Майже нульовий простій для переважно безстанних стеків (blue/green + DNS/LB)

  1. Збіг runtime: Встановіть Docker Engine і плагін Compose на новому хості. Тримайте версії близькими, щоб зменшити сюрпризи.
  2. Провізіонування сховища: Створіть директорії для bind mount; заплануйте місця томів. Переконайтеся у достатньому дисковому просторі і IOPS.
  3. Підвантажте образи: Завантажте всі потрібні образи на новий хост, щоб уникнути збоїв реєстру в день cutover.
  4. Розгорніть паралельно: Підніміть стек на новому хості на альтернативних портах або за окремою групою цілей LB.
  5. Валідуйте: Ендпоїнти здоровʼя, підключення до БД, фонова робота і будь-які заплановані завдання.
  6. Тіньовий трафік (опційно): Дзеркальте read-only трафік або запускайте синтетичні перевірки проти нового стеку.
  7. Cutover: Змініть цілі LB або DNS-запис на новий хост.
  8. Моніторинг: Спостерігайте за рівнем помилок, латентністю і логами. Тримайте старий хост для відкоту.
  9. План відкату: Якщо бюджет помилок перевищено — швидко поверніть. Не «дебажте в продакшені», якщо є інші варіанти.
  10. Виведення з експлуатації пізніше: Після стабільного вікна вимкніть старий хост і заархівуйте конфіги/бекапи.

План 2: Короткий простій з консистентними снапшотами (заморозка + снапшот + відновлення)

  1. Підготуйте новий хост: Ті самі образи, та сама конфігурація, ті самі секрети, та сама структура директорій для bind mount.
  2. Знизьте TTL: Якщо робите DNS cutover, зменшіть TTL щонайменше за 24 години.
  3. Початковий синк: Для завантажень та інших дерев файлів виконайте rsync під час роботи додатку.
  4. Заморозьте записи: Переведіть додаток в режим обслуговування або зупиніть сервіси, інтенсивно записуючі. Підтвердіть, що записів немає.
  5. Зробіть снапшот/бекап: Використовуйте нативний бекап БД або снапшот файлової системи, якщо підтримується.
  6. Фінальний синк: Rsync знову, щоб захопити останні зміни.
  7. Відновлення на новому хості: Імпортуйте базу, помістіть файли, перевірте власність.
  8. Запустіть стек: Підніміть Compose і дочекайтеся проходження healthchecks.
  9. Переключіть трафік: DNS або LB switch. Тримайте старий хост зупиненим або в режимі read-only, щоб уникнути split-brain.
  10. Зніміть заморозку: Вийдіть з режиму обслуговування і моніторьте.

План 3: Stateful «майже без простою» з реплікацією (міграція з орієнтацією на базу)

  1. Підніміть нову БД: Налаштуйте її як репліку старої БД. Переконайтеся, що лаг реплікації залишається малим під звичайним навантаженням.
  2. Розгорніть додаток на новому хості: Вкажіть його на репліку для верифікації лише для читання, якщо можливо, або тримайте його в стані очікування.
  3. План промоушну: Визначте точну хвилину cutover і як ви будете працювати із записами (коротка заморозка або апдейти на рівні додатку).
  4. Cutover: Коротко заморозьте записи, дочекайтеся відставання репліки, підніміть репліку до primary.
  5. Переключіть трафік додатку: Змініть LB/DNS на новий хост і нову кінцеву точку БД.
  6. Тримайте стару БД: Залиште її як репліку (якщо підтримується) для вікна відкату, але не дозволяйте записи в неї.

Три корпоративні міні-історії з поля бою

Міні-історія 1: Інцидент через неправильне припущення (rsync як «бекап»)

Середня SaaS-компанія вирішила перемістити стек Compose з «тимчасової» VM на новий хост з більшим CPU.
Стек був типовий: Nginx, API, PostgreSQL, Redis і воркер. Команда вже робила безстанні cutover-операції, тож план міграції здавався знайомим:
підняти новий хост, rsync /var/lib/docker/volumes, запустити Compose, переключити DNS.

Вони виконали rsync поки все ще обслуговували трафік. Це зайняло деякий час. Потім запустили його ще раз «щоб зловити дельту», пишалися розміром дельти і переключилися.
Новий стек піднявся і виглядав нормально. Користувачі заходили. Запити проходили. Інцидент не почався з сигналів моніторингу; він почався з квитків у підтримку.

Перший симптом був тонким: невеликий відсоток користувачів бачив застарілі дані після оновлень. Потім деякі фонові завдання падали з помилками унікальності.
Згодом Postgres почав виводити попередження про WAL, спочатку нефатальні, але все гірше. Команда спробувала перезапустити контейнер бази даних. Тоді все й розвалилося.

Неправильне припущення було в тому, що rsync директорії Postgres «досить близько» якщо зробити його двічі.
Це не так. Postgres очікує консистентних чекпоінтів і послідовність WAL; копіювання файлів під час запису може створити датасет, який стартує, але містить міни.
Вони відновлювалися з старішого логічного бекапу, потім вручну ремонтували прогалину з журналів додатку і купою обережних SQL-запитів.

Урок: якщо ваш план міграції не включає явного механізму консистентності для бази даних — це не план.
Це оптимізм з командним рядком.

Міні-історія 2: Оптимізація, що далася боком (швидше на папері)

Велика корпоративна команда мігрувала внутрішню платформу на нове залізо. «Оптимізація» полягала в консолідації сховища:
помістити Docker root, томи бази даних і завантаження додатків всі на один великий RAID-масив.
Рationale звучав чудово на зустрічі: менше точок монтування, простіші бекапи і «RAID же швидкий».

Після cutover все працювало, але латентність подвоїлася в пікові години. API не був завантажений по CPU. Мережа була в порядку.
База мала періодичні затримки. Воркери довше виконували завдання. Інженери почали звинувачувати ядро нового хоста, версію Docker і навіть «накладні витрати контейнерів».

Виявилося, що контролер RAID був налаштований на політику, орієнтовану на ємність із write cache, яка була безпечною, але повільною для їхнього навантаження.
На старому хості база працювала на SSD з низькою латентністю. На новому вона ділила шпинделі з великими послідовними записами від завантажень і бекапів.
Консолідована схема створила конкуренцію I/O, яка не проявилася в синтетичних тестах, але відразу в продуктивності.

Виправлення було нудним: винести базу на низьколатентне сховище і тримати масивні файли окремо, з обмеженням пропускної здатності для фонового копіювання.
Також додали простий моніторинг I/O, який зробив би корінь проблеми очевидним в перший день.
«Оптимізація» не була злочинною; вона просто не врахувала, що бази даних не домовляються зі слабкими дисками.

Міні-історія 3: Нудна, але правильна практика, що врятувала день (репетиція + відкат)

Інша, менша і менш гламурна команда мусила мігрувати стек Compose, що обробляв погодження по зарплатах.
Система не була високонавантаженою, але критичною. Вони не могли дозволити втрату даних і не могли довго блокувати доступ.
Нікого це не надихало. Це зазвичай хороший знак.

Вони провели репетицію. Не теоретичну — реальний прогін на клоні staging з реалістичним знімком даних.
Документували кожну команду, навіть ті, що здавалися очевидними. Перевірили процедури відновлення, а не тільки бекапи.
Потім зробили ще менш захопливу річ: написали план відкату, що включав повернення DNS, порядок зупинки/запуску контейнерів і чітку точку «зупини світ».

В день міграції новий хост піднявся, але один сервіс впав через відсутній CA bundle, який тихо був присутній на старому хості.
Оскільки вони відрепетирували, помилка була знайома. Вони виправили пакет, перезапустили послідовність і продовжили.
Простой був коротким, передбачуваним і зрозумілим. Користувачі трохи поскаржилися, потім забули.

День був «успішним», бо вони ставилися до зміни як до процесу з гострими краями: репетиція, валідація, відкат.
Нічого геройського. Нічого хитрого. Просто компетентність.

Питання та відповіді

1) Чи можу я мігрувати стек Compose з абсолютно нульовим простоєм?

Якщо стек безстанний — майже так. Якщо є стан — «абсолютно нульовий» зазвичай вимагає реплікації або зовнішнього рівня збереження даних.
Більшість команд можуть досягти «без вікна обслуговування» з невеликим бліпом, але не нульовим у математичному сенсі.

2) Чи безпечно копіювати іменовані томи Docker за допомогою rsync?

Для статичних даних або тих, що можна пригасити — так. Для живих баз даних — ні. Якщо мусите копіювати, зупиніть сервіс або використайте снапшоти, або нативні інструменти бекапу/реплікації СУБД.

3) Чи варто копіювати /var/lib/docker на новий хост?

Уникайте цього, якщо немає серйозної причини і тестового середовища. Це звʼязує вас із деталями драйвера сховища, внутрішніми механізмами Docker і сумісністю версій.
Краще мігрувати дані додатку і чисто перепроєктувати контейнери.

4) Який найнадійніший механізм cutover: DNS, балансувальник чи VIP?

Зміни цілей балансувальника і переміщення VIP зазвичай найшвидші для відкоту і менше залежать від поведінки клієнтів.
DNS працює, але потрібно планувати кешування і довгоживучі зʼєднання.

5) Як поводитися з TLS-сертифікатами під час міграції?

Трактуйте їх як першокласний стан. Інвентаризуйте, де вони живуть (bind mount, файли секретів).
Скопіюйте їх безпечно, перевірте права файлів і підтвердьте повний ланцюг на новому хості до cutover.

6) Чи потрібно зберігати ті самі IP контейнерів або імена мереж?

Ви не повинні залежати від IP контейнерів взагалі. Використовуйте імена сервісів у мережі Compose.
Імена мереж важливі лише якщо зовнішні системи на них посилаються (рідко). Зазвичай достатньо стабільного неймінгу проєкту.

7) Як мігрувати завантаження або інші змінні файли?

Зробіть двофазний синк: копіюйте під час роботи, потім коротко заморозьте запис і зробіть фінальний rsync.
Якщо можете — перенесіть завантаження в обʼєктне сховище і більше не турбуйтеся про файли на хості.

8) Що з фоновими воркерами і запланованими завданнями під час cutover?

Призупиніть їх або забезпечте ідемпотентність. Під час cutover дубльовані воркери можуть подвоїти процеси, і так фінансові системи вигадують нові форми розваг.
Якщо не можете паузити — забезпечте дедуплікацію і замки задач.

9) Чи варто оновлювати Docker/Compose під час міграції?

Краще не поєднувати міграцію хоста з оновленням рантайму, якщо немає часу на тестування.
Якщо мусите оновити — робіть це навмисно з репетицією і валідацією, а не як побічний ефект «збірки нового сервера».

Висновок: практичні наступні кроки

Якщо взяти одне: міграції Compose провалюються, коли команди ставляться до стану як до деталі реалізації.
Визначте, що «без простою» означає в бізнес-термінах, а потім оберіть модель міграції, що відповідає реальності ваших даних.
Реплікуйте, коли можете. Снапшотте, коли потрібно. Заморожуйте записи, коли немає кращого варіанту. Не називайте rsync живої бази даних інженерією.

Наступні кроки, що одразу окупаються:

  • Виконайте інвентаризаційні завдання вище на старому хості і запишіть, що є станним.
  • Виберіть механізм cutover з важелем для відкату, якому ви довіряєте (LB/VIP краще за чистий DNS).
  • Відрепетируйте міграцію на staging-клоні, включно з відновленням і відкатом.
  • Додайте healthchecks і gating готовності, щоб «запущено» означало «готово».
  • Зробіть розміщення сховища явним на новому хості; бази даних не повинні бути на «таємничих» дисках.

Міграції не гламурні. Вони — операційне правдиве зілля. Робіть їх так, щоб потім хотілося спати.

← Попередня
Debian 13 SR-IOV: основи — чому не працює і як налагодити вперше
Наступна →
Замінити vCenter на Proxmox: що ви отримуєте, що втрачаєте та робочі обходи, які справді працюють

Залишити коментар