DevOps

Fix: Docker container exits immediately with code 0

Docker container starts and stops in less than a second with exit code 0 — no error message. Here are the real causes and the fix for each.

You run:

docker run myapp

Container starts. Container stops. Zero output, exit code 0, nothing wrong per docker inspect. Just… gone. Here are the six real causes and the specific fix for each.

Diagnose first

Get actual state:

docker ps -a                    # see the exited container
docker logs <container-id>      # see any output
docker inspect <container-id>   # full detail — exit code, cmd, entrypoint

Check the effective command:

docker inspect <container-id> --format '{{.Config.Cmd}} / {{.Config.Entrypoint}}'

Exit code 0 means “container ran and finished normally”. So the real question is: what “finished normally” means for your container.

Cause 1 — Foreground command exited (nothing keeps container alive)

Docker containers stop the moment their main process (PID 1) exits. If your CMD is ["echo", "hello"], the container prints and stops.

Symptom: brand-new image, first run, immediately exits.

Fix — make PID 1 a long-running process:

For a web app, run the actual server:

CMD ["python", "app.py"]         # ✅ blocks on server

Not:

CMD ["python", "-c", "print('hi')"]   # ❌ exits after print

For debugging, override with a shell:

docker run -it myapp /bin/sh

This keeps the container alive as long as the shell has input.

Cause 2 — Web server daemonizes and exits

Apache, nginx, and some Node.js frameworks fork into the background by default. The container’s foreground process exits, so the container exits — while the actual daemon runs (until Docker kills it seconds later).

Symptom: logs show starting up then nothing. Container exits with 0.

Fix — run the server in foreground mode:

nginx:

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

Apache:

CMD ["apachectl", "-D", "FOREGROUND"]

PM2:

CMD ["pm2-runtime", "app.js"]      # not `pm2 start` — that forks

The “container way” is always foreground. Docker treats stdout as the log; daemonizing hides it.

Cause 3 — Entrypoint script exits silently after prep work

Custom entrypoint scripts (entrypoint.sh) sometimes do setup, then forget to exec the actual server:

#!/bin/sh
# ❌ Broken — script finishes after configure
./configure --port=8080

The script exits, so PID 1 exits, so container exits.

Fix — always exec the main process at the end:

#!/bin/sh
./configure --port=8080
exec "$@"           # runs whatever was passed as CMD

Then:

ENTRYPOINT ["./entrypoint.sh"]
CMD ["./my-server"]

exec replaces the shell process with the server, keeping it as PID 1 and inheriting signal handling.

Cause 4 — Alpine / distroless container missing an interpreter

You built a Go binary on Ubuntu with glibc, then copied to Alpine which has musl. Or you’re running a bash script in busybox which only has sh.

Symptom: container exits with 0 but nothing runs. Or docker logs shows no such file or directory (misleading — the file exists, but its interpreter doesn’t).

Fix — check the binary’s linkage:

docker run --rm -it myapp file /path/to/binary
docker run --rm -it myapp ldd /path/to/binary

If ldd says not a dynamic executable but the app crashes, it’s static and OK. If ldd lists shared libraries missing from the image, install them or switch to a compatible base.

Options:

  • Build fully static: CGO_ENABLED=0 for Go, --target=x86_64-unknown-linux-musl for Rust
  • Use compatible base: match Ubuntu build with ubuntu runtime, not alpine
  • Use distroless/cc for glibc binaries; distroless/static for fully static

Cause 5 — Shell script shebang mismatch

The script has #!/bin/bash but the container only has sh (Alpine, busybox, distroless).

Symptom: docker logs shows /bin/bash: not found and exit code 127 (not 0). But close enough to this class of issue.

Fix — one of:

  • Change shebang to #!/bin/sh and use POSIX-only syntax
  • Install bash in the container: RUN apk add --no-cache bash (Alpine)
  • Use bash-compatible shell script tools (shellcheck catches most bashisms)

Cause 6 — Missing —tty flag with interactive-only apps

Some apps (Python REPL, less, vim) exit immediately if stdin/stdout aren’t a TTY.

Symptom: works with docker run -it, fails with plain docker run.

Fix — allocate a TTY for interactive commands:

docker run -it myapp python

For non-interactive scripts, don’t use an interactive-only app as the entrypoint. Write a script that redirects to python -c or similar.

The universal exits-immediately debug flow

Every silent-exit case, run in order:

# 1. See the ACTUAL exit code + cmd/entrypoint
docker inspect <container> --format 'exit={{.State.ExitCode}} cmd={{.Config.Cmd}}'

# 2. Read all logs (even if empty)
docker logs <container> 2>&1

# 3. Run the image with an interactive shell to explore
docker run --rm -it --entrypoint /bin/sh myapp

# 4. Inside that shell, try running the intended command manually
./my-server
# (see what it does when NOT under Docker's normal PID 1 rules)

# 5. If a script, check for exec at the end
cat entrypoint.sh

Ninety percent of silent exits resolve when step 4 reveals the truth.

Prevention

For new Dockerfiles:

  • CMD runs a long-lived foreground process — never echo, ls, or something that returns
  • Always exec at the end of entrypoint scripts — inherits PID 1 correctly
  • Match glibc/musl between build and runtime bases — or build fully static
  • HEALTHCHECK in the Dockerfile — orchestrators know when app is dead:
    HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \
      CMD wget -qO- http://localhost:8080/health || exit 1
    
  • docker run --rm --init in dev — properly handles zombie processes
  • CI smoke test — docker run myapp & then sleep 5; docker ps | grep myapp — fail build if container isn’t still running

Bottom line

A container exiting with code 0 means “PID 1 finished its job.” The question is always “what was PID 1?” Read docker inspect to see the actual command, then run the image with --entrypoint /bin/sh to explore what should be happening. Ninety percent of silent exits are: foreground command returned immediately, daemonizing web server, entrypoint script forgot exec, or a binary/interpreter mismatch. Six specific fixes cover them all.

Recommended

DevOps YAML Pack

36 production-ready configs — Kubernetes, Docker Compose, GitHub Actions, Terraform, Helm, Ansible. Every file heavily commented. Copy, paste, ship.

Get the pack — ₹499 →
Never miss an article