Docker: чому ваш контейнер перезапускається вічно (і єдиний журнал, який вам потрібен)

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

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

Ось момент, коли люди годинами пускають очі по docker ps і роблять здогадки. Не робіть так. Коли контейнер перезапускається вічно, існує один журнал, який надійно підкаже, що сталося: журнали з попередньої спроби, а не поточної, що ще завантажується.

Єдиний журнал, який вам потрібен (і чому це не той, що ви дивитеся)

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

Журнал, який вам потрібен — це попередня спроба контейнера:

cr0x@server:~$ docker logs --previous myservice
error: cannot open config file /etc/myapp/config.yaml: permission denied

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

Чому це працює: потік журналу Docker прив’язаний до життєвого циклу конкретного контейнера. Коли контейнер пересоздається або перезапускається (залежно від сценарію і рантайму), вам потрібні stdout/stderr останнього запуску. Саме це дає --previous для циклів перезапуску, коли контейнер перезапускається під тим самим іменем.

І так, є зауваження. Якщо ви використовуєте Compose і контейнери пересоздаються (нові ID контейнерів), а не перезапускаються, можливо, доведеться тягнути журнали за ID контейнера або використовувати docker compose logs. Але принцип залишається: перестаньте дивитися на поточну спробу завантаження й проаналізуйте останній збій.

Що насправді означає «перезапускається вічно» в Docker

Цикл перезапуску — це не одна річ. Це сімейство поведінок, які з відстані виглядають однаково: контейнер відображається як «Restarting (x) …» або постійно з’являється в docker ps.

Політики перезапуску: дрібниці, які люди пропускають

Цикли перезапуску зазвичай керуються політикою перезапуску. Docker підтримує:

  • no (за замовчуванням): він завершується і залишається мертвим.
  • on-failure[:max-retries]: перезапускає при ненульовому коді виходу.
  • always: перезапускає незалежно від коду виходу (крім випадку, коли ви зупиняєте його).
  • unless-stopped: перезапускає, якщо ви явно не зупинили його.

У продакшні зазвичай хочуть unless-stopped для сервісів, що працюють довго. Але «хороші дефолти» перетворюються на «шум», коли процес падає миттєво. Політика вірно перезапускає зламане — як сумлінний співробітник, вона робить саме те, що ви попросили, а не те, що ви мали на увазі.

Коди виходу — ваш перший реальний натяк

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

  • Додаток вирішив завершитися (помилка конфігурації, збій міграції, відсутня залежність).
  • ОС його вбила (OOM killer, SIGKILL, обмеження cgroup).
  • Ви ініціювали це (неуспішний healthcheck, watchdog, unit systemd).

Код виходу і прапорець «OOMKilled» підкажуть, на який шлях ви потрапили. Ви діагностуєте не «Docker», а чому PID 1 у цьому контейнері не може залишатися живим.

Цитата, яку варто тримати в голові під час дебагу: «Надія — не стратегія.» — генерал Гордон Р. Салліван. Це не строго SRE-цитата, але вона жорстко підходить до циклів перезапуску.

Швидкий план діагностики (перший/другий/третій)

Це план, який я використовую, коли контейнер флапає і мені треба швидко знайти вузьке місце, не перетворюючи інцидент на дослідницький проєкт.

Перший: зафіксуйте останній збій (не дивіться поточний старт)

  1. Отримайте попередні журнали: docker logs --previous (або за ID контейнера).
  2. Отримайте код виходу та причину: docker inspect для State.ExitCode, State.OOMKilled, State.Error.

Якщо попередні журнали показують явну помилку конфігурації — зупиніться. Виправте її. Не «додавайте більше пам’яті» до YAML-опечатки.

Другий: визначте, чи це крах, вбивство або свідомий перезапуск

  1. Перевірте dmesg / журнал на предмет OOM kill.
  2. Перевірте стан healthcheck контейнера (unhealthy може спричиняти рестарти, навіть коли процес живе).
  3. Перевірте, хто його перезапускає: політика Docker, systemd, Compose, Swarm.

Третій: перевірте середовище виконання (зберігання, мережа, залежності)

  1. Монтування та дозволи: bind mounts, secrets, файли конфігурації.
  2. Порти і DNS: чи він не може прив’язатися до порту, не може розв’язати ім’я, проблеми з TLS?
  3. Обмеження ресурсів: пам’ять, pids, ulimits, місце на диску, вичерпання inode.

Анекдот №1: Контейнери як кімнатні рослини — ігноруєш базу (воду, світло, ґрунт) і вони вчасно помруть.

Практичні завдання: команди, виходи і рішення (12+)

Нижче — завдання, які реально рухають вас уперед. Кожне включає реалістичну команду, приклад виводу, що це означає і яке рішення прийняти далі. Виконуйте їх на хості, якщо не зазначено інше.

Завдання 1: Подивіться цикл перезапуску і отримайте ID контейнера

cr0x@server:~$ docker ps --no-trunc
CONTAINER ID                                                       IMAGE               COMMAND                  CREATED          STATUS                         PORTS                    NAMES
b5a1c0b7fd4b1f7b4b5d5c6a9c8d2d9c7a1c2e3f4a5b6c7d8e9f0a1b2c3d4e5   myapp:1.4.2         "/entrypoint.sh"         2 minutes ago    Restarting (1) 5 seconds ago                            myservice

Значення: Docker показує «Restarting» із кодом виходу в дужках. Це число зазвичай — останній код виходу.

Рішення: Негайно переходьте до попередніх журналів та перевірки стану.

Завдання 2: Отримайте потрібний журнал

cr0x@server:~$ docker logs --previous myservice
[2026-02-04T10:15:02Z] FATAL: DB_URL is not set
[2026-02-04T10:15:02Z] exiting with code 2

Значення: Додаток коректно завершується з помилкою через відсутню змінну середовища.

Рішення: Виправте конфігурацію на рівні розгортання (Compose env_file, secrets, CI). Не чіпайте налаштування демонa Docker.

Завдання 3: Інспектуйте стан, код виходу та OOMKilled

cr0x@server:~$ docker inspect -f 'ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}} Error={{.State.Error}} FinishedAt={{.State.FinishedAt}}' myservice
ExitCode=137 OOMKilled=true Error= FinishedAt=2026-02-04T10:15:19.120401234Z

Значення: Код виходу 137 разом з OOMKilled=true — класичне вбивство через пам’ять (SIGKILL).

Рішення: Перевірте журнали ядра та обмеження пам’яті контейнера. Це не «помилка додатка», поки не доведено протилежне.

Завдання 4: Підтвердіть OOM kill у системних логах

cr0x@server:~$ sudo dmesg -T | tail -n 20
[Sun Feb  4 10:15:19 2026] Memory cgroup out of memory: Killed process 23184 (myapp) total-vm:812340kB, anon-rss:512120kB, file-rss:1200kB, shmem-rss:0kB
[Sun Feb  4 10:15:19 2026] oom_reaper: reaped process 23184 (myapp), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB

Значення: Ядро вбило процес через тиск пам’яті в cgroup.

Рішення: Збільште ліміт пам’яті контейнера, зменшіть використання пам’яті або виправте витік. Також переконайтеся, що хост має запас; просто підняти ліміт без додаткової ємності — це просто переміщення проблеми.

Завдання 5: Перевірте застосовані обмеження ресурсів контейнера

cr0x@server:~$ docker inspect -f 'Memory={{.HostConfig.Memory}} MemorySwap={{.HostConfig.MemorySwap}} PidsLimit={{.HostConfig.PidsLimit}}' myservice
Memory=268435456 MemorySwap=268435456 PidsLimit=100

Значення: 256 MiB ліміт пам’яті без додаткового swap; тісно для багатьох рантаймів. Ліміт PID теж може вплинути на програми, що активно форкаються.

Рішення: Якщо сервіс має бути більшим — підвищіть ліміти. Якщо він має бути малим — профілюйте пам’ять і усуньте піки (JIT warmup, кеші, міграції).

Завдання 6: Визначте, чи політика перезапуску примушує цикл

cr0x@server:~$ docker inspect -f 'Name={{.Name}} RestartPolicy={{.HostConfig.RestartPolicy.Name}} MaximumRetryCount={{.HostConfig.RestartPolicy.MaximumRetryCount}}' myservice
Name=/myservice RestartPolicy=always MaximumRetryCount=0

Значення: «always» означає, що буде перезапуск навіть при ExitCode=0. MaximumRetryCount=0 означає безліч спроб.

Рішення: Під час налагодження розгляньте тимчасову зміну на on-failure:5 або відключення рестартів, щоб ви могли оглянути мертвий контейнер, не даючи йому одразу знову запускатися.

Завдання 7: Зупиніть петлю настільки, щоб безпечно оглянути

cr0x@server:~$ docker update --restart=no myservice
myservice

Значення: Ви змінили політику перезапуску для цього екземпляра контейнера.

Рішення: Тепер зупиніть його, а потім запускайте вручну, коли готові. Також виправте джерело (файл Compose, unit systemd) або воно повернеться при наступному деплої.

Завдання 8: Інспектуйте події контейнера, щоб побачити ритм і причину

cr0x@server:~$ docker events --since 10m --filter container=myservice
2026-02-04T10:15:18.992345678Z container die b5a1c0b7fd4b (exitCode=137, image=myapp:1.4.2, name=myservice)
2026-02-04T10:15:19.101234567Z container start b5a1c0b7fd4b (image=myapp:1.4.2, name=myservice)
2026-02-04T10:15:24.220987654Z container die b5a1c0b7fd4b (exitCode=137, image=myapp:1.4.2, name=myservice)

Значення: Чіткий цикл перезапусків. Код виходу повторюється.

Рішення: Повторювані однакові коди виходу зазвичай означають детермінований збій на старті (конфігурація, права, прив’язка порту) або детермінований вбивство (OOM під час прогріву). Сфокусуйтеся там, а не на випадковій мережевій нестабільності.

Завдання 9: Перевірте стан healthcheck (healthcheck можуть створювати «м’які петлі»)

cr0x@server:~$ docker inspect -f 'Health={{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}} FailingStreak={{if .State.Health}}{{.State.Health.FailingStreak}}{{else}}0{{end}}' myservice
Health=unhealthy FailingStreak=12

Значення: Процес контейнера може бути запущеним, але healthcheck постійно провалюється. Деякі налаштування (Compose з залежностями, зовнішні вотчдори) реагують перезапуском.

Рішення: Далі інспектуйте команду healthcheck та її вивід. Ставтеся до неї як до продакшн-коду, бо це так і є.

Завдання 10: Отримайте журнали healthcheck

cr0x@server:~$ docker inspect -f '{{range .State.Health.Log}}{{.End}} {{.ExitCode}} {{.Output}}{{end}}' myservice
2026-02-04T10:16:02.000000000Z 1 curl: (7) Failed to connect to 127.0.0.1 port 8080: Connection refused
2026-02-04T10:16:12.000000000Z 1 curl: (7) Failed to connect to 127.0.0.1 port 8080: Connection refused

Значення: Ваш додаток не слухає порт, на який очікує healthcheck (або слухає на іншому інтерфейсі, або ще не стартував).

Рішення: Виправте адресу/порт прослуховування додатку або healthcheck. Якщо старт довгий — налаштуйте start_period, щоб уникнути передчасних невдач.

Завдання 11: Виявлення конфліктів прив’язки порту на хості

cr0x@server:~$ sudo ss -ltnp | grep ':8080 '
LISTEN 0      4096         0.0.0.0:8080       0.0.0.0:*    users:(("old-nginx",pid=1187,fd=7))

Значення: Щось інше вже слухає цей хост-порт.

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

Завдання 12: Перевірте монтування і дозволи (тихий вбивця)

cr0x@server:~$ docker inspect -f '{{range .Mounts}}{{.Type}} {{.Source}} -> {{.Destination}} (RW={{.RW}}){{"\n"}}{{end}}' myservice
bind /srv/myservice/config.yaml -> /etc/myapp/config.yaml (RW=false)
volume myservice-data -> /var/lib/myapp (RW=true)

Значення: Конфіг — це bind-mount і він тільки для читання. Це добре. Але якщо додаток намагається записати в нього — він впаде.

Рішення: Переконайтеся, що додаток пише тільки в доступні для запису шляхи. Якщо йому потрібно генерувати конфіг — монтуйте директорію та пишіть туди, або змініть поведінку додатку.

Завдання 13: Увійдіть у налагоджувальний шел (не змінюючи образ)

cr0x@server:~$ docker run --rm -it --network container:myservice --pid container:myservice --entrypoint /bin/sh myapp:1.4.2
/ # ps aux
PID   USER     TIME  COMMAND
1     root      0:00 myapp --config /etc/myapp/config.yaml
/ # netstat -ltn
Active Internet connections (only servers)
tcp        0      0 127.0.0.1:9090          0.0.0.0:*               LISTEN

Значення: Ви можете спостерігати процеси та порти в неймспейсах контейнера. Тут він слухає 9090, а не 8080.

Рішення: Виправте healthcheck / мапінг портів. Неймспейси прибирають здогадки.

Завдання 14: Перевірте тиск на файлову систему та вичерпання inode

cr0x@server:~$ df -h /var/lib/docker
Filesystem      Size  Used Avail Use% Mounted on
/dev/nvme0n1p3  100G   98G  2.0G  99% /var/lib/docker
cr0x@server:~$ df -i /var/lib/docker
Filesystem       Inodes   IUsed    IFree IUse% Mounted on
/dev/nvme0n1p3  6553600  6551000     2600  100% /var/lib/docker

Значення: Диск майже повний і іноди вичерпані. Контейнери можуть фейлити дивними способами: не записати PID-файл, не витягти шари, не додати рядок у журнали.

Рішення: Очистіть образи/контейнери/томи, збільшіть сховище або перемістіть Docker root. Потім перевірте знову. Якщо не виправити іноди — «додати 10ГБ» може не допомогти.

Завдання 15: Перевірте журнали демона Docker (іноді демон — це винуватець)

cr0x@server:~$ sudo journalctl -u docker --since "10 minutes ago" -n 50
Feb 04 10:15:19 server dockerd[1023]: containerd: time="2026-02-04T10:15:19Z" level=warning msg="failed to shim reaping" id=b5a1c0b7fd4b
Feb 04 10:15:19 server dockerd[1023]: Error response from daemon: OCI runtime create failed: runc create failed: unable to start container process: error during container init: error mounting "/srv/myservice/config.yaml" to rootfs at "/etc/myapp/config.yaml": permission denied: unknown

Значення: OCI/рантайм помилки можуть не дозволити контейнеру навіть почати старт. Це відрізняється від «додаток стартував і впав».

Рішення: Виправте права монтувань, SELinux/AppArmor профілі або наявність шляхів на хості. Інженери аплікації не можуть виправити те, що взагалі не стартує.

Режими відмов, що викликають цикли перезапуску

Більшість циклів перезапуску належать до цих категорій. Вивчіть патерн — і перестанете трактувати кожний інцидент як унікальну сніжинку.

1) Додаток завершується через неправильну конфігурацію

Підпис: ExitCode — маленьке ненульове число (1, 2, 64), журнали показують «missing env var», «invalid config», «failed to parse».

Типові причини: відсутня змінна після ротації секретів, неправильний шлях до файлу конфігурації, баг у шаблоні, синтаксична помилка JSON/YAML.

Виправлення: Валідуйте конфіг під час збірки/деплоя. Додайте режим «configtest» у entrypoint. Fail fast — але хоча б одного разу (обмежте ретраї під час релізу).

2) Поведінка PID 1 і обробка сигналів (класика «працює в мене, не працює в проді»)

У контейнері головний процес — PID 1. PID 1 має спеціальні семантики в Linux: він ігнорує деякі сигнали за замовчуванням і відповідає за reaping zombie-процесів. Якщо ви обгорнули додаток у наївний shell-скрипт, можна отримати дивну поведінку при вимкненні, діти, що ніколи не помирають, або «миттєве завершення», бо скрипт закінчився.

Підпис: Контейнер швидко виходить з кодом 0; журнали показують, що скрипт завершився; немає довго працюючого процесу. Або контейнер не зупиняється коректно і отримує SIGKILL, після чого перезапускається.

Виправлення: Використовуйте exec у entrypoint-скриптах. Розгляньте мінімальний init (наприклад, tini), якщо ви запускаєте підпроцеси.

3) OOM kill та обмеження пам’яті

OOM-kill створює найбільш тісні й жорсткі цикли перезапуску: усе стартує, виділяє багато пам’яті (JVM warmup, імпорт Python, побудова Node, кеш у пам’яті), потім ядро його вбиває. Docker перезапускає. Повторюється.

Підпис: ExitCode 137, OOMKilled=true, журнали ядра показують cgroup OOM.

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

4) Healthcheck, що занадто агресивний (або просто неправильний)

Healthcheck — чудова річ, поки його не пишуть як юніт-тест: крихкий, залежний від таймінгу і впевнений, що сервіс мертвий через одноразовий провал TCP-з’єднання.

Підпис: Сервіс працює, але стає «unhealthy», оркестратор перезапускає або залежні сервіси відмовляються стартувати.

Виправлення: Додайте start_period, налаштуйте interval/retries і зробіть перевірку такою, що відображає реальну готовність для користувача (а не внутрішню досконалість).

5) Зберігання та файлові системи: повний диск, неправильні дозволи, зламані монтування

Проблеми зі зберіганням не завжди кричать. Іноді вони шепочуть: «файлова система тільки для читання», «нема місця на пристрої», «permission denied». Потім додаток виходить. Потім Docker перезапускає. Нескінченні ввічливі страждання.

Підпис: Журнали містять помилки запису; журнали демона показують помилки монтування; диск/inode близькі до 100%.

Виправлення: Виправте монтування, власність (UID/GID), SELinux-лейбли, і ємність. Також: не пишіть журнали в файлову систему контейнера, наче це 2014 рік.

6) Збої залежностей: DNS, TLS, БД та порядок старту

Додатки часто припускають, що залежності доступні миттєво. У розподілених системах це мило і неправильно.

Підпис: Журнали показують connection refused/timeout до БД; ненульовий код виходу; рестарти відбуваються відразу при старті.

Виправлення: Додайте exponential backoff і retry у додатку. Або використовуйте патерн init-контейнера (поза чистим Docker) або скрипт запуску, що чекає з таймаутами. Уникайте нескінченних «wait-for-it» без дедлайну.

7) «Оптимізації», що змінюють таймінги і ламають усе

Коли ви стискаєте час старту або зменшуєте розмір образу, ви змінюєте таймінг і середовище виконання. Це може виявити гонки: healthcheck запускається раніше, залежності ще не готові, файли ще не створені.

Підпис: Флап після очищення образу або зміни базового образу; той самий код — інша поведінка.

Виправлення: Розглядайте зміни базового образу й entrypoint як продакшн-зміни. Тестуйте cold-start. Тестуйте з реалістичними обмеженнями ресурсів.

Анекдот №2: Контейнер «перезапускається для застосування оновлень», що саме він каже перед тим, як перестає робити це.

Три короткі історії з продакшна

Міні-історія 1: Інцидент через неправильне припущення

Команда мала невелике внутрішнє API в Docker Compose на кількох віртуалках. Просте налаштування: контейнер додатку, контейнер Postgres і реверс-проксі. Все працювало місяцями — поки не стався маленький патч ОС і повторне розгортання.

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

Хтось інший запустив docker logs --previous і одразу побачив рядок про неможливість відкрити сертифікат. Припущення було: «сертифікат впечений в образ». Ні — це був bind-mount з хоста, і патч ОС змінив права на директорію, де лежав сертифікат.

Процес API працював від не-root користувача. Він не міг прочитати сертифікат, тому виходив. Docker перезапускав його. Нескінченний цикл.

Виправлення було нудним: правильно виставити власність і права на хості, потім redeploy. Тривале виправлення — ще більш нудне: не припускати, що файли на хості стабільні; керувати ними явно (config management або secret store) і додати перевірку старту, що виводить зрозумілу помилку перед будь-якою роботою.

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

Платформна команда хотіла швидші деплої й менші образи. Вони перенесли кілька сервісів з Debian-based образу на тонший Alpine. Час збірки покращився. Висновки CVE стали «чистішими». Усі відчули, що «зменшили витрати».

За тиждень один сервіс почав флапати після рутинного релізу. Поведінка була не однакова на всіх вузлах. На деяких вузлах він працював годинами; на інших перезапускався кожну хвилину. Політика перезапуску була unless-stopped, тому він намагався знову й знову.

Корінь проблеми виявився у нативній залежності. Сервіс використовував бібліотеку, що поводиться інакше під musl (Alpine), ніж під glibc (Debian). Під навантаженням пам’ять різко зростала, перевищуючи ліміт cgroup. Ядро вбивало його (exit 137), він перезапускався, і цикл повторювався. Через відмінності в розподілі навантаження по вузлах проблема виглядала «рандомною».

Вони відкотили базовий образ для цього сервісу, а потім виконали важку роботу: встановили реалістичні ліміти пам’яті, зібрали правильний нагрузочний тест, що вимірював RSS у steady-state і під час прогріву, і задокументували, які сервіси безпечні для «зменшення» образу.

Урок не в тому, що Alpine поганий. Урок у тому, що оптимізації змінюють фізику. Якщо не вимірюєте — ви просто переставляєте відмови.

Міні-історія 3: Сумна, але правильна практика, що врятувала день

Одна команда, пов’язана з фінансами, запускала контейнеризовану пакетну задачу, що виробляла файли для іншої системи. Нічого яскравого. Вона запускалась раз на годину, писала в змонтований том і виходила. Політика перезапуску була on-failure:3, а не always. Такий вибір виглядав консервативним, можливо навіть боязким.

Одного ранку задача почала відразу падати. Помилка не була в логах додатку; вона була в логах демона Docker: шлях для bind-mount не існував на одному з хостів після реорганізації файлової системи. Контейнер навіть не стартував.

Оскільки політика перезапуску була обмеженою, задача припинилась після трьох спроб, замість того щоб флапати годинами і споживати ресурси. Аларм спрацював як «задача не виконалась», а не «CPU вузла горить». Черговий інженер міг діагностувати без того, щоб система постійно змінювалася під ним.

Вони виправили шлях монтування і додали preflight-перевірку в pipeline розгортання, що перевіряє існування хост-шляхів і права на них. Задача знову стала нудною, а це правильний стан для фінансових процесів.

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

Це розділ, який ви будете бажати мати під час інциденту. Симптоми — це те, що ви бачите. Кореневі причини — що насправді відбувається. Виправлення — найкоротший безпечний шлях до стабільності.

1) «Restarting (0)» вічно

Симптом: Контейнер перезапускається, код виходу показаний як 0.

Корінь: Політика перезапуску always з процесом, що успішно завершується (скрипт виконався; контейнер job не призначений для довготривалої роботи), або супервізор, що некоректно завершується після створення дочірнього процесу.

Виправлення: Використовуйте on-failure для одноразових задач. Переконайтеся, що entrypoint-скрипт використовує exec, щоб реальний сервіс став PID 1.

2) Exit code 137 і «OOMKilled=true»

Симптом: Перезапуски у послідовній точці старту; іноді працює при меншому навантаженні; журнали обриваються.

Корінь: Cgroup memory OOM kill.

Виправлення: Вимірюйте пам’ять; підвищуйте ліміти адекватно; виправляйте витоки; налаштовуйте рантайми (JVM heap, Node memory flags). Підтвердіть через журнали ядра.

3) «permission denied» на монтуваннях

Симптом: Журнали демона показують OCI помилки монтування; або журнали додатку показують невдачі при відкритті файлу.

Корінь: Права на файловій системі хоста, SELinux-лейбли, AppArmor-профілі або rootless Docker-мапінг користувачів.

Виправлення: Виправте власність/права; застосуйте коректний SELinux-контекст; для rootless забезпечте доступ до шляхів користувачу, що запускає dockerd.

4) Healthcheck падає, хоча додаток насправді в порядку

Симптом: Додаток відповідає на одному порту/шляху, але healthcheck маркує його як unhealthy; оркестратор виключає сервіс з ротації.

Корінь: Неправильний порт, неправильний шлях, невідповідність TLS або старт повільніший за час до перевірки.

Виправлення: Виправте команду healthcheck; додайте start_period; перевірте, що health відповідає видимій готовності для користувача.

5) Контейнер ніколи не доходить до логів додатку

Симптом: docker logs порожній; контейнер миттєво завершується; демон показує runtime-помилки.

Корінь: Відсутній entrypoint/бінар, exec format error (не та архітектура), помилка монтування, відсутній бин, недійсний користувач.

Виправлення: Інспектуйте журнали демона; перевірте архітектуру образу; протестуйте docker run --entrypoint з шелом; перевірте наявність шляхів для монтувань.

6) Цикл перезапуску після «жорсткого» закріплення або «затвердження безпеки»

Симптом: Працює в dev; падає в prod після ввімкнення read-only root FS, опускання привілеїв або видалення пакетів.

Корінь: Додаток пише в root FS, потребує CA сертифікатів, тайм-зон або очікує /tmp доступним для запису.

Виправлення: Надати записувані томи для потрібних шляхів; встановити необхідні runtime-дані; задокументувати вимоги до файлової системи та прав.

7) Випадкові перезапуски, корельовані з навантаженням

Симптом: Контейнер стабільний вночі, флапає в пікові години.

Корінь: Піки пам’яті ведуть до OOM, вичерпання дескрипторів файлів, вичерпання потоків/процесів або таймаути до апстриму, що спричиняють крах при старті.

Виправлення: Слідкуйте за використанням ресурсів; встановіть ulimits; додайте backpressure; уникайте краху через тимчасові помилки залежностей.

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

Чек-лист A: «Мій контейнер зараз перезапускається» (10-хвилинний план)

  1. Ідентифікуйте контейнер: docker ps --no-trunc.
  2. Зхапайте попередні журнали: docker logs --previous <name>.
  3. Інспектуйте причину завершення: docker inspect для ExitCode і OOMKilled.
  4. Якщо ExitCode=137 або OOMKilled=true: перевірте dmesg та ліміти пам’яті.
  5. Якщо журнали показують конфіг/змінні середовища: порівняйте очікувані змінні з фактичними у контейнері та конфігурації розгортання.
  6. Якщо монтування/права: перегляньте docker inspect mounts і журнали демона.
  7. Якщо healthcheck: перевірте .State.Health журнали; підтвердіть порт/шлях.
  8. Зупиніть цикл, якщо він шкодить хосту: docker update --restart=no потім docker stop.
  9. Виправте в джерелі (файл Compose/systemd/CI), щоб наступний деплой не повторив цикл.
  10. Запустіть раз, спостерігайте події й журнали, підтвердіть стабільність.

Чек-лист B: «Зробити цикли перезапуску менш болючими» (заходи на етапі проєктування)

  1. Використовуйте обмежені спроби там, де потрібно: on-failure:5 для батч-процесів.
  2. Додайте чіткі перевірки старту: валідуйте потрібні змінні, файли й з’єднання з ясними помилками.
  3. Забезпечте, щоб entrypoint-скрипти використовували exec і виходили ненульовим кодом при фатальних помилках під час старту.
  4. Визначайте healthchecks, що відображають реальну готовність, з періодом на старт.
  5. Ставте реалістичні ліміти ресурсів і моніторьте їх; «без обмежень» — не стратегія, а зізнання.
  6. Переносьте персистентний стан у томи; розглядайте FS контейнера як ефемерний.
  7. Централізуйте логи; не покладайтеся на «docker logs» як єдиний запис під час інциденту.
  8. Документуйте залежності та поведінку при відмовах (що трапиться, якщо БД недоступна при старті?).

Цікаві факти та історичний контекст

  • Факт 1: Рання популярність Docker (близько 2013–2014) була обумовлена пакуванням і дистрибуцією, а не оркестрацією; цикли перезапуску стали помітнішою проблемою, коли люди почали ставитися до контейнерів як до «петів».
  • Факт 2: Стандарт OCI runtime виник, бо екосистема потребувала узгодженої поведінки контейнерів; багато «помилок Docker» насправді — помилки рантайму (runc/containerd), які видно через Docker.
  • Факт 3: Exit code 137 зазвичай означає SIGKILL (128 + 9). У контейнерному середовищі це часто відповідає OOM kill, але може бути й зовнішнє вбивство.
  • Факт 4: Семантика PID 1 старша за контейнери; контейнери лише роблять так, що багато додатків випадково стають PID 1 без відповідної підготовки.
  • Факт 5: Healthchecks були додані до Docker значно пізніше за «docker run»; багато образів досі постачаються без них, а багато команд додають їх без налаштування таймінгів.
  • Факт 6: Драйвери логів (json-file, journald, syslog, fluentd тощо) впливають на те, що може показати «docker logs»; діагностика перезапусків змінюється, якщо логи не зберігаються локально.
  • Факт 7: Overlay-файлові системи (overlay2) змінили продуктивність і семантику зберігання контейнерів порівняно зі старими драйверами; деякі «випадкові» помилки старту раніше були крайовими випадками драйвера зберігання.
  • Факт 8: Політики перезапуску передували сучасним оркестраторам; це локальний механізм надійності, а не повноцінна стратегія планування. Саме тому вони можуть посилювати проблему на одному хості.
  • Факт 9: Поведінка Compose при пересозданні контейнерів (нові ID) проти перезапуску на місці часто вводить в оману, коли очікують, що --previous завжди працюватиме за назвою.

FAQ

1) Який «єдиний журнал», який мені потрібен, коли контейнер перезапускається вічно?

Журнали з попередньої спроби запуску: docker logs --previous <container>. Вони фіксують збій, який ви пропустили, поки дивилися на новий старт.

2) Чому docker logs не показує нічого корисного під час циклу перезапусків?

Тому що ви спостерігаєте не ту мить життєвого циклу. Контейнер може вийти до того, як щось виведе, або корисний рядок був надрукований у попередній спробі. Використовуйте --previous і інспектуйте стан виходу.

3) Docker пересоздає контейнер чи перезапускає той самий?

Політика перезапуску Docker перезапускає той самий контейнер (той самий ID). Деякі інструменти вищого рівня (Compose при оновленнях, Swarm) можуть створити новий контейнер/таск, що змінює спосіб отримання «попередніх» журналів.

4) Що означає код виходу 137 у Docker?

Це зазвичай означає, що процес отримав SIGKILL. У контейнерах це часто відповідає OOM killer. Підтвердіть через docker inspect (OOMKilled) і dmesg.

5) Мій контейнер виходить з кодом 0, але все одно перезапускається. Чому?

Політика перезапуску always перезапустить навіть при успішному завершенні. Це підходить для демонів, але не для одноразових задач. Переключіться на on-failure або переробіть контейнер, щоб він залишався запущеним, якщо це сервіс.

6) Чи може невдалий healthcheck спричиняти перезапуски?

Сам Docker автоматично не перезапускає через unhealthy, але зовнішні системи часто роблять це: залежності в Compose, скрипти, unit-и systemd або контролери балансувальників. У будь-якому разі діагностуйте вивід healthcheck — він зазвичай вказує на реальну проблему готовності.

7) Як зупинити цикл перезапуску, не видаляючи все?

Тимчасово відключіть політику перезапуску: docker update --restart=no <name>, потім зупиніть контейнер. Виправте корінь проблеми, а потім знову встановіть потрібну політику через конфігурацію розгортання.

8) Що робити, якщо не можу використати docker logs, бо логи відправляються кудись ще?

Тоді «єдиним журналом» є еквівалент у вашому логінг-пайплайні, відфільтрований за ID контейнера та часовою міткою навколо збою. Проте docker inspect для кодів виходу і журнали демона лишаються локальною правдою.

9) Як налагоджувати контейнер, що помирає занадто швидко, щоб в нього зайти?

Вимкніть рестарти, запустіть образ з перекриттям entrypoint (шелл) або запустіть debug-контейнер у тих же неймспейсах. Мета — спостерігати FS, змінні середовища та мережу з тієї самої точки зору, що й додаток.

10) Це те саме, що Kubernetes CrashLoopBackOff?

Це ті самі базові симптоми — процес завершується і система пробує знову — але Kubernetes додає backoff, події, probes і управління репліками. Примітиви діагностики (попередні журнали, коди виходу, OOM) залишаються релевантними.

Наступні кроки, які вам варто зробити

Якщо у вас контейнер перезапускається вічно, зробіть це в порядку:

  1. Запустіть docker logs --previous і серйозно його прочитайте.
  2. Запустіть docker inspect для ExitCode і OOMKilled; вирішіть, чи це «крах», чи «вбивство».
  3. Перевірте журнали демона на предмет OCI/помилок монтувань, якщо контейнер взагалі не стартує.
  4. Якщо це OOM: підтвердіть через dmesg, потім виправте ліміти/ємність або профіль пам’яті додатку.
  5. Виправте джерело правди (файл Compose, unit systemd, CI), а не живий контейнер, якщо це не тимчасовий хак для аварійного зупину.

Потім зробіть нудні покращення: обмежені ретраї, налаштовані healthchecks, правильна поведінка PID 1 і preflight-перевірка конфігурації, що провалюється гучно один раз, а не тихо вічно.

← Попередня
Windows Update зависає: виправити без очищення ПК
Наступна →
Proxmox: Резервне копіювання Windows VM без проблем з VSS — Практичний робочий процес

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