Короткий ответ

Если контейнер сервиса web проекта app сразу Exited, не поднимайте его снова через up -d в цикле. Запустите тот же образ без detach, чтобы stdout/stderr пришли в ваш терминал: docker compose run --rm --no-deps web или docker start -a. Пустой docker logs чаще значит, что процесс не писал в stdout (буфер Python/Java) или PID 1 — shell, который умер до exec. Не лечите restart: always и не трогайте volume app_data, пока не увидели текст ошибки.

Цикл Restarting — та же смерть плюс политика: перезапуск.

Симптомы и как отличить

  • docker compose up -d мгновенно «Created», psExited (1);
  • docker logs app-web-1 пустой или одна строка standard_init_linux.go;
  • entrypoint bash -c './start.sh' и скрипт с set -e на первой команде;
  • путают с «портом занят»: тогда контейнер часто не создаётся, ошибка у compose, не Exited.
СигналСкорееСтатья
YAML/networksстек не поднялсяCompose
error mounting volumebind/namedvolume
Permission denied на dataUIDправа
Up + unhealthyпроцесс живhealthcheck

Возможные причины

  1. CMD/ENTRYPOINT указали неверный путь (python app.py без файла).
  2. Конфиг обязателен, файла нет на volume или bind.
  3. Shell-форма vs exec-форма: сигнал и код выхода «не те», скрипт падает на пробеле.
  4. Интерактивный бинарь ждёт TTY (-t нет в compose).
  5. stdin_open: true без процесса, читающего stdin — реже; чаще наоборот, java без -XX:+UseContainerSupport тут ни при чём.
  6. command: в YAML перетёр ENTRYPOINT образа так, что остался nginx без -g 'daemon off;' — nginx демонтизировался, PID 1 умер, контейнер Exited 0. Это выглядит как «сразу завершился» при успехе.
  7. Редко: read_only: true без tmpfs на /tmp.

Диагностика

Хост host.example, каталог /opt/app.

1. Состояние без догадок

cd /opt/app
docker compose ps -a
docker inspect app-web-1 --format 'status={{.State.Status}} exit={{.State.ExitCode}} err={{.State.Error}} pid={{.State.Pid}} oom={{.State.OOMKilled}}'
docker inspect app-web-1 --format '{{json .Config.Entrypoint}} {{json .Config.Cmd}}'

Pid 0 и exit != 0 — процесс уже мёртв. Error с OCI runtime — не приложение.

2. Запуск без detach

Именно это и есть рабочий приём, не «ещё один up -d».

docker compose run --rm --no-deps --entrypoint '' web sh -c 'id; ls -l /app; echo ENTRY; command -v python3'
docker compose run --rm --no-deps web

Вторая команда повторяет штатный ENTRYPOINT. Вывод сразу в SSH-сессию. --no-deps отсекает шум от db, если падает сам web. Если нужен DNS-имя db — уберите --no-deps и поднимите зависимость отдельно.

Эквивалент на уже созданном контейнере:

docker start -a app-web-1

Не добавляйте -d.

3. Почему logs пустые

docker inspect app-web-1 --format '{{.HostConfig.LogConfig.Type}}'
docker compose run --rm --no-deps -e PYTHONUNBUFFERED=1 -e NODE_DEBUG=cluster web

Для Java часто мешает буфер; для nginx в daemon mode логов в контейнере не будет, потому что процесса уже нет.

4. Mount и рабочий каталог

docker inspect app-web-1 --format '{{json .Mounts}}'
docker inspect app-web-1 --format '{{.Config.WorkingDir}}'

Bind на несуществующий файл хоста Docker иногда создаёт директорию вместо файла — приложение читает «каталог» и падает. Это классика ./nginx.conf.

Решение

Сценарий A. Видно исключение в foreground

Исправляете конфиг/секрет/путь. Пересборка образа, если правили код:

cd /opt/app
docker compose build web
docker compose up -d --no-deps web
docker compose logs --tail 50 web

Named volume app_data не трогайте.

Сценарий B. nginx/apache сразу Exited 0

В образе должен остаться foreground. В compose:

command: ["nginx", "-g", "daemon off;"]

Не service nginx start внутри скрипта без exec.

Сценарий C. executable file not found / 127

docker compose run --rm --no-deps --entrypoint sh web -c 'ls -l /usr/bin /app'

Windows CRLF в start.sh даёт bad interpreter. sed -i 's/\r$//'. Не «переустановите docker».

Сценарий D. Падает только в compose, run вручную жив

Сравните docker compose config с тем, что вы передали в docker run: environment, user:, read_only, command. Часто виноват переопределённый command: в YAML.

Сценарий E. Нужна зависимость

Поднимите db, дождитесь healthy, затем foreground web:

docker compose up -d db
docker compose ps db
docker compose run --rm web

Не вставляйте sleep 30 без верхней границы и без проверки порта.

Как проверить, что проблема устранена

docker compose ps
docker inspect app-web-1 --format '{{.State.Running}} {{.State.ExitCode}} {{.State.Pid}}'
docker logs --tail 20 app-web-1

Running true, PID ≠ 0, в логах — штатный баннер приложения, не stacktrace. Повторный compose up -d не должен сразу дать Exited.

Если не помогло

  • State.Error про seccomp: точечный cap_add/security_opt, не privileged.
  • TTY: tty: true только если бинарь реально требует; иначе оставьте выкл.
  • Multi-stage образ: в runtime-stage забыли скопировать бинарь — foreground сразу 127.
  • PID 1 — dumb-init + дочерний упал: смотрите код именно дочернего в логе.
  • Контейнер Exited 0 через 1 с — ищите daemonize, не «успех бизнеса».

Профилактика

  • В CI: docker compose run --rm --no-deps web /app/health-or-version без -d.
  • exec в скриптах entrypoint, чтобы PID 1 был приложением.
  • Не глотать ошибки set +e.
  • Bind-файлы проверять [ -f ] на хосте до up.
  • Для Python/Node — unbuffered в образе, не только в wiki.

FAQ

Чем run отличается от up?

up создаёт долгоживущий сервис по политике restart и сети проекта. run — разовый контейнер, по умолчанию без того же имени. Для отладки падения стартового — run или start -a, не десятый up -d.

Почему -d в cron «удобнее»?

Cron не видит stderr. Вы получите Exited и пустое письмо. Либо start -a с таймаутом, либо сразу logs.

Нужен ли --rm?

Для отладки да, чтобы не копить анонимные контейнеры. На named сервисе web используйте start -a, --rm удалит одноразовый run.

Можно ли docker logs --details вместо foreground?

Дополняет, не заменяет. Если процесс не стартовал, details не покажут traceback приложения.

Exited 0 — это ошибка?

Для long-running — да. Для migrate — нет. Смотрите роль сервиса, не только код.