Pager не каже «причина невідома». Він каже «сервіс недоступний». А десь контейнер робить ту незручну річ: запускається, вмирає, знову запускається — наче він веде переговори з реальністю.
Цикли перезапуску марнують час, бо люди ганяються за симптомами: «Docker нестабільний», «образ зіпсований», «можливо вузол проклятий». Насправді майже завжди справа в чомусь буденному: код виходу, healthcheck, OOM‑вбивство, таймінг залежностей або політика рестарту, яка робить саме те, що ви їй наказали. Ось як швидко знайти реальну причину, не перетворюючи інцидент на стиль життя.
Швидкий план діагностики (5 хвилин)
Мета не «зібрати дані». Мета: ідентифікувати тригер перезапуску і компонент, що відмовляє, до того як цикл знищить докази.
Ви намагаєтеся відповісти на три питання:
- Хто перезапускає? Політика рестарту Docker, оркестратор (Compose, Swarm), systemd, чи ви самі?
- Чому він виходить? Збій програми, невірна конфігурація, сигнал, OOM‑вбивство, провалений healthcheck, відсутня залежність.
- Що змінилося? Тег образу, змінна оточення, секрет, том, навантаження/пам’ять ядра, DNS, фаєрвол.
Хвилина 1: Ідентифікуйте контейнер і джерело рестарту
- Отримайте
RestartCount,ExitCode,OOMKilled, політику рестарту. - Підтвердіть, чи залучені Compose/Swarm/systemd.
Хвилина 2: Заберіть останні докази відмови (поки лог не прокрутився)
- Перегляньте логи з останнього запуску (
--since/ tail). - Огляньте
State.Errorі відмітки часу.
Хвилина 3: Класифікуйте режим відмови
- Коди виходу 1/2/126/127: проблеми з додатком/конфігом/викликом.
- Код 137 або
OOMKilled=true: тиск пам’яті. - Healthcheck «unhealthy»: програма може бути запущена, але оркестратор її вбиває.
- Миттєві виходи: скрипт entrypoint, відсутній файл, неправильні права/користувач.
Хвилина 4: Перевірте залежності та середовище виконання
- DNS, мережа, порти, змонтовані файли, дозволи, секрети.
- Доступність бекендів (БД, черга, авторизація) і таймаути.
Хвилина 5: Прийміть рішення, а не звіт
Вирішіть, що робити далі: виправити конфіг, додати ресурси, відкатити образ, вимкнути некоректний healthcheck або зафіксувати залежності.
Якщо після п’яти хвилин ви не можете вирішити — вам бракує одного з: коду виходу, доказів OOM, статусу healthcheck або того, хто перезапускає.
Цитата, яка має бути в голові у кожного на виклику: «Надія — не стратегія.» — Gene Kranz.
Що насправді означає «постійно перезапускається»
Цикл перезапуску контейнера — це не поодинокий баг. Це угода між вашим процесом, Docker і тим, хто наглядає за Docker.
Процес контейнера виходить. Хтось помічає. Хтось перезапускає. Це «хтось» може бути сам Docker (політика рестарту), Docker Compose, Swarm, Kubernetes (якщо Docker лише runtime), або навіть systemd, що керує docker run.
Отже перший антипатерн: пильно дивитися в ім’я контейнера, ніби воно має відповіді. Контейнери не перезапускаються самі по собі; наглядачі перезапускають контейнери.
Найкращий крок у налагодженні — ідентифікувати наглядача й прочитати докази, які він лишає.
Типові драйвери рестарту
- Політика рестарту Docker:
no,on-failure,always,unless-stopped. - Compose:
restart:уdocker-compose.yml, плюс проблеми з порядком запуску. - systemd: юніт із
Restart=always, що запускає Docker‑команду. - Зовнішня автоматизація: cron, watchdog‑скрипти, CI/CD‑завдання «переконайся, що працює».
Два види циклів, що виглядають однаково (але різні)
Crash loop: процес швидко помирає через помилку програми/конфігурації/ресурсів.
Kill loop: процес працює, але healthcheck несправний або наглядач вбиває його (OOM, watchdog, політика оркестратора).
Ваше завдання — відокремити ці два випадки. Логи й коди виходу роблять це за хвилини — якщо ви правильно їх витягнете.
Цікаві факти та невелика історія (щоб покращити інтуїцію)
- Політики рестарту Docker з’явилися рано, бо користувачі ставилися до контейнерів як до легких демонів і потребували поведінки ініту без самого ініту всередині контейнера.
- Код виходу 137 зазвичай означає SIGKILL (128 + 9). У контейнерах SIGKILL часто — це OOM‑killer ядра або жорстке убивство наглядачем.
- Healthchecks додали після того, як люди почали доставляти «запущені але мертві» сервіси — процес ще живий, але не приймає трафік. Без healthchecks такі відмови тихо гниють.
- Docker‑логи — не «логи програми»; це те, що процес пише в stdout/stderr, захоплене драйвером логування. Якщо ваша програма пише в файли,
docker logsможе виглядати порожнім, хоч програма кричить у/var/logвсередині контейнера. - Overlay‑файлові системи зробили контейнери практичними, дозволивши copy‑on‑write шари, але вони можуть посилювати IO‑накладні витрати при інтенсивних записах — що призводить до таймаутів, які виглядають як «випадкові перезапуски».
- Цикли рестарту часто маскують проблеми з залежностями: програма виходить, бо не може дістатися БД, але справжня причина — DNS, фаєрвол, TLS‑невідповідність або змінений пароль.
- Compose «depends_on» не означає «готовий» у класичному Compose; воно лише задає порядок старту, а не готовність. Це непорозуміння спалило більше команд, ніж незрозумілі баги ядра.
- OOM‑вбивства можуть статися при «вільній пам’яті» на хості, бо важливі ліміти — це cgroup‑межі та облік memory+swap для контейнера, а не глобальна вільна RAM хоста.
- Поведінка PID 1 має значення: сигнали, зомбі та обробка виходу відрізняються, якщо ви запускаєте shell як PID 1 vs. init‑подібний wrapper, і це змінює вигляд рестартів.
12+ практичних завдань: команди, що означає вивід, і рішення
Це кроки, які можна виконати під тиском. Кожне завдання містить: команду, що каже вивід, і яке рішення вона дозволяє прийняти.
Виконуйте їх по черзі, поки не знайдете курця. І так — більшість цього можна зробити без «exec» у контейнер, який вмирає кожні чотири секунди.
Завдання 1: Підтвердіть цикл перезапуску і отримайте ID контейнера
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.RunningFor}}'
NAMES IMAGE STATUS RUNNING FOR
api myco/api:1.24.7 Restarting (1) 8 seconds ago 2 minutes
redis redis:7 Up 3 hours 3 hours
Значення: Restarting (1) вказує, що Docker бачить контейнер, який повторно виходить, і застосовує політику рестарту.
Число в дужках — останній код виходу (не завжди; це те, що Docker спостерігав останнім).
Рішення: Ідентифікуйте ім’я сервісу (api) і негайно переходьте до інспекції стану й деталей виходу. Не починайте з теорій про мережу.
Завдання 2: Перевірте політику рестарту, код виходу, OOM та відмітки часу
cr0x@server:~$ docker inspect api --format '{{json .State}}'
{"Status":"restarting","Running":true,"Paused":false,"Restarting":true,"OOMKilled":false,"Dead":false,"Pid":24711,"ExitCode":1,"Error":"","StartedAt":"2026-01-02T09:14:44.129885633Z","FinishedAt":"2026-01-02T09:14:51.402183576Z","Health":null}
cr0x@server:~$ docker inspect api --format 'RestartPolicy={{.HostConfig.RestartPolicy.Name}} MaxRetry={{.HostConfig.RestartPolicy.MaximumRetryCount}}'
RestartPolicy=always MaxRetry=0
Значення: ExitCode=1 — помилка на рівні програми. OOMKilled=false зменшує ймовірність OOM‑вбивства (не абсолютне, але сильний сигнал).
Політика рестарту always означає, що Docker буде пробувати нескінченно. Це добре — поки не стає погано.
Рішення: Зосередьтеся на помилках старту/конфігурації програми і діставайте логи за останній запуск. Якби було OOMKilled=true або код 137, варто було б переключитися на пам’ять.
Завдання 3: Отримайте останні логи з попередньої спроби (не з початку історії)
cr0x@server:~$ docker logs --timestamps --tail 200 api
2026-01-02T09:14:49.903214817Z level=error msg="config parse failed" err="missing ENV DATABASE_URL"
2026-01-02T09:14:49.903955113Z level=error msg="fatal: cannot start without database"
Значення: Контейнер не «таємниче перезапускається». Він детерміновано падає: відсутня змінна оточення DATABASE_URL.
Рішення: Виправити конфіг і задеплоїти заново. Зупинитися. Не додавати пам’ять. Не «перебирати образ». Не звинувачувати Docker.
Завдання 4: Підтвердіть змінні оточення і те, що контейнер думає, що має
cr0x@server:~$ docker inspect api --format '{{range .Config.Env}}{{println .}}{{end}}' | sed -n '1,12p'
APP_ENV=prod
LOG_LEVEL=info
PORT=8080
Значення: Середовище не містить того, що вказують логи. Це узгоджено. Добре.
Рішення: Визначте, звідки мають надходити змінні: файл Compose, --env-file, інжекція секретів або інструменти платформи. Виправляйте в джерелі, а не одноразово через docker exec.
Завдання 5: Якщо це Compose, перевірте згенеровану конфігурацію
cr0x@server:~$ docker compose config | sed -n '/services:/,$p' | sed -n '1,120p'
services:
api:
environment:
APP_ENV: prod
LOG_LEVEL: info
PORT: "8080"
image: myco/api:1.24.7
restart: always
Значення: У конфігурації Compose немає DATABASE_URL. Можливо файл env не був підвантажений або змінна перейменована.
Рішення: Виправити docker-compose.yml або шлях до env‑файлу. Потім задеплоїти з чистим recreate, щоб стара конфігурація не лишалася.
Завдання 6: Шукайте рестарти, спричинені healthcheck (це хитріше ніж здається)
cr0x@server:~$ docker inspect api --format '{{json .State.Health}}'
{"Status":"unhealthy","FailingStreak":5,"Log":[{"Start":"2026-01-02T09:20:10.001712312Z","End":"2026-01-02T09:20:10.045221991Z","ExitCode":7,"Output":"curl: (7) Failed to connect to localhost port 8080: Connection refused\n"}]}
Значення: Процес може бути запущений, але healthcheck не дістається сервісу. Код виходу 7 від curl — «не вдалося підключитися».
Деякі середовища (особливо з Compose‑обгортками або зовнішніми наглядачами) перезапускають нездорові контейнери.
Рішення: Вирішіть, чи некоректний healthcheck (перевіряє неправильний порт/інтерфейс), чи надто агресивний (інтервал/таймаут), або реально фіксує мертвий додаток. Виправте або healthcheck, або прив’язку сервісу.
Завдання 7: Визначте, чи причетний OOM‑killer ядра
cr0x@server:~$ docker inspect api --format 'ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}} Error={{.State.Error}}'
ExitCode=137 OOMKilled=true Error=
cr0x@server:~$ dmesg -T | tail -n 20
[Thu Jan 2 09:25:13 2026] oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=docker.service,mems_allowed=0,oom_memcg=/docker/3b2e...,task_memcg=/docker/3b2e...,task=myco-api,pid=31244,uid=1000
[Thu Jan 2 09:25:13 2026] Killed process 31244 (myco-api) total-vm:2147488kB, anon-rss:612344kB, file-rss:1420kB, shmem-rss:0kB
Значення: Тепер це інша категорія проблем. Контейнер не «впав» сам — його вбили.
OOMKilled=true разом із dmesg підтверджує, що ядро убило процес через тиск пам’яті в cgroup.
Рішення: Збільшити ліміт пам’яті, зменшити використання пам’яті, або виправити витік/регресію. Також перевірте конкуренцію за пам’ять на вузлі та «гучних сусідів».
Завдання 8: Перевірте ліміти пам’яті контейнера і поточне використання
cr0x@server:~$ docker inspect api --format 'Memory={{.HostConfig.Memory}} MemorySwap={{.HostConfig.MemorySwap}}'
Memory=536870912 MemorySwap=536870912
cr0x@server:~$ docker stats --no-stream --format 'table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.CPUPerc}}'
NAME MEM USAGE / LIMIT MEM % CPU %
api 510MiB / 512MiB 99.6% 140.3%
redis 58MiB / 7.7GiB 0.7% 0.4%
Значення: Ліміт 512MiB зі swap, рівним пам’яті, — це тісно; фактично немає простору для дихання. CPU 140% вказує на інтенсивну роботу (кілька потоків).
Рішення: Якщо цей ліміт був навмисним, тонкуйте додаток (розміри heap, кеші) і перевірте пам’яттєвий профіль. Якщо це помилка — підвищте ліміт і рухайтесь далі.
Завдання 9: Визначте проблеми швидкого виходу: неправильний entrypoint, відсутній бінар, права
cr0x@server:~$ docker inspect api --format 'Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}} User={{json .Config.User}}'
Entrypoint=["/docker-entrypoint.sh"] Cmd=["/app/server"] User="10001"
cr0x@server:~$ docker logs --tail 50 api
/docker-entrypoint.sh: line 8: /app/server: Permission denied
Значення: Бінар існує, але не виконується для вказаного користувача, або файлову систему змонтовано з noexec, або під час збірки образу втрачено біт виконання.
Рішення: Виправте права в образі (chmod +x під час збірки), або запускайте від користувача, який може виконувати, або приберіть noexec для тієї точки монтування. Не «виправляйте» це через chmod в робочому контейнері — не переживе пересборки.
Завдання 10: Перевірте монтування і чи том не ховає ваші файли
cr0x@server:~$ docker inspect api --format '{{range .Mounts}}{{println .Destination "->" .Source "type=" .Type}}{{end}}'
/app -> /var/lib/docker/volumes/api_app/_data type= volume
/config -> /etc/myco/api type= bind
Значення: Монтування тому на /app може затемнити бінар, що був упакований в образ. Якщо том пустий або застарілий, контейнер завантажиться в порожню директорію і помре.
Рішення: Не монтуйте поверх шляху додатку, якщо ви цього не маєте на увазі. Перенесіть записувані дані в /data або подібне. Для гарячого перезавантаження в деві — залишайте це тільки в деві.
Завдання 11: Перевірте потік подій, щоб побачити, хто вбиває/перезапускає
cr0x@server:~$ docker events --since 10m --filter container=api | tail -n 20
2026-01-02T09:31:10.002345678Z container die 3b2e... (exitCode=137, image=myco/api:1.24.7, name=api)
2026-01-02T09:31:10.120456789Z container start 3b2e... (image=myco/api:1.24.7, name=api)
Значення: Події показують явні цикли die/start і коди виходу. Якщо ви бачите події kill з атрибуцією користувача/демона (іноді видно в audit‑логах), це ваш зовнішній наглядач або оператор.
Рішення: Якщо рестарти керуються політикою — виправляйте причину виходу. Якщо рестарти ручні чи автоматизовані — знайдіть автоматизацію і зупиніть її, щоб вона не боролася з вами.
Завдання 12: Перевірте systemd, якщо Docker керується зовні
cr0x@server:~$ systemctl status myco-api.service --no-pager
● myco-api.service - MyCo API container
Loaded: loaded (/etc/systemd/system/myco-api.service; enabled; vendor preset: enabled)
Active: activating (auto-restart) (Result: exit-code) since Thu 2026-01-02 09:33:12 UTC; 4s ago
Process: 32511 ExecStart=/usr/bin/docker run --rm --name api myco/api:1.24.7 (code=exited, status=1/FAILURE)
Main PID: 32511 (code=exited, status=1/FAILURE)
Значення: systemd перезапускає процес запуску контейнера, а не сам Docker. Ваш цикл рестарту може навіть не бути політикою Docker.
Рішення: Виправте unit‑файл (середовище, монтування, backoff рестарту). Також вирішіть, чи варто керувати цим через Compose, щоб уникнути «перетягування каната» між двома наглядачами.
Завдання 13: Відтворіть без рестарту, щоб зберегти докази
cr0x@server:~$ docker inspect api --format 'RestartPolicy={{.HostConfig.RestartPolicy.Name}}'
RestartPolicy=always
cr0x@server:~$ docker update --restart=no api
api
cr0x@server:~$ docker start -a api
2026-01-02T09:35:01.110Z level=error msg="fatal: cannot open /config/app.yaml" err="permission denied"
Значення: Вимкнення рестарту зупиняє цикл і дає змогу приєднатися до помилки. Це часто найшвидший спосіб перестати втрачати логи.
Рішення: Використовуйте це під час налагодження. Потім відновіть потрібну політику рестарту після виправлення. Не лишайте продакшн‑сервіси з вимкненим рестартом, якщо вам не подобаються нічні сюрпризи.
Завдання 14: Перевірте права та відображення користувачів на bind‑mount
cr0x@server:~$ ls -l /etc/myco/api/app.yaml
-rw------- 1 root root 2180 Jan 2 09:00 /etc/myco/api/app.yaml
cr0x@server:~$ docker inspect api --format 'User={{.Config.User}}'
10001
Значення: Контейнер запускається як UID 10001, але файл на хості доступний лише root. Це пряма, буденна причина відмови.
Рішення: Виправте власника/права на хості, або запускайте контейнер від користувача, що має доступ, або використайте механізм секретів/конфігів, призначений для цього.
Завдання 15: Швидко перевірте DNS/мережеву залежність з debug‑контейнера в тій же мережі
cr0x@server:~$ docker network ls
NETWORK ID NAME DRIVER SCOPE
6c0f1b1e2c0a myco_default bridge local
cr0x@server:~$ docker run --rm --network myco_default busybox:1.36 nslookup postgres
Server: 127.0.0.11
Address 1: 127.0.0.11
Name: postgres
Address 1: 172.19.0.3 postgres.myco_default
Значення: DNS всередині Docker‑мережі резолвить postgres. Якщо ваш додаток повідомляє «host not found», проблема може бути в конфігурації додатку або він на іншій мережі.
Рішення: Якщо DNS тут падає — виправте мережу або ім’я сервісу. Якщо DNS працює — переключайтесь на облікові дані, TLS, фаєрвол або таймінг готовності.
Короткий жарт #1: Контейнер у циклі рестарту — це спосіб DevOps навчити терпіння, повторно й наполегливо.
Коди виходу, які варто запам’ятати
Коди виходу — найближче до зізнання, яке ви отримаєте. Docker показує код виходу, але вам треба його інтерпретувати з урахуванням Unix‑конвенцій і реалій контейнера.
Корисні
- 0: чистий вихід. Якщо він все одно рестартується — хтось це наказав.
- 1: загальна помилка. Дивіться логи; зазвичай конфіг або виняток у додатку.
- 2: неправильне використання shell‑вбудованих/CLI помилки; частіше неправильні флаги або помилки в entrypoint‑скрипті.
- 125: Docker не зміг запустити контейнер (помилка демона, некоректні опції). Це не ваша програма.
- 126: команда викликана не може виконатись (права, неправильна архітектура, mount з noexec).
- 127: команда не знайдена (помилковий шлях у ENTRYPOINT/CMD, відсутній shell, неправильний PATH).
- 128 + N: процес вмер від сигналу N. Часті: 137 (SIGKILL=9), 143 (SIGTERM=15).
- 137: SIGKILL. Часто OOM‑killer, інколи навмисне вбивство watchdog‑ом.
- 139: SIGSEGV. Нативний crash; може бути проблема libc, бінаря або корупція пам’яті.
Як коди виходу поєднуються з політиками рестарту
Політика on-failure спрацьовує на ненульових кодах виходу. Отже вона не перезапустить при виході 0.
Політика always не звертає уваги; вона перезапускає незалежно від коду, що може приховати контейнер, який спеціально завершує роботу після виконання завдання.
Якщо ви запускаєте job‑подібний контейнер (міграції, cron, batch), always зазвичай невірно. Якщо це сервіс — always підходить, доки ви не задеплоїте щось, що миттєво завершується і ви втрачаєте логи в циклі.
Healthchecks: коли «healthy» стає детектором брехні
Healthchecks — хороша річ. Погані healthchecks — генератори хаосу.
Їх часто неправильно розуміють: вбудований healthcheck Docker сам по собі не перезапускає контейнер автоматично. Але багато наглядачів і схем деплойменту трактують «unhealthy» як «вбити і перезапустити».
Як healthchecks відмовляють у продакшені
- Неправильний інтерфейс/порт: додаток прив’язаний до
0.0.0.0, а healthcheck таргетуєlocalhostневірно — або навпаки. - Час старту: healthcheck стартує до готовності додатку, викликаючи невдалу послідовність і рестарти.
- Залежності: healthcheck викликає зовнішні сервіси. Коли вони недоступні, ваш контейнер помирає, хоча міг би обслуговувати частковий трафік.
- Скачки ресурсів: healthcheck надто частий; на навантаженому вузлі він доводить сервіс до стану падіння.
- Використання curl в мінімальних образах: команда healthcheck не знайдена дає код 127, що сприймається як «додаток мертвий», коли це просто «curl не встановлений».
Що робити
Healthcheck має тестувати здатність вашого сервісу обслуговувати, а не всю Всесвіт.
Якщо БД впала, цілком валідно, що додаток звітує unhealthy — якщо без БД він не може працювати. Але не включайте всі зовнішні залежності в ендпойнт здоров’я, якщо ви не впевнені, що рестарт допомагає.
Налаштуйте start_period (якщо доступно), інтервали і таймаути. І ще важливіше: тримайте healthchecks детермінованими та «дешевими».
Пастки зберігання та продуктивності, які виглядають як краші
Як людина, що працює зі зберіганням, скажу прямо: багато інцидентів «контейнер постійно перезапускається» насправді — «IO уповільнився, сталися таймаути, процес вийшов».
Docker не цікавить, чому процес вийшов. Він лише бачить вихід. Ваш додаток може вийти через провал міграції БД, таймаут блокувань або «диск заповнений».
Диск заповнений: класика, що ніколи не вмирає
Контейнери пишуть логи, тимчасові файли, іноді файли баз даних (помилково), і шари шарів. Якщо Docker root filesystem заповнюється, контейнери починають падати у приємні способи:
write() помилки, пошкоджені тимчасові файли, БД відмовляються стартувати, або ваш додаток крашиться, бо не може записати PID файл.
cr0x@server:~$ df -h /var/lib/docker
Filesystem Size Used Avail Use% Mounted on
/dev/nvme0n1p4 80G 79G 320M 100% /var/lib/docker
Значення: У вас закінчився простір. Очікуйте випадкових відмов.
Рішення: Звільніть місце (обережно очистіть images/volumes), перенесіть Docker root на більший диск, або припиніть писати великі файли в шари контейнера. Потім виправте політику логування/ретеншн, щоб це не повторилось.
Підсилення записів у Overlay і «перезапускається тільки під навантаженням»
Overlay2 підходить для більшості задач, але якщо робоче навантаження інтенсивно записує в шар контейнера (не в томи), продуктивність може впасти.
Коли латентність зростає, таймаути каскадують: додаток втрачає готовність, healthcheck падає, оркестратор вбиває, відбуваються рестарти.
Практична порада: записувані дані — у томи. Логи — у stdout/stderr (а потім збираються нормальним драйвером логування), не в файл у шарі образу. Бази даних не повинні писати в overlay, якщо вам не до вподоби вчитися про fsync‑латентність о 3 ранку.
Права та невідповідність UID на bind‑mount
Безпекова практика — «запуск від не‑root». Чудово. Але тоді монтують файли хоста, що належать root, і дивуються, чому йде рестарт.
Це не Docker злий. Це Linux такий.
Годинник і TLS‑помилки: неочевидна залежність
Якщо час хоста здригається, TLS може не працювати. Додатки, що трактують «не вдалося встановити TLS» як фатальну помилку, можуть миттєво виходити. Це виглядає як цикл рестарту з кодом 1.
Якщо ви бачите раптові масові рестарти сервісів, що використовують TLS, перевірте NTP/chrony і терміни дійсності сертифікатів.
Три корпоративні історії з реального життя
1) Інцидент від неправильної припущення: «depends_on означає готовність»
Середня компанія запускала стек Compose: API, worker, Postgres, Redis. Він був стабільним місяцями, поки під час планового перезавантаження хоста.
Після ребута контейнер API почав флапати. Перезапуск кожні кілька секунд. Інженери звинуватили «поганий образ», бо деплой відбувся день тому.
Неправильне припущення було тонким і поширеним: вони вірили, що depends_on означає, що Postgres готовий приймати з’єднання. Насправді Compose запускав Postgres першим, але Postgres ще потребував часу на відновлення після крашу та ініціалізацію.
API намагався виконати міграції при старті, не зміг підключитися і вийшов з кодом 1. Docker добросовісно перезапускав його. Знову і знову.
Логи були, але поховані — бо цикл був швидким, а вивід логів змішувався з іншим. Команда спочатку ганялася за мережевими проблемами: «DNS Docker зламався після ребута?» Ні. Вони запустили debug‑контейнер і перевірили DNS та підключення — все було гаразд.
Виправлення було нудним і правильним: додати повторні спроби з експоненційним backoff при підключенні до БД, і винести міграції в однорозовий job з чітким виводом помилок.
Також додали healthcheck для Postgres і змусили API чекати готовності (або принаймні не виходити при відсутності готовності).
Інцидент завершився не героїчними діями, а визнанням: порядок оркестрації ≠ готовність, і надійність приходить із проєктуванням стартапу, який витримує реальний світ.
2) Оптимізація, яка зіграла злий жарт: «зменшимо ліміти пам’яті, щоб зекономити»
Інша організація хотіла зменшити витрати і «стиснути використання ресурсів». Вони знизили ліміти пам’яті для кількох сервісів.
В тестовому середовищі це виглядало нормально — трафік низький і кеші холодні. Продакшн — не тестове середовище. Продакшн ніколи ним не буває.
За день ключовий API почав періодично рестартитись. Не постійно — достатньо, щоб моніторинг став шумним і клієнти незадоволені.
Код виходу був 137. OOM‑вбивства. Сервіс використовував керований runtime з heap, що адаптується під тиск, плюс сплески JSON‑запитів, які викликали тимчасові алокації.
Інженери спочатку тонкували GC runtime і обмежували heap. Це допомогло, але вони пропустили вторинний ефект: нова «оптимізація» в тому ж наборі змін підвищила рівень стиснення відповідей, щоб зекономити пропускну здатність.
CPU піднявся, латентність зросла, і коли запити накопичилися, тиск пам’яті збільшився. OOM‑вбивства почастішали.
Справжнє виправлення — відкотити зміну стиснення для цього endpoint і відновити реалістичний ліміт пам’яті з запасом. Потім профілювали алокації під продакшн‑навантаженням і зробили таргетовані оптимізації.
Урок закріпився: «ефективні» ліміти, що викликають OOM, неефективні; вони платять податок у вигляді інцидентів.
3) Нудна, але правильна практика, що врятувала день: зупинити цикл, зберегти докази
Фінансова компанія мала сервіс у контейнері, який почав рестартитись після ротації сертифікатів.
У їхньому runbook був простий рядок: «Якщо контейнер флапає — вимкнути рестарт і запустити прикріпленим раз, щоб зловити фатальну помилку».
Це не було гламурно. Це не виглядало як чаклунство. Це працювало.
Вони виконали docker update --restart=no і потім docker start -a. Додаток одразу вивів чітку TLS‑помилку: не може прочитати новий приватний ключ.
Ключ було розгорнуто з обмежувальними правами на bind‑mount, читабельними лише для root, тоді як контейнер запускався під не‑root UID.
Без зупинки циклу логи були б уривчастими і перезаписи від повторних рестартів зробили б повідомлення непомітним. Зупинивши цикл, повідомлення про помилку було неможливо пропустити.
Вони виправили власника файлу, перезапустили сервіс і продовжили життя.
Наступне покращення було ще більш нудним: вони перейшли до розгортання сертифікатів через механізм секретів з коректними правами за замовчуванням і додали перевірку на старті, яка повідомляє чітку помилку перед тим, як почати обробляти трафік.
Типові помилки: симптом → корінь → виправлення
Цей розділ — каталог «я вже бачив цей фільм». Якщо ви на виклику, перегляньте симптоми, виберіть ймовірну причину і перевірте її одним із завдань вище.
1) Перезапускається кожні 2–10 секунд, код виходу 1
- Симптом:
Restarting (1), логи показують помилки парсингу конфігів або відсутні env. - Корінь: відсутня змінна оточення, неправильний флаг, не примонтовані секрети, помилка парсингу конфігу.
- Виправлення: виправити інжекцію env у Compose; перевірити
docker compose config; задеплоїти з recreate.
2) Перезапускається з кодом 127 або «command not found»
- Симптом: логи:
exec: "foo": executable file not found in $PATH. - Корінь: неправильний
CMD/ENTRYPOINT, відсутній бінар, використання Alpine без bash, а entrypoint потребує/bin/bash. - Виправлення: виправити Dockerfile ENTRYPOINT; віддавати перевагу exec‑формі; забезпечити наявність потрібного shell або прибрати залежність від нього.
3) Перезапускається з кодом 126 або «permission denied»
- Симптом: бінар існує, але не виконується, або скрипт entrypoint не виконуваний.
- Корінь: неправильний режим файлу, bind‑mount з
noexec, невідповідність власника при запуску не‑root. - Виправлення: поставити біт виконання під час збірки; змінити опції монтування; вирівняти UID/GID або права.
4) Код виходу 137, випадково під навантаженням
- Симптом:
OOMKilled=trueв inspect; dmesg показує oom‑kill. - Корінь: ліміт пам’яті занадто малий; витік пам’яті; сплеск навантаження; недостатній swap.
- Виправлення: підняти ліміт, тонкувати heap/caches, дослідити профіль пам’яті; зменшити concurrency; додати backpressure.
5) «Up», але постійно перемикається healthy/unhealthy і потім рестартить
- Симптом: healthcheck втрачає ланцюжок; рестарти, якщо оркестратор реагує на unhealthy.
- Корінь: агресивний healthcheck, неправильний endpoint, перевіряє зовнішні залежності, додаток слухає на іншому інтерфейсі.
- Виправлення: зробити healthcheck дешевим і коректним; підлаштувати інтервал/таймаут/start_period; переконатися, що додаток слухає там, де очікують.
6) Працює на одному хості, на іншому флапає
- Симптом: той самий образ, різна поведінка.
- Корінь: відмінності в ядрі/cgroup, диск заповнений, різні дозволи монтувань, DNS‑конфіг, несумісна архітектура CPU.
- Виправлення: порівняти
docker info, простір на диску хоста, опції монтування; перевірити архітектуру образу; стандартизувати runtime.
7) Контейнер «виходить успішно» (код 0), але постійно рестартується
- Симптом: код виходу 0; політика рестарту
always. - Корінь: ви запускаєте job (міграції, init, CLI) з політикою сервісу рестарту.
- Виправлення: використати
restart: "no"абоon-failure; відокремити job від довгоживучого сервісу.
8) Після «дрібної зміни» все починає флапати
- Симптом: кілька контейнерів рестартять приблизно одночасно.
- Корінь: спільна залежність: збій DNS, ротація сертифікатів, дрейф часу, обмеження з реєстром, заповнений диск, тиск пам’яті хоста.
- Виправлення: перевірити сигнали хоста (диск, dmesg, синхронізація часу); перевірити дозволи секретів/сертифікатів; відкотити спільну зміну.
Короткий жарт #2: Якщо ваш healthcheck залежить від п’яти інших сервісів, це вже не healthcheck — це груповий проєкт.
Контрольні списки / покроковий план
Чекліст A: Зупиніть кровотечу (безпечні дії в продакшн)
- Підтвердіть масштаб: це один контейнер чи багато? Якщо багато — підозрюйте проблеми на рівні хоста (диск, пам’ять, DNS, час).
- Захопіть докази: збережіть
docker inspectState, останні 200 рядків логів іdocker eventsза 10 хвилин. - Стабілізуйте: якщо цикл надто швидкий, тимчасово вимкніть рестарт (
docker update --restart=no), щоб зберегти логи і зменшити метушню. - Виберіть дію: відкатити тег образу, виправити env/secrets, підвищити пам’ять або скорегувати healthcheck.
- Чітко повідомте: «Exit code 137, OOM kill підтверджено в dmesg. Піднімаємо ліміт і відкачуємо зміну пам’яті.» Не «Docker якийсь дивний».
Чекліст B: Знайдіть тригер чисто і відтворювано
- Ідентифікуйте політику рестарту і наглядача (Docker vs Compose vs systemd).
- Прочитайте код виходу і прапорець OOM.
- Прочитайте логи з останньої спроби (
--tail, з відмітками часу). - Перевірте статус healthcheck‑логів, якщо healthchecks налаштовані.
- Перевірте монтування і права (особливо при запуску не‑root).
- Перевірте підключення залежностей з того ж Docker‑мережевого простору.
- Перевірте простір на хості і логи OOM хоста.
- Запустіть контейнер прикріпленим один раз з вимкненим рестартом, щоб відтворити чисто.
Чекліст C: Запобігти повторенню (те, що пропускають)
- Зробіть старт стійким: повтори з backoff; не виходити одразу при транзиторних помилках.
- Відокремте однорозові jobs: міграції і зміни схеми повинні бути явними job, а не сховані у старті сервісу.
- Підійміть healthchecks: дешеві, детерміновані, не залежні від усього світу.
- Встановіть здраві ліміти: запас пам’яті, адекватні CPU‑обмеження і уникайте оптимізму при виборі лімітів.
- Логи в stdout/stderr: тримайте логи доступними і централізованими.
- Документуйте інваріанти: потрібні env, потрібні монтування, потрібні права, очікувана поведінка при виході.
Чого уникати під час налагодження циклу рестарту
- Не перебирайте образи, поки не зможете вказати код виходу і останній фатальний рядок логу.
- Не exec‑тесь у контейнер одразу; він може вмерти до того, як ви щось дізнаєтесь. Почніть з inspect/logs/events.
- Не ставте «restart: always» скрізь як пластир. Воно ховає job‑контейнери і може посилювати хвилі відмов.
- Не звинувачуйте Docker, поки не перевірили диск і OOM. Docker здебільшого лише репортує, що зробив Linux.
Питання та відповіді (FAQ)
1) Чому docker ps показує «Restarting (1)»?
Це означає, що контейнер виходить і Docker застосовує політику рестарту. Число — останній спостережуваний код виходу.
Підтвердіть через docker inspect і перегляньте .State.ExitCode з відмітками часу.
2) Як зрозуміти, хто саме рестарує: Docker чи хтось інший?
Перевірте політику рестарту в docker inspect (.HostConfig.RestartPolicy), потім шукайте зовнішніх наглядачів:
systemctl status для unit‑файлів, і docker events для шаблонів start/stop. Compose також має власну поведінку життєвого циклу.
3) Який найшвидший спосіб зловити реальну помилку?
Тимчасово вимкніть рестарт (docker update --restart=no) і запустіть прикріпленим один раз (docker start -a).
Це зупиняє метушню і виводить фатальний рядок чітко.
4) Код виходу 137: чи це завжди OOM?
Ні. Це означає SIGKILL. OOM — найпоширеніша причина в контейнерах, але наглядач також може віддати SIGKILL.
Підтвердіть через docker inspect (OOMKilled=true) і логи хоста (dmesg).
5) Чому docker logs порожні, хоча програма падає?
Бо Docker лише захоплює stdout/stderr. Якщо програма пише в файли всередині контейнера, docker logs може бути порожнім.
Перекваліфікуйте додаток на логування в stdout/stderr або інспектуйте файли (краще через том, а не шар контейнера).
6) Контейнер «unhealthy», але процес працює. Чому рестарт?
Статус здоров’я Docker сам по собі не перезапускає контейнер, але багато схем деплойменту трактують unhealthy як «замінити».
Виправте endpoint healthcheck, таймінги або чутливість, або налаштуйте поведінку наглядача.
7) Чому працює вручну, але не під Compose?
Compose змінює мережі, інжекцію оточення, монтування томів і іноді робочу директорію. Порівняйте:
docker compose config vs docker inspect для запущеного контейнера. Різниці в монтуваннях і env — звичні винуватці.
8) Як налагоджувати контейнер, що виходить занадто швидко для exec?
Використовуйте docker logs і docker inspect спочатку. Якщо потрібно — вимкніть рестарт і запустіть прикріпленим один раз.
Ви також можете тимчасово перевизначити entrypoint, щоб потрапити в shell і оглянути файлову систему, але робіть це як контрольований експеримент, а не як фікс.
9) Чи можуть проблеми з диском реально викликати цикли рестарту?
Абсолютно. Диск заповнений, повільний IO або проблеми з дозволами на томах можуть спричинити відмови стартових перевірок, краші або таймаути.
Перевірте df -h для Docker root, інспектуйте монтування і шукайте «no space left on device» в логах.
10) На що ставити алерт, щоб вловити це раніше?
Алертуйте про зростання лічильників рестартів, фліпи статусу health, події OOM kill, заповнення диска Docker root і підвищений рівень виходів контейнерів.
Рестарти самі по собі не погані; небажані рестарти — так. Задайте базову лінію нормальності.
Висновок: кроки, щоб запобігти повторенню
Цикл перезапуску контейнера виглядає хаотично, але зазвичай він детермінований. Перестаньте гадати.
За п’ять хвилин ви можете знати: хто рестартить, який код виходу повертається, чи було OOM‑вбивство, і який був останній фатальний рядок логу.
Далі виправлення зазвичай буденні: поправити env/secrets, права, поведінку healthcheck або дати процесу достатньо пам’яті, щоб вижити.
Зробіть наступне:
- Уніфікуйте runbook: inspect → logs → events → сигнали хоста (disk/OOM) → відтворення прикріпленим один раз.
- Зробіть старт стійким: повтори з backoff, таймаути і чіткі фатальні повідомлення.
- Відокремте jobs від сервісів; не запускайте міграції як побічний ефект старту сервісу, якщо ви цього не планували.
- Перегляньте політики рестарту:
alwaysдля сервісів,on-failureдля job‑ів, іnoдля однорозових задач. - Перемістіть записувані дані в томи і тримайте логи доступними через stdout/stderr.
Вам не потрібні героїчні дії. Потрібні швидкі докази. Потім — дисципліна, щоб виправити те, що справді зламалося.