Skip to main content

Як налагоджувати (debug) проблеми у контейнері?

Debugging Docker container це flowchart: починай з найзагальнішого сигналу (логи, exit code), звужуй до специфічного (exec всередину, entrypoint override), бери спеціалізовані tools, коли більш нічого не допомагає. Правильний порядок економить години.

Теорія

TL;DR

Debugging-flowchart, у порядку:

  1. docker ps -a — який стан container?
  2. docker logs --tail 200 <name> — що друкував?
  3. docker inspect <name> — exit code, OOM flag, error message, mount, мережі.
  4. docker exec -it <name> sh — якщо running, подивися всередині live.
  5. docker run -it --entrypoint sh <image> — якщо крашиться надто швидко для attach.
  6. Override зламаного: --entrypoint, --user root, --cap-add SYS_PTRACE для strace.
  7. Спеціалізовані tools: dive для шарів, tcpdump для мережі, docker stats для ресурсів.

Крок 1: стан

bash
$ docker ps -a --filter name=api CONTAINER ID IMAGE STATUS NAMES a3f9d2b8c1e4 myapp Exited (1) 3 seconds ago api

Три факти в одному рядку: крашнувся, exit 1, 3 секунди тому. Більшість failure-шляхів виявляють себе на цьому етапі.

Крок 2: логи

bash
# Останній вивід docker logs --tail 200 api # Live-tail (для поточних проблем) docker logs -f --tail 50 api # Time-обмежено docker logs --since 10m api # З timestamp docker logs -t --tail 50 api

Важливо: docker logs читає stdout/stderr PID 1. Якщо застосунок логує у файл всередині container, тут буде порожньо. Фікс: змусь застосунок логувати у stdout (12-factor норма).

Для verbose-output capture: docker logs api > app.log 2>&1.

Крок 3: inspect

Повний JSON стану container. Через --format витягуєш конкретні поля:

bash
# Status-квартет, зазвичай відповідає на «що сталося?» docker inspect api --format \ '{{.State.Status}} (exit={{.State.ExitCode}}) OOM={{.State.OOMKilled}} Err={{.State.Error}}' # exited (137) OOM=true Err= ← OOM killer # exited (1) OOM=false Err= ← app error code 1 # exited (139) OOM=false Err= ← segfault
bash
# Що змонтовано? docker inspect api --format '{{range .Mounts}}{{.Type}}: {{.Source}} -> {{.Destination}}\n{{end}}' # Яка мережа? docker inspect api --format '{{range $k, $v := .NetworkSettings.Networks}}{{$k}}: {{$v.IPAddress}}\n{{end}}' # Health-check лог (останні 5 спроб) docker inspect api --format '{{json .State.Health}}'

Крок 4: exec всередину (якщо running)

bash
# Drop у shell docker exec -it api sh # або bash, якщо доступний docker exec -it api bash # Перевірити, що файли існують docker exec api ls -la /app # Перевірити env docker exec api env | grep DATABASE # Перевірити connectivity до сусіда docker exec api wget -O- http://db:5432 # Що робить процес docker exec api ps aux

Крок 5: entrypoint-override (якщо крашиться надто швидко)

Якщо container виходить до того, як ти можеш exec всередину:

bash
# Скіпни реальний застосунок, drop у shell docker run -it --rm --entrypoint sh myimage # Те саме але з оригінальними env-змінними + volume docker run -it --rm \ --entrypoint sh \ -e DATABASE_URL=... \ -v ./data:/data \ myimage # Або запусти оригінальну команду, але pause спочатку для attach docker run -it --rm --entrypoint /bin/sh myimage -c "sleep 3600 & node server.js"

--entrypoint міняє entrypoint image на те, що ти задаєш. У комбінації з -it отримуєш інтерактивний prompt замість умираючого застосунку.

Крок 6: тактичні override

bash
# Запуск як root для діагностики (дефолтний user може не мати permission) docker exec -it -u root api sh # Mount strace з host або встанови у image docker run -it --cap-add=SYS_PTRACE myimage strace -p 1 # Вимкни healthcheck-driven restart-цикли docker run --health-cmd=NONE ... # Запуск з restart=no, щоб тримати failed-state видимим docker run --restart=no ... # Форсувати IPv4 для DNS-проблем docker run --dns=8.8.8.8 ...

Distroless-специфічний debug

Distroless image не мають shell. Щоб дебажити:

bash
# Більшість проектів публікують :debug варіант з busybox docker run -it --entrypoint sh gcr.io/distroless/base:debug # Або скопіюй busybox у твій debug-container docker run -it --entrypoint /busybox/sh gcr.io/distroless/base:debug

Для прод distroless image: збери окремий :debug tag з тим самим контентом + busybox-шар, деплой debug лише коли треба.

Типові помилки

Шукати log-файли у writable-шарі

bash
$ docker exec web cat /var/log/myapp.log # (часто порожньо або застаріло; застосунок має логувати у stdout)

Застосунки, що логують у файли всередині container, не видні для docker logs. Або переконфігуруй застосунок під stdout, або змонтуй volume для логів і читай з host.

Запуск без -t і отримання порожнього виводу

bash
$ docker exec api sh # (іноді висить або одразу виходить)

Потрібен -t для виділення TTY. Завжди -it для інтерактивних shell.

Забути, що docker stop міг вбити застосунок мід-cleanup

Якщо застосунок помирає під час docker stop, exit 143 (SIGTERM) або 137 (SIGKILL після grace), логи можуть бути truncated. Збільши --time на stop або перехоплюй SIGTERM у застосунку.

Плутати docker-proxy помилки з app-помилками

Error starting userland proxy: listen tcp 0.0.0.0:8080: bind: address already in use

Це від Docker, не від твого застосунку, порт 8080 уже зайнятий на host. Перевір lsof -i :8080 або обери інший host-порт.

Спеціалізовані debugging-tools

bash
# Image layer-аналіз dive myimage # інтерактивний layer-by-layer view docker history --no-trunc myimage # хто додав що # Network debugging docker exec api tcpdump -i any -nn -c 20 docker exec api ss -tnlp # listening-порти docker exec api ip route # Process tracing docker run --cap-add=SYS_PTRACE myimage strace -p 1 # Resource debugging docker stats --no-stream docker inspect --format '{{.HostConfig.Memory}}' container # Live filesystem changes docker diff <container> # що змінилося у writable-шарі

Реальне застосування

  • «Мій застосунок одразу виходить»: перевір exit code → перевір логи → exec в через --entrypoint sh, щоб переконатися, що бінар існує і виконуваний.
  • «Локально працює, у CI ні»: порівняй env-змінні, mount-шляхи, мережу. docker inspect це rosetta-камінь.
  • «Container unhealthy, але я не знаю чому»: docker inspect --format '{{json .State.Health}}' показує останні 5 healthcheck-спроб з виводом.
  • «Out-of-memory крашиться»: docker inspect на OOMKilled: true. Розмір? Недавно бампнуто? Memory-leak? docker stats за час.
  • «Не дістає інший container»: exec в, ping по service-імені, перевір /etc/resolv.conf, переконайся, що обидва container на тій самій мережі.

Питання для поглиблення

Q: Як отримати логи container, що видалено?


A: Не можна, docker rm видаляє log-файли container. Якщо передбачаєш потребу, налаштуй віддалений log-driver (json-file-логи переживають container, але syslog/journald/fluentd шиппять offsite). Або завжди docker logs > backup.log перед rm.

Q: Що означає exit code 125?


A: Помилка Docker daemon до старту container. Зазвичай проблема Docker-config (зламане image, поганий mount, конфлікт порту). Дивися daemon-лог (journalctl -u docker).

Q: Що означає exit code 126 vs 127?


A: 126 = команду знайдено, але не executable (permission або не та arch). 127 = команду не знайдено взагалі. Часто typo або відсутній бінар у image.

Q: Як дебажити Compose-сервіс?


A: Усі ті самі команди працюють через docker compose: docker compose logs api, docker compose exec api sh, docker compose run --rm api sh для one-off. Compose-обгортки знають твій project-контекст.

Q: (Senior) Як дебажити container, що крашиться на kubelet, але працює локально?


A: Відтвори точний run-config kubelet: той самий image-digest, ті самі env, той самий entrypoint, той самий UID, та сама network-policy. Pod-spec → docker run флаги це відоме перетворення. Часто різниця: K8s крутиться як non-root UID за замовчуванням, твій локал ні. Або K8s інжектить sidecar, що блокує трафік. Або K8s монтує secrets/configmaps, що твій локал не має. Бери kubectl debug для інтерактивного container у тому самому pod-контексті.

Приклади

Повна debugging-сесія

bash
$ docker ps -a --filter name=api STATUS NAMES Exited (137) 5 seconds ago api $ docker logs --tail 50 api ... багато виводу ... ERROR: connect ECONNREFUSED 172.18.0.5:5432 $ docker inspect api --format '{{.State.OOMKilled}} {{.State.ExitCode}} {{.HostConfig.Memory}}' false 137 536870912 # OOM=false, exit 137 → не пам'ять; SIGKILL прийшов звідкись $ docker logs --tail 50 db 2026-04-30 ... database system is shut down # db вийшов; api не зміг підключитися → SIGTERM каскадом. # Фікс: додай depends_on з healthcheck у compose, переконайся, що db переживає

Чотири команди тріангулювали проблему: api було вбито, бо db впав першим.

Дебаг image, що одразу крашиться

bash
$ docker run myimg # (виходить за 0.1 секунди) $ docker run --rm myimg --version # (теж виходить одразу, без виводу) # Override entrypoint, щоб дослідити $ docker run -it --rm --entrypoint sh myimg / # which myapp /usr/local/bin/myapp / # ls -la /usr/local/bin/myapp -rwxr-xr-x 1 root root 12M ... / # /usr/local/bin/myapp Segmentation fault # → не та архітектура! Скоріш за все x86-бінар у ARM-image. $ docker inspect --format '{{.Architecture}}' myimg amd64 $ uname -m aarch64 # Підтверджено: image-arch != host-arch.

Без --entrypoint sh segfault бінаря був невидимим; docker logs нічого не мав, бо процес помер до stdio.

Дебаг healthcheck

bash
$ docker ps --format '{{.Names}}: {{.Status}}' api: Up 5 minutes (unhealthy) $ docker inspect api --format '{{range .State.Health.Log}}{{.End}}: exit={{.ExitCode}} out={{.Output}}\n{{end}}' 2026-04-30T10:00:00Z: exit=0 out=ok 2026-04-30T10:01:00Z: exit=0 out=ok 2026-04-30T10:02:00Z: exit=7 out=connect: connection refused 2026-04-30T10:03:00Z: exit=7 out=connect: connection refused 2026-04-30T10:04:00Z: exit=7 out=connect: connection refused # → health-endpoint припинив відповідати 3 хвилини тому. Застосунок мабуть завис. $ docker exec api ps aux # Дивися на головний процес; чи він running, але не responsive? # Якщо так, застосунок застряг (deadlock, нескінченний цикл). Захопи stack trace.

Healthcheck-лог золото, п'ять спроб з exit-кодами і виводом.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Коментарі

Ще немає коментарів