Існує особливий тип відмови, коли нічого не «впало», бо все постійно «запускається». Ваші дашборди показують стрибки CPU, обсяг логів різко зростає, а ім’я контейнера миготить від перезапусків. Ви намагаєтеся зайти через docker exec, але процес помирає ще до того, як з’явиться запрошення shell. Вітаю: ви створили нескінченний crash loop.
Політики перезапуску Docker призначені підвищувати стійкість сервісів. У продакшні вони також можуть перетворити дрібну помилку на самопідтримувану інцидент: галасливий, дорогий і важкий для діагностики. Ось як припинити так робити.
Політики перезапуску: що вони реально роблять (не те, на що ви сподіваєтесь)
На папері політики перезапуску Docker прості. На практиці це контракт між життєвим циклом контейнера та упередженою поведінкою демона. Вони не роблять вашу програму здоровішою. Вони роблять її персистентною. Це різні властивості, і їх плутання веде до нескінченних циклів збоїв із записом «самовідновлення» в розборі інциденту.
Чотири політики, якими ви справді користуєтесь
- no (за замовчуванням): Docker не перезапускатиме контейнер після виходу. Це не «небезпечно». Часто це найрозумніший вибір для пакетних завдань і одноразових скриптів.
- on-failure[:max-retries]: Перезапуск лише якщо контейнер вийшов із ненульовим кодом. За бажанням припиняє після N спроб. Це найближчий еквівалент до «спробуй кілька разів, потім зупинись».
- always: Перезапускає незалежно від коду виходу. Якщо демон рестартується, контейнер теж повертається. Це політика, яка перетворює «грамотне завершення» на «неочікуване воскресіння».
- unless-stopped: Як
always, але ручна зупинка зберігається після рестарту демона. Це «always, але з повагою до людського втручання».
Що Docker вважає «перезапуском» (і чому це важливо)
Docker перезапускає контейнер, коли виходить його головний процес. Це PID 1 всередині контейнера. Якщо ваш PID 1 — shell-скрипт, який форкає реальний сервіс, а потім завершується, Docker інтерпретуватиме це як «сервіс помер» і буде сумлінно перезапускати… вічно. Політика перезапуску не зламано; ваша стратегія ініціалізації — зламано.
Також: у Docker є вбудована затримка/бекоф перезапусків. Це не конфігурований circuit breaker у класичному Docker Engine. Вона запобігає дуже частим перезапускам на секунду, але не зупиняє стійкий цикл. Це просто робить інцидент довшим і більш заплутаним.
Цитата, бо вона все ще актуальна в 2026: «Сподівання — не стратегія.»
— генерал Гордон Р. Салліван.
Як вибрати політику в продакшні (думка)
Якщо ви запускаєте продакшн-сервіси на одиночних хостах (або на невеликих кластерах) з Docker Engine або Compose, ставте політики перезапуску як останню лінію захисту, а не як основний механізм надійності.
- Використовуйте
on-failure:5за замовчуванням для більшості довготривалих сервісів, які рідко падають. Якщо не вдається запустити після 5 спроб — щось не так. Зупиніться й викличте відповідальну людину. - Використовуйте
unless-stopped, якщо є вагома причина (наприклад, прості sidecar-агенти, локальна розробка або хости, що мають чисто підніматися після перезавантаження). Все одно інструментуйте це. - Уникайте
alwaysдля всього, що може швидко провалитись (погана конфігурація, відсутній секрет, невідповідність схеми, міграції). «Always» — це спосіб спалювати CPU, нічого не роблячи корисного. - Використовуйте
noдля пакетних завдань. Якщо нічний звіт провалюється, ймовірно, ви хочете, щоб він провалився голосно, а не запускався безкінечно й засипав фінансів 400 повідомленнями.
Перший короткий жарт: Контейнери не лікують себе; вони просто стають майстрами реінкарнації.
Факти та історія, що змінюють думку про перезапуски
Трохи контексту робить поведінку перезапуску Docker менш довільною й більш зрозумілою як набір компромісів, що можуть просочитися у вашу систему оповіщення.
- Політики перезапуску Docker походять ще до масового прийняття Kubernetes, коли керування контейнерами на одному хості було звичним випадком, і головним запитом було «тримати працюючим».
- Проблема «PID 1» — давня історія Unix: сигнали, зомбі та реханінг процесів. Контейнери не створили це; вони зробили неможливим ігнорувати.
- Семантика кодів виходу — це контракт: Docker використовує їх для
on-failure. Якщо ваш додаток виходить з кодом 0 при помилці («все добре!»), ви обрали хаос. - Цикли перезапусків існували задовго до контейнерів: units systemd з
Restart=alwaysможуть завдати ті ж проблеми. Docker просто зробив це легким в одну рядок. - Healthchecks з’явились пізніше, ніж багато хто думає. Довгий час «контейнер запущено» означало «процес існує», а не «сервіс працює». Це спадщина все ще формує звичні практики.
- Драйвери логів історично важливі: типовий драйвер
json-fileробив легко заповнити диск під час перезапусків. Це не теоретично; це повторна причина аварій. - Поведінка OOM-kill — це реальність на рівні ядра: контейнер не «завалився», ядро його вбило. Docker фіксує симптом; вам треба читати розтин.
- Бекоф Docker — не повний circuit breaker. Він уповільнює частоту перезапусків, але не вирішує питання зупинки. Це рішення — ваше через політику та автоматизацію.
Чому трапляються crash-loopи: моди відмов, а не моральні провали
Crash loop зазвичай викликаний одним із таких:
- Погана конфігурація або відсутня залежність: неправильна змінна середовища, відсутній файл, поганий DNS, недоступна БД, секрет не змонтовано.
- Додаток свідомо виходить: потрібні міграції, провал перевірки ліцензії, недійсний feature flag, образ «запустити один раз» використано як сервіс.
- Тиск на ресурси: OOM-kill, CPU-троттлінг що викликає таймаути, диск повний, вичерпано іноди, ліміти файлових дескрипторів.
- Неправильний порядок запуску: додаток стартує раніше, ніж БД/черга готові; без логіки повторних спроб воно виходить відразу.
- Поганий PID 1: shell-скрипти, що рано виходять; відсутній init; сигнали не обробляються; зомбі-процеси накопичуються й потім падають.
- Корумпований стан: томи містять часткові оновлення, lock-файли або версії схеми, що не відповідають бінарнику.
- Зовнішнє обмеження: upstream лімітує швидкість; додаток трактує це як фатальну помилку й виходить; перезапуски лише підсилюють thundering herd.
Другий короткий жарт: «Ми поставили restart: always за надійність» — це еквівалент того, щоб замотати стрічкою індикатор «check engine».
Швидкий план діагностики
Коли ви в інциденті і контейнер флапає, часу на філософію немає. Ось порядок дій, що швидко знаходить вузьке місце.
Спершу: з’ясуйте, чи це збій додатка чи платформи
- Перевірте лічильник перезапусків і останній код виходу. Якщо бачите код 1/2/78 або подібний — це ймовірно додаток/конфіг. Якщо 137 — думайте про OOM/kill. Якщо 0 з перезапусками — у вас встановлено
alwaysабо демон рестартувався. - Подивіться останні 50 рядків логів з попереднього запуску. Ви шукаєте явну помилку, а не постійне «starting…».
- Перевірте host dmesg/journal на OOM або помилки диска. Контейнери не скажуть, що їх убило ядро, якщо ви не спитаєте ядро.
По-друге: припиніть кровотечу (не втрачаючи доказів)
- Тимчасово відключіть перезапуски, щоб інспектувати стан і логи. Не видаляйте контейнер, поки не зняли потрібні докази.
- Зробіть знімок конфігурації й перевірте монтування. Більшість «таємничих циклів» — це «неправильний шлях файлу» з додатковими кроками.
По-третє: вирішіть, що ви виправляєте — додаток, хост чи політику
- Якщо це конфіг/залежність, виправте конфігурацію або імплементуйте повторні спроби/бекоф в додатку. Політика перезапуску — не алгоритм повторних спроб.
- Якщо це тиск на ресурси, встановіть обмеження пам’яті правильно, налаштуйте логування, збільшіть диск або перемістіть навантаження. Політики перезапуску не створюють ОЗП.
- Якщо це погана політика, переключіться на
on-failure:5абоunless-stoppedз оповіщеннями про перезапуски.
Практичні завдання (команди + вивід + рішення)
Ви хотіли команди. Ось вони. Кожне завдання включає: що виконати, що значить вивід і яке рішення прийняти далі.
Завдання 1: швидко ідентифікувати контейнер, що флапає
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.RunningFor}}'
NAMES IMAGE STATUS RUNNING FOR
api myco/api:1.42.0 Restarting (1) 6 seconds ago 2 minutes ago
postgres postgres:16 Up 3 hours (healthy) 3 hours ago
nginx nginx:1.25 Up 3 hours 3 hours ago
Значення: api перезапускається; Docker показує останній код виходу в дужках. Воно падає дуже швидко (кожні кілька секунд).
Рішення: Зосередьтесь на api. Не чіпайте поки що здорові залежності.
Завдання 2: переглянути політику перезапуску і лічильник перезапусків
cr0x@server:~$ docker inspect -f 'Name={{.Name}} Policy={{.HostConfig.RestartPolicy.Name}} Max={{.HostConfig.RestartPolicy.MaximumRetryCount}} Restarts={{.RestartCount}}' api
Name=/api Policy=always Max=0 Restarts=37
Значення: Політика — always; максимум спроб — необмежено; вже 37 перезапусків.
Рішення: Ви в «нескінченному циклі за дизайном». Плануйте відключити перезапуски для дебагу.
Завдання 3: отримати останній код виходу і причину завершення
cr0x@server:~$ docker inspect -f 'ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}} Error={{.State.Error}} FinishedAt={{.State.FinishedAt}}' api
ExitCode=1 OOMKilled=false Error= FinishedAt=2026-01-03T09:14:22.948601234Z
Значення: Код виходу 1, не OOM, без помилок рантайму. Ймовірно, логіка додатка/конфіг/залежність.
Рішення: Читайте логи з нещасного запуску.
Завдання 4: витягнути логи з попереднього запуску (не лише поточного флікера)
cr0x@server:~$ docker logs --tail 80 api
2026-01-03T09:14:21Z INFO starting api version=1.42.0
2026-01-03T09:14:22Z ERROR config: missing required env var: DATABASE_URL
2026-01-03T09:14:22Z INFO exiting with code=1
Значення: Контейнер поводиться правильно: відмовляється стартувати без конфігурації.
Рішення: Виправити інжекцію середовищних змінних/секретів. Також: саме тому always — поганий дефолт — ця помилка ніколи не вилікується сама.
Завдання 5: підтвердити середовище контейнера (що Docker вважає середовищем)
cr0x@server:~$ docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' api | sed -n '1,12p'
NODE_ENV=production
LOG_LEVEL=info
PORT=8080
Значення: Немає DATABASE_URL. Це не проблема DNS у рантаймі; це відсутня конфігурація під час створення контейнера.
Рішення: Оновіть Compose, systemd unit або інструмент деплойменту, щоб надати змінну (або змонтуйте файл і вкажіть на нього).
Завдання 6: зупинити цикл перезапусків без видалення контейнера
cr0x@server:~$ docker update --restart=no api
api
Значення: Політика перезапуску оновлена. Docker не перезапускатиме контейнер після наступної зупинки/виходу.
Рішення: Зупиніть його акуратно, потім виправте конфігурацію, після чого запустіть один раз з відомими робочими налаштуваннями.
Завдання 7: зупинити його зараз (щоб інспектувати стан і тиск хоста)
cr0x@server:~$ docker stop -t 10 api
api
Значення: Контейнер зупинений і має залишатися зупиненим, бо ви встановили restart=no.
Рішення: Якщо він все одно повертається, щось інше (systemd, watchdog) його відновлює.
Завдання 8: перевірити, чи systemd перезапускає сам Docker (рестарти демона можуть виглядати як перезапуски контейнера)
cr0x@server:~$ systemctl status docker --no-pager
● docker.service - Docker Application Container Engine
Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
Active: active (running) since Sat 2026-01-03 09:00:05 UTC; 20min ago
TriggeredBy: ● docker.socket
Docs: man:docker(1)
Main PID: 1123 (dockerd)
Tasks: 23
Memory: 312.4M
CPU: 1min 54.931s
Значення: Демон Docker зараз стабільний.
Рішення: Розглядайте це як проблему контейнера/додатку, а не флапінг демона.
Завдання 9: перевірити OOM-kill на рівні хоста (навіть коли Docker каже OOMKilled=false)
cr0x@server:~$ sudo journalctl -k --since "10 min ago" | tail -n 12
Jan 03 09:12:01 server kernel: Memory cgroup out of memory: Killed process 24081 (node) total-vm:1820040kB, anon-rss:612340kB, file-rss:2140kB, shmem-rss:0kB
Jan 03 09:12:01 server kernel: oom_reaper: reaped process 24081 (node), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
Значення: Ядро вбило процес у пам’яті cgroup. Залежно від таймінгу та того, як контейнер помер, флаги стану Docker не завжди дають повну картину.
Рішення: Якщо це збігається з PID вашого контейнера, потрібні ліміти пам’яті і/або виправлення додатку, а не додаткові перезапуски.
Завдання 10: підтвердити ліміти пам’яті й чи вони адекватні
cr0x@server:~$ docker inspect -f 'MemLimit={{.HostConfig.Memory}} MemSwap={{.HostConfig.MemorySwap}} PidsLimit={{.HostConfig.PidsLimit}}' api
MemLimit=268435456 MemSwap=268435456 PidsLimit=0
Значення: 256 MiB ліміт пам’яті з відсутністю swap‑простору. Це нормально для невеликого Go-сервісу; пастка для Node-додатку з великими heap.
Рішення: Підніміть ліміт або налаштуйте heap рантайму; потім переключіть на on-failure:5, щоб регресія не стала DoS проти вашого хоста.
Завдання 11: перевірити диск і ріст логів (цикли перезапусків люблять заповнювати диски)
cr0x@server:~$ df -h /var/lib/docker
Filesystem Size Used Avail Use% Mounted on
/dev/nvme0n1p3 200G 189G 1.2G 100% /var/lib/docker
Значення: Путь зберігання Docker повний. Це може спричинити дивні вторинні відмови (pull образів не працює, запис у контейнері не вдається, ризик корупції метаданих).
Рішення: Зупиніть флапінг контейнерів, акуратно очистіть простір і обмежте логи. Не перезапускайте безкінечно в повний диск.
Завдання 12: виявити контейнери з величезними JSON-логами
cr0x@server:~$ sudo du -h /var/lib/docker/containers/*/*-json.log 2>/dev/null | sort -h | tail -n 5
2.1G /var/lib/docker/containers/8f2c.../8f2c...-json.log
3.8G /var/lib/docker/containers/31ab.../31ab...-json.log
5.4G /var/lib/docker/containers/aa90.../aa90...-json.log
6.0G /var/lib/docker/containers/3c11.../3c11...-json.log
7.2G /var/lib/docker/containers/1d77.../1d77...-json.log
Значення: Деякі контейнери пишуть багатогігабайтні логи. Цикли перезапусків швидко це множать, бо кожний старт логуватиме банери й stack trace.
Рішення: Увімкніть ротацію логів у конфігу демона і виправте галасливий додаток. Тим часом обережно звільніть простір.
Завдання 13: перевірити часові мітки останньої спроби старту контейнера
cr0x@server:~$ docker inspect -f 'StartedAt={{.State.StartedAt}} FinishedAt={{.State.FinishedAt}}' api
StartedAt=2026-01-03T09:14:21.115312345Z FinishedAt=2026-01-03T09:14:22.948601234Z
Значення: Живе приблизно ~1.8 секунди. Це не «транзитна» помилка; це детермінований збій на стартапі.
Рішення: Припиніть використовувати always. Виправте конфіг, потім стартуйте один раз, і лише потім включайте обмежену політику перезапуску.
Завдання 14: отримати точну команду/entrypoint, який запускає Docker
cr0x@server:~$ docker inspect -f 'Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}}' api
Entrypoint=["/bin/sh","-c"] Cmd=["/app/start.sh"]
Значення: PID 1 — /bin/sh -c, що запускає скрипт. Класичне джерело проблем з обробкою сигналів і «скрипт рано завершується».
Рішення: Перегляньте скрипт. Віддавайте перевагу exec-form entrypoint і додавайте init, якщо потрібно.
Завдання 15: відтворити збій інтерактивно (без політики перезапуску)
cr0x@server:~$ docker run --rm -it --entrypoint /bin/sh myco/api:1.42.0 -lc '/app/start.sh; echo exit=$?'
config: missing required env var: DATABASE_URL
exit=1
Значення: Ви відтворили проблему поза флапінг-контейнером. Це прогрес: помилка детермінована.
Рішення: Виправляйте інжекцію середовища, а не Docker.
Завдання 16: застосувати розумну політику після виправлення конфігурації
cr0x@server:~$ docker update --restart=on-failure:5 api
api
Значення: Якщо воно падатиме повторно, зупиниться після п’яти невдач.
Рішення: Поєднайте це з оповіщенням по лічильнику перезапусків, щоб «зупинилось після п\’яти» було сторожовим сигналом, а не тихим простоєм.
Завдання 17: перевірити поведінку healthcheck (healthcheck-и самі по собі не перезапускають)
cr0x@server:~$ docker inspect -f 'Health={{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' api
Health=unhealthy
Значення: Docker помічає стан як unhealthy, але він не перезапустить контейнер лише через це (класична поведінка Docker Engine).
Рішення: Якщо вам потрібен «unhealthy → restart», потрібен зовнішній контролер (або інший оркестратор), або імплементуйте в додатку самозавершення при невідновлюваному стані здоров’я (обережно).
Завдання 18: стежити за подіями перезапуску в реальному часі
cr0x@server:~$ docker events --since 5m --filter container=api
2026-01-03T09:10:21.115Z container start 8b4f... (name=api)
2026-01-03T09:10:22.948Z container die 8b4f... (exitCode=1, name=api)
2026-01-03T09:10:24.013Z container start 8b4f... (name=api)
Значення: Ви бачите каденцію циклу та коди виходу. Корисно, коли логи шумні або вже повернуті.
Рішення: Якщо перезапуски корелюють із подіями хоста (рестарт демона, мережевий флап), розширте діагностику. Якщо каденція стабільна — це додаток/конфіг.
Healthchecks, готовність залежностей і міф «restart вирішить»
Більшість циклів перезапусків — це проблеми готовності залежностей, які маскуються під «дивну поведінку Docker». Додаток стартує, раз спробує підключитись до бази, падає, виходить. Docker перезапускає. Повторюйте, поки або всесвіт не охолоне, або поки БД щасливо не повернеться і вам пощастить.
Що робити в додатку: ретраї з бекофом і розрізнення фатального/транзитного
Якщо база недоступна 30 секунд під час обслуговування, негайний вихід — не «чистий». Це крихке рішення. Реалізуйте повторні спроби з експоненційним бекофом і максимальним бюджетом часу. Якщо помилка фатальна (невірний пароль, неправильний хост) — вийдіть одного разу і нехай обмежені on-failure спроби впораються з транзитними проблемами при порядок запуску.
Що робити в Docker: використовувати healthchecks для спостереження й шлюзування, а не магічного лікування
Healthcheck-и цінні тим, що дають машинозчитувальний сигнал: healthy/unhealthy. У класичному Docker вони не перезапускають автоматично, але вони:
- допомагають бачити «процес існує, але сервіс мертвий»,
- інтегруються з Compose
depends_onумовами (в новіших реалізаціях Compose), - дають зовнішньому моніторингу кращий сигнал, ніж «контейнер існує».
Перевірки залежностей: уникайте «wait-for-it» скриптів, що ніколи не закінчуються
Є два стилі відмов:
- Fail-fast цикли: додаток виходить негайно, Docker перезапускає. Шумно, але очевидно.
- Hang-forever старти: entrypoint чекає залежність вічно. Docker думає, що контейнер «Up», але він нічого не обслуговує. Тихо, але смертельно.
Надавайте перевагу обмеженим очікуванням з явними таймаутами. Якщо залежність не з’явилась, виходьте з ненульовим кодом і нехай on-failure зробить кілька спроб, а потім зупиниться.
Docker Compose, Swarm і чому ваша політика може не бути застосована
Поведінка політики перезапуску залежить від способу деплойменту.
Compose: restart: легко встановити і легко забути
Compose робить тривіальним розкидати restart: always по файлу. Команди роблять це, бо «зменшує кількість інцидентів». Воно також зменшує вивчення системи, поки не настане день, коли просту неправильну конфігурацію воно перетворить на шторми логів по всьому кластеру.
Також: версії Compose важливі. Деякі поля під deploy: ігноруються, якщо ви не використовуєте Swarm. Люди копіпастять конфіги і вважають, що вони активні. Вони ні.
Swarm services: поведінка рестарту — інша модель
Swarm має власний цикл реконціляції. Політики перезапуску там — частина планування сервісів, а не лише поведінки локального демона. Якщо ви на Swarm, використовуйте умови рестарту та затримки на рівні сервісу. Якщо ви не на Swarm, не прикидайтеся ним, використовуючи ключі deploy: в Compose і очікуючи, що вони працюватимуть.
systemd навколо Docker: подвійні цикли перезапусків — реальна річ
Поширений патерн: unit systemd запускає docker run, а systemd має Restart=always. Контейнер Docker має --restart=always. Коли щось йде не так, обидва шари «допомагають». Тепер у вас цикл перезапусків, що виживає після рестарту демона і не зупиняється, бо systemd миттєво його відтворює.
Якщо ви мусите використовувати systemd, нехай systemd відповідає за рестарт, а політику контейнера встановіть в no. Або навпаки. Оберіть одного «дорослого в кімнаті».
Спостереження і запобіжні заходи: обмежувачі для власного хаосу
Політика перезапуску без оповіщень — це просто тихий провал із додатковими кроками. Вашою метою не є «вічно перезапускатися». Мета — «швидко відновлюватися від транзитних помилок і виокремлювати персистентні помилки». Це означає запобіжні заходи.
Запобіжний захід 1: оповіщення по перезапускам і по швидкості перезапусків
Лічильник перезапусків сам по собі недостатній. Контейнер, що перезапускається раз на день — може бути нормою. Контейнер, що перезапускається 50 разів за 5 хвилин — інцидент. Відстежуйте і абсолютну кількість, і швидкість. Якщо у вас немає пайплайну метрик, ви все ще можете зробити cron + docker inspect і простий файл стану. Це не гламурно, але краще, ніж пояснювати керівництву, чому рахунок за логування подвоївся за ніч.
Запобіжний захід 2: ротація логів на рівні демона
Якщо ви використовуєте json-file (багато хто так робить), налаштуйте ротацію. Цикли перезапусків + необмежені логи + малі диски — передбачуваний генератор відмов.
Запобіжний захід 3: обмежені повторні спроби на рівні політики
on-failure:5 не ідеальний, але воно створює ясний стан: «це постійно зламано». Той стан є дієвим. «Воно перезапускається вічно» — ні.
Запобіжний захід 4: ліміти ресурсів, що відповідають реальності
Неврегульована пам’ять робить контейнер здатним вивести хост із ладу. Надто жорсткий ліміт пам’яті робить його перезапускатись вічно. Обидва варіанти погані. Встановлюйте розумні ліміти і моніторте фактичне використання. Розглядайте ліміти як інструмент SLO, а не покарання.
Три міні-історії з корпоративного світу (анонімізовано, правдоподібно, технічно точно)
Міні-історія 1: Індцидент через хибне припущення
В одній середній компанії команда мігрувала легасі API з VM до Docker на парі потужних хостів. Вони пишалися: менше складності, легші деплойменти, однакові середовища. Вони додали --restart=always, «щоб сервіс залишався вгорі». Ніякої оркестрації, ніякого зовнішнього супервізора. Просто Docker Engine і впевненість.
Під час ротації секретів пароль бази змінився. Новий секрет потрапив у сховище, але job деплойменту, що перебудовував контейнери, упав наполовину, залишивши один хост зі старою змінною середовища і новим образом. API стартував, провалив автентифікацію і вийшов з кодом 1. Docker перезапустив його. Ще раз. І ще.
Логи були повні помилок автентифікації, записаних у json-file на спільному диску. За годину диск з /var/lib/docker був майже повний. Потім інші контейнери почали провалюватися при записі стану. Моніторинг почав флапати, бо його власний контейнер не міг записати на диск. На черговому здавалося, що «усе перезапускається», і спочатку підозрювали ядро.
Хибне припущення було тонким: вони вважали, що політика перезапуску — це функція надійності. Ні. Це функція персистентності. Персистентна помилка — все ще помилка; вона просто стає голоснішою з часом.
Виправлення було нудним: переключити сервіси на on-failure:5, ротація логів і — найважливіше — трактувати відсутні/недійсні секрети як інцидент деплойменту з чітким шляхом відкату.
Міні-історія 2: Оптимізація, що відкотилась
Інша організація тримала флот хостів Docker для внутрішніх інструментів. Хтось помітив, що рестарти сервісів під час деплойментів повільні через великі образи та стартові перевірки. Вони оптимізували: обрізали образ, прибрали купу перевірок і замінили entrypoint на маленький shell-оболонку, що виставляла змінні середовища й запускала сервіс. Деплоями стали швидші. Усім аплодували.
Через тижні upstream почав повертати інтермітентні TLS-помилки через проблему ланцюга сертифікатів. Раніше додаток повторював спроби протягом хвилини перед виходом; одна із видалених «допоміжних перевірок» включала цикл перевірки мережі. Тепер воно провалювалось швидко й виходило відразу. Оскільки сервіс мав restart: always, флот молотив по upstream ще сильніше, створюючи зворотний цикл. Upstream лімітував їх, що робило відмови частішими і підсилювало перезапуски — замкнене коло болю.
Ставало гірше: shell-оболонка була PID 1 і не форвардила сигнали коректно. Під час пом’якшення оператори намагалися зупинити контейнери, але завершення були непослідовні і іноді зависали, лишаючи зайняті порти. Це робило наступні перезапуски відрізняльними («address already in use»), що ускладнило інцидент і продовжило час відновлення.
Оптимізація не була однозначно неправильною — менші образи корисні — але зміни вилучили логіку стійкості з додатка й замінили її на «Docker перезапустить». Docker зробив саме це, і результат був технічно правильним та операційно катастрофічним.
Кінцеве виправлення: відновити коректну логіку повторних спроб з jitter, використовувати exec-form entrypoints (і init там, де потрібно), та змінити політику на обмежені повторні спроби. Вони також імплементували failure budgets для upstream, щоб уникнути штампування залежностей під час часткових відмов.
Міні-історія 3: Сухе, але правильне практичне рішення, що врятувало
Команда fintech запускала сервіс, суміжний з платіжною системою, через Docker Compose на невеликому кластері хостів. Нічого екзотичного. Але вони мали дисципліну: кожен сервіс використовував on-failure:3, якщо не було письмового винятку, і кожен контейнер мав healthcheck. Лічильники перезапусків відправлялися як метрики і викликали пейдж при перевищенні швидкості.
Одного ранку нова збірка потрапила в продакшн з тонкою помилкою парсингу конфігу. Сервіс виходив з кодом 78 (помилка конфігу) відразу після логування одного чіткого рядка. Перші три перезапуски сталися швидко, потім контейнер зупинився. Черговий отримав пейдж: «сервіс зупинився після retry». Логи були короткі і читабельні завдяки глобальній ротації. Хост залишився здоровим, бо цикл завершився політикою, а не вдачею.
Вони відкотили зміни за кілька хвилин. Ніяких каскадних заповнень диска, ніякого «чому CPU на максимумі», ніякого шумного сусіда, що впливає на інші сервіси. Розбір інциденту був майже нудним — найвища похвала для операцій.
Практика, що їх врятувала, не була екзотичною. Це були два дефолти: обмежені перезапуски і оповіщення при досягненні межі. Контейнер не «самовилікувався». Система сама звітувала.
Типові помилки: симптом → корінна причина → виправлення
1) Симптом: Контейнер перезапускається вічно з тим самим рядком логів
Корінна причина: restart: always (або unless-stopped) + детермінований збій на старті (відсутня змінна середовища, відсутній файл, поганий прапорець).
Виправлення: Переключіть на on-failure:5, виправте інжекцію конфігу і зробіть так, щоб додаток виводив один ясний рядок помилки перед виходом.
2) Симптом: перезапуски показують код виходу 137
Корінна причина: OOM kill або примусове завершення. Часто занадто жорсткий ліміт пам’яті або витік пам’яті.
Виправлення: Підтвердьте логи ядра на OOM, збільште ліміт пам’яті або налаштуйте heap рантайму, і додайте моніторинг пам’яті. Обмежені перезапуски запобігають thrash хоста.
3) Симптом: docker stop працює, але контейнер повертається
Корінна причина: Політика — always (або інший супервізор відтворює його: systemd, cron, CI агент).
Виправлення: docker update --restart=no і перевірка зовнішніх супервізорів. Нехай тільки один шар відповідає за рестарт.
4) Симптом: Контейнер «Up», але сервіс мертвий
Корінна причина: Відсутній healthcheck і процес живий, але завислий (deadlock, залежність). Політика перезапуску не допоможе, бо нічого не виходить.
Виправлення: Додайте healthcheck і зовнішнє оповіщення; розгляньте watchdog, що рестартує на постійний unhealthy (обережно), або виправте причину deadlock.
5) Симптом: Після перезавантаження хоста контейнери, які ви «зупиняли», знову працюють
Корінна причина: restart: always ігнорує попередні ручні зупинки після рестарту демона; unless-stopped їх поважає.
Виправлення: Використовуйте unless-stopped, коли ручна зупинка має зберігатися після рестарту демона, або передайте контроль вищому оркестратору.
6) Симптом: Диск заповнюється під час інциденту
Корінна причина: Цикли перезапусків підсилюють логування; типовий json-file лог без ротації — не обмежений.
Виправлення: Налаштуйте ротацію логів демона, зменшіть стартовий шум логів і обмежте перезапуски, щоб помилка не генерувала нескінченні логи.
7) Симптом: «depends_on» не запобіг crash loop
Корінна причина: Порядок старту — не те саме, що готовність. Контейнер залежності може бути «up», але не готовим приймати з’єднання.
Виправлення: Додайте readiness-перевірки і логіку повторних спроб; використовуйте healthchecks і readiness gates, де підтримується.
8) Симптом: Грамотне завершення не відбувається; ризик корупції даних
Корінна причина: PID 1 — shell-обгортка, що не форвардить сигнали; додаток не обробляє SIGTERM; таймаут зупинки занадто малий.
Виправлення: Використовуйте exec-form entrypoint, додайте init (наприклад, --init), обробляйте сигнали і встановіть розумні таймаути зупинки.
Чеклісти / покроковий план
Покроковий план: як виправити політики перезапуску, не зламаючи продакшн
- Інвентаризація поточних політик. Перелічіть контейнери та їх політики перезапуску. Позначте все з
alwaysбез очевидного виправдання. - Класифікуйте сервіси. Пакетні роботи, безстанні сервіси, стейтфул сервіси, інфра-агенти. Кожному — свій дефолт.
- Оберіть політику за замовчуванням: зазвичай
on-failure:5для сервісів,noдля джобів,unless-stoppedдля невеликого набору агентів «повинні піднятися після ребуту». - Додайте оповіщення по швидкості перезапусків. Якщо не можете, принаймні створіть щоденний звіт і поріг пейджингу «зупинився після N retry».
- Увімкніть ротацію логів. На рівні демона. Не покладайтеся на кожну команду додатка.
- Перегляньте entrypoint-и. Shell-обгортки вимагають додаткової уваги. Додайте
--init, де це допомагає. - Тестуйте моди відмов. Від’єднайте БД (образно). Видаліть секрет. Переконайтесь, що система провалюється голосно і передбачувано.
- Впроваджуйте зміни поступово. Один хост або одна група сервісів за раз. Спостерігайте за «прихованими залежностями» на нескінченні перезапуски (так, таке буває).
- Документуйте винятки. Якщо сервіс справді потребує
always, опишіть чому і яке оповіщення ловить його цикл перезапусків.
Чекліст: що захопити під час інциденту crash loop
- Політика перезапуску і лічильник перезапусків (
docker inspect). - Останній код виходу і прапор OOMKilled.
- Останні 100 рядків логів (перед ротацією або очищенням).
- Логи ядра хоста для OOM/помилок диска/мережі.
- Використання диска для
/var/lib/dockerі точок монтування томів. - Будь-яка конфігурація зовнішнього супервізора (systemd units, cron jobs, CI ранери).
Поширені питання
1) Чи варто використовувати restart: always в продакшні?
Рідко. Використовуйте лише коли ви розумієте моди відмов і маєте оповіщення по швидкості перезапусків. За замовчуванням для сервісів — on-failure:5.
2) У чому практична різниця між always і unless-stopped?
unless-stopped поважає ручну зупинку після рестарту демона. always поверне контейнер після рестарту демона, навіть якщо ви раніше зупинили його вручну.
3) Чи перезапустить Docker контейнер, коли він стане unhealthy?
Не за замовчуванням у класичному Docker Engine. Healthcheck-и маркують стан; вони не тригерять рестарт автоматично. Для цього потрібен зовнішній контролер.
4) Якщо мій додаток виходить 0 при помилці, чи перезапустить його on-failure?
Ні. on-failure перезапускає лише при ненульовому коді виходу. Виправте коди виходу вашого додатка; це частина експлуатаційного контракту.
5) Чому я не можу docker exec в контейнер, що флапає?
Бо він не працює достатньо довго. Вимкніть перезапуски (docker update --restart=no), зупиніть його, потім запустіть образ інтерактивно з shell для відтворення помилки.
6) За якими кодами виходу варто слідкувати?
Типові: 1 — загальна помилка (читайте логи), 137 — часто killed/OOM, 143 — SIGTERM, 0 — успішний вихід (але якщо є перезапуски, ймовірно ви поставили always або рестарт демона відбувся).
7) Чи можуть політики перезапуску приховувати простої?
Так. Вони можуть перетворити «сервіс впав» на «сервіс флапає», що на поверхневому моніторингу виглядає живим. Оповіщайте про перезапуски і рівень сервісу, а не лише про існування контейнера.
8) Чи варто встановлювати максимум повторних спроб?
Так, для більшості сервісів. Це створює стабільний кінцевий стан для постійних помилок і запобігає безкінечному використанню ресурсів. Поєднуйте з оповіщенням, щоб «зупинилось після retry» було дієвим.
9) Як найкраще запобігти заповненню диска під час циклу перезапусків?
Обмежуйте перезапуски, налаштуйте ротацію логів на рівні демона і зменшіть шумні стартові логи. Також моніторте використання /var/lib/docker явно.
10) Хіба Kubernetes не кращий у цьому?
Kubernetes дає сильніші контролери і примітиви, але він теж може створити crash-loopи, якщо неправильно налаштувати проби і бекофи. Принцип лишається: перезапуски — не виправлення для детермінованих помилок.
Висновок: наступні кроки, які можна впровадити цього тижня
Політики перезапуску — це скальпель, а не ізоляційна стрічка. Використовуйте їх, щоб відновитися від транзитних помилок, а не щоб тримати зламаний бінарник у нескінченному крузі, поки ваші логи з\’їдають диск.
Практичні кроки:
- Аудит усіх контейнерів на предмет
restart: alwaysі обґрунтуйте кожен випадок. - Змініть дефолт на
on-failure:5для сервісів і наnoдля джобів. - Увімкніть ротацію логів на рівні демона, якщо ви використовуєте
json-file. - Додайте оповіщення по швидкості перезапусків і по «зупинено після retry».
- Виправте PID 1 і обробку сигналів в образах, що використовують shell entrypoint; використовуйте exec-form і init, коли потрібно.
Тоді наступного разу, коли щось зламається о 2-й ночі, воно поведеться як доросла система: один раз, чітко і з достатньою доказовою базою для виправлення.