Устранение неполадок Docker-контейнеров: распространенные проблемы при запуске и их решения

Диагностика контейнеров Docker, которые завершаются с ошибкой, не могут привязать порты, не находят файлы, сталкиваются с проблемами прав доступа или завершаются из-за нехватки памяти.

Устранение неполадок Docker-контейнеров: распространенные проблемы при запуске и их решения

Когда контейнер Docker не запускается, самое быстрое решение обычно приходит, если сопротивляться желанию гадать. Контейнер — это просто процесс с файловой системой, окружением, сетевыми настройками и ограничениями вокруг него. Если этот процесс завершается, Docker записывает причину. Ваша задача — собрать доказательства в правильном порядке.

Обычно я начинаю с трех вопросов: создал ли Docker контейнер, запустился ли основной процесс, и не убил ли или не заблокировал ли его что-то извне? Эти вопросы отделяют неправильное имя образа от сломанной команды, конфликт портов от сбоя приложения и проблему с правами доступа от ограничения памяти.

Начните с простой команды, которая говорит правду:

docker ps -a

Посмотрите на STATUS, PORTS и NAMES. Created означает, что Docker создал контейнер, но не запустил его. Exited (1) часто означает, что приложение вернуло обычную ошибку. Exited (127) обычно указывает на отсутствующую команду. Exited (137) часто означает, что процесс был убит извне, часто из-за нехватки памяти. Эти коды — подсказки, а не окончательные ответы, но они не дают вам отлаживать не тот слой.

Затем прочитайте логи:

docker logs --tail 100 <container>
docker logs -f <container>

Если контейнер умирает сразу, docker logs обычно полезнее, чем повторный запуск той же команды docker run. Фреймворки приложений часто выводят точную отсутствующую переменную окружения, ошибку миграции, неверный файл конфигурации или ошибку привязки перед завершением.

Для низкоуровневого состояния проверьте контейнер:

docker inspect <container> --format '{{json .State}}'
docker inspect <container> --format 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}} error={{.State.Error}}'

Эту вторую команду стоит запомнить. Она сообщает, видел ли Docker убийство из-за нехватки памяти (OOM), какой код выхода был записан и была ли ошибка во время выполнения.

Если контейнер завершается сразу

Контейнер остается живым только до тех пор, пока жив его основной процесс. Если команда завершается, Docker останавливает контейнер. Это удивляет людей, когда они запускают скрипты, которые запускают демон в фоне и затем возвращаются.

Например, такой шаблон часто завершается:

CMD service nginx start

Команда service может запустить nginx и затем завершиться. Docker видит, что основной процесс завершился, и останавливает контейнер. Дружественный к контейнерам шаблон — запускать сервер на переднем плане:

CMD ["nginx", "-g", "daemon off;"]

Та же идея применима к Node, Python, Java и рабочим процессам. Команда в CMD или ENTRYPOINT должна быть долго работающим процессом, а не запускатором, который отправляет реальную работу в фон и завершается.

Если в логах отображается command not found, no such file or directory или exec format error, протестируйте образ в интерактивном режиме:

docker run --rm -it --entrypoint sh <image>

Некоторые образы не включают bash, особенно Alpine и образы в стиле distroless. Используйте sh в первую очередь, если вы не знаете, что bash существует. Внутри проверьте путь к файлу, права доступа и интерпретатор:

ls -l /app
which python || true
head -1 /app/start.sh

Скрипт может существовать и все равно завершаться с ошибкой no such file or directory, если его shebang указывает на отсутствующий интерпретатор, например #!/bin/bash в образе, где есть только /bin/sh. Другая распространенная причина — окончания строк Windows. Если скрипт оболочки был отредактирован в Windows, невидимый символ \r может заставить Linux искать /bin/sh\r.

Если Docker говорит, что порт уже занят

Конфликты портов происходят на стороне хоста. В -p 8080:80 8080 — это порт хоста, а 80 — порт контейнера. Если что-то уже прослушивает порт хоста 8080, Docker не может его привязать.

Вы можете увидеть ошибку типа bind: address already in use или port is already allocated. Найдите слушателя:

sudo lsof -i :8080
# или
sudo ss -ltnp 'sport = :8080'

На macOS lsof обычно проще всего. На серверах Linux ss часто доступен по умолчанию. В Windows PowerShell используйте:

Get-NetTCPConnection -LocalPort 8080

Затем выберите другой порт хоста или остановите службу, которой он принадлежит:

docker run -d -p 8081:80 nginx

Не меняйте порт контейнера, если только приложение внутри контейнера действительно не прослушивает этот новый порт. Если nginx прослушивает порт 80 внутри контейнера, -p 8081:80 правильно. -p 8081:8081 не сработает из браузера, если ничего внутри контейнера не прослушивает порт 8081.

Если приложение запускается, но не может найти конфигурацию

Многие сбои при запуске связаны с отсутствующими переменными окружения. Образ в порядке, команда в порядке, но приложение ожидает DATABASE_URL, REDIS_URL, ключ API или файл конфигурации.

Проверьте, что Docker передал:

docker inspect <container> --format '{{range .Config.Env}}{{println .}}{{end}}'

Для проектов Compose проверьте разрешенную конфигурацию, а не только читайте docker-compose.yml:

docker compose config

Это позволяет выявить ошибки отступов, сюрпризы в файле .env и переменные, которые развернулись в пустые строки. Реальный пример: DATABASE_URL=${DATABASE_URL} выглядит безобидно, но если оболочка или файл .env не определяют его, ваше приложение может получить пустое значение и завершиться с ошибкой при запуске.

Будьте осторожны с секретами в логах и истории терминала. Для быстрой локальной отладки передача -e NAME=value подходит. Для общих систем используйте механизм секретов вашей платформы или файл окружения с контролируемыми правами доступа.

Если примонтированные тома или тома вызывают ошибки прав доступа

Контейнер может не запуститься, потому что не может прочитать файл конфигурации, записать PID-файл, создать каталог кэша или инициализировать каталог базы данных. В логах обычно говорится permission denied, read-only file system или operation not permitted.

Сначала проверьте монтирование:

docker inspect <container> --format '{{json .Mounts}}'

Затем проверьте, от какого пользователя запущен контейнер:

docker inspect <container> --format 'user={{.Config.User}}'

Если user пуст, образ может по умолчанию запускаться от root, но многие производственные образы устанавливают пользователя, не являющегося root. Каталог хоста, принадлежащий вашему локальному UID, может быть недоступен для записи UID 1000, 1001 или пользователя, специфичного для службы внутри контейнера.

Практическая последовательность отладки:

ls -ld ./data
docker run --rm -it -v "$PWD/data:/data" --entrypoint sh <image>
id
ls -ld /data
touch /data/test

Избегайте решения каждой проблемы с правами доступа с помощью chmod 777. Это может скрыть непосредственную проблему, создав более серьезную. Предпочтительнее сопоставлять владельца или использовать именованные тома для данных приложения:

docker volume create app_data
docker run -d -v app_data:/var/lib/app <image>

Именованные тома особенно полезны в Docker Desktop, где примонтированные каталоги пересекают границу виртуализации и могут вести себя иначе, чем нативные файловые системы Linux.

Если контейнер был убит из-за нехватки памяти

Код выхода 137 — это сильный намек на то, что процесс получил SIGKILL. В работе с Docker это часто означает, что ядро или Docker Desktop убили его из-за нехватки памяти. Подтвердите с помощью inspect:

docker inspect <container> --format 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}}'

Если OOMKilled равно true, у вас две задачи: дать процессу достаточно памяти для запуска и понять, почему ему потребовалось так много. Увеличение лимита может быть правильным производственным исправлением для базы данных или службы JVM. Для небольшого веб-сервиса это может указывать на плохое значение по умолчанию.

Java-приложения — классический пример. Старое поведение JVM не всегда хорошо вписывалось в ограничения контейнеров, и даже современным JVM все еще нужны разумные настройки -Xmx или процентные настройки для предсказуемого поведения. Службы Node могут нуждаться в --max-old-space-size в средах с ограниченной памятью. Базы данных могут нуждаться в явных настройках кэша.

Для одноразового теста:

docker run --memory=1g <image>

Если вы используете Docker Desktop, также проверьте память, выделенную виртуальной машине Docker. Ограничение контейнера не поможет, если сама виртуальная машина испытывает нехватку.

Если образ никогда не загружается или сборка никогда не создала образ

Иногда проблемы с контейнером нет, потому что нет пригодного для использования образа. Если docker run завершается ошибкой до создания контейнера, проверьте образ отдельно:

docker image ls | grep my-app
docker pull my-registry/my-app:tag

Для частных реестров подтвердите аутентификацию:

docker login <registry>

Для локальных образов убедитесь, что тег, который вы запускаете, совпадает с тегом, который вы собрали:

docker build -t my-app:dev .
docker run --rm my-app:dev

Распространенная локальная ошибка — сборка my-app:dev и запуск my-app:latest, который может указывать на более старый образ или вообще ни на что.

Если винят сеть, но служба не прослушивает

Когда браузер не может подключиться к контейнеру, люди часто сразу переходят к сетям Docker. Сначала докажите, что приложение прослушивает внутри контейнера.

docker exec -it <container> sh
ss -ltnp || netstat -ltnp

Если приложение привязано к 127.0.0.1 внутри контейнера, публикация портов Docker не поможет. Приложение должно прослушивать 0.0.0.0 или адрес интерфейса контейнера. Это часто встречается с серверами разработки. Например, многие фреймворки по умолчанию используют localhost и требуют флаг типа --host 0.0.0.0.

Затем подтвердите опубликованный порт:

docker port <container>
docker ps --format 'table {{.Names}}	{{.Ports}}'

Вы должны увидеть что-то вроде 0.0.0.0:8080->3000/tcp. Если опубликованного порта нет, служба может работать из другого контейнера в той же сети, но не из браузера вашего хоста.

Надежный контрольный список для запуска

Используйте этот порядок, когда застряли:

  1. docker ps -a, чтобы увидеть, существует ли контейнер и как он завершился.
  2. docker logs --tail 100 <container>, чтобы прочитать жалобу самого приложения.
  3. docker inspect <container>, чтобы проверить код выхода, статус OOM, команду, пользователя, монтирования и порты.
  4. docker run --rm -it --entrypoint sh <image>, чтобы протестировать образ вручную.
  5. Убирайте по одной переменной за раз: сначала запустите без монтирований, затем без пользовательских сетей, затем только с необходимыми переменными окружения.

Этот последний шаг важен. Длинная команда docker run с портами, томами, файлами окружения, пользовательским DNS, ограничениями памяти и пользовательской точкой входа дает вам слишком много подозреваемых. Упростите ее, пока образ не запустится, затем добавляйте настройки обратно, пока он не сломается. Настройка, которую вы только что добавили, обычно и есть место, где находится реальная проблема.