In this blog post Keep Docker Containers Running Prevent Exits in Production and Dev we will walk through why Docker containers exit and the reliable ways to keep them running.
At a high level, a container runs a single main process. When that process finishes, the container stops. This sounds simple, but it’s the root of most “my container keeps exiting” issues. The fix is usually to ensure the correct process is running in the foreground and to apply the right lifecycle controls for your environment.
We’ll start with the key concepts behind Docker’s process model, then move to practical steps, patterns, and common pitfalls. By the end, you’ll have a checklist to keep containers alive for development, CI, or production.
How Docker decides whether a container is “running”
Docker is built on Linux namespaces and cgroups and manages containers by tracking their PID 1—the first process inside the container. The Docker daemon considers a container “running” as long as PID 1 is alive. When PID 1 exits, Docker stops the container.
Important implications:
- Your application (or the command you configure) must run in the foreground. If it daemonizes or backgrounds itself, the container will exit when the shell script ends.
- CMD and ENTRYPOINT define what becomes PID 1. Overriding these at runtime changes what keeps the container alive.
- PID 1 has special signal and zombie-reaping responsibilities. Use an init process when needed.
Quick checklist when a container exits immediately
- Inspect logs:
docker logs <container>
- Check exit reason:
docker inspect -f '{{.State.ExitCode}} {{.State.Error}} {{.State.OOMKilled}}' <container>
- Review the command/entrypoint actually used:
docker inspect -f '{{.Path}} {{.Args}}' <container>
- Run the image interactively to reproduce:
docker run --rm -it <image> sh
Proven ways to keep a container running
1) Make the main service run in the foreground
Many services (nginx, Apache, some app servers) default to daemon mode. In containers, you want the opposite: keep the service in the foreground so Docker can track it.
# NGINX example (foreground mode)
docker run -d --name web nginx:alpine nginx -g 'daemon off;'
In a Dockerfile, encode this as the default CMD so you don’t need to remember it each time:
# Dockerfile
FROM nginx:alpine
CMD ["nginx", "-g", "daemon off;"]
Other examples:
- Apache HTTPD:
httpd-foreground
is the correct command provided by the official image. - Node.js:
CMD ["node", "server.js"]
, and make sureserver.js
blocks (e.g., starts an HTTP server) instead of running a one-off script and exiting.
2) Use a long-running command for dev and debugging
Sometimes you want a “stay alive” container to exec into. Use a harmless long-running command:
# Keeps running indefinitely (good for dev)
docker run -d --name devbox alpine sleep infinity
# Alternative universal pattern
docker run -d --name devbox alpine sh -c "while true; do sleep 3600; done"
# Tail a file (e.g., logs) to keep PID 1 active
docker run -d -v $(pwd)/logs:/logs alpine sh -c "touch /logs/app.log && tail -f /logs/app.log"
These are great for development and debugging but are not a replacement for a properly foregrounded service in production.
3) Get CMD and ENTRYPOINT right
Prefer the JSON (exec) form to avoid shell quirks and signal-handling issues, and so that your process becomes PID 1 directly.
# Good: exec form (no implicit shell)
ENTRYPOINT ["/app/start"]
CMD ["--config", "/etc/app/config.yml"]
# Risky: shell form (signal pass-through and quoting pitfalls)
ENTRYPOINT /app/start --config /etc/app/config.yml
Also, avoid scripts that start your app with an ampersand (&). If a script backgrounds the main process and exits, the container stops. Keep the main process in the foreground or end with exec
to replace the shell:
# entrypoint.sh
#!/bin/sh
set -e
# Correct: replace the shell so your app becomes PID 1
exec /usr/bin/myapp --serve
4) Use an init process for proper signal handling
PID 1 inside a container doesn’t automatically reap zombies or forward signals as you might expect. An init process solves this. The simplest option is Docker’s built-in --init
flag (uses tini).
# Add a minimal init to handle signals and zombies
docker run -d --init <image>
In Dockerfile or Compose, you can make it the default:
# Dockerfile example using dumb-init (Alpine)
FROM node:20-alpine
RUN apk add --no-cache dumb-init
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
CMD ["node", "server.js"]
# docker-compose.yml
services:
api:
image: myorg/api:latest
init: true
command: ["node", "server.js"]
5) Apply restart policies for resilience
Restart policies don’t keep a healthy container “busy,” but they do recover from crashes, reboots, and transient failures.
# Restart unless you explicitly stop it
docker run -d --restart unless-stopped myimage
# Only restart on non-zero exit codes
docker run -d --restart on-failure:3 myflakyimage
# docker-compose.yml
services:
web:
image: nginx:alpine
command: ["nginx", "-g", "daemon off;"]
restart: unless-stopped
Note: a constantly failing container will loop with a restart policy. Use logs and health checks to fix the root cause.
6) Keep interactive dev containers open
For hands-on work, start a shell and keep STDIN open with a TTY:
docker run --rm -it --name toolbox ubuntu:24.04 bash
If you need it alive in the background, pair with a long-running command:
docker run -d --name toolbox -it ubuntu:24.04 sleep infinity
# Then attach or exec as needed
docker exec -it toolbox bash
7) Running multiple processes? Use a supervisor—or split services
Best practice is one service per container. If you must run multiple processes (e.g., tiny agent + main app), use a lightweight supervisor or an init system and ensure the supervisor stays in the foreground.
# Example using s6-overlay or a minimal supervisor (conceptual)
# Ensure the supervisor is PID 1 and the process tree stays attached
If processes are unrelated, prefer splitting them into separate containers and connecting them via a network.
Common pitfalls that cause containers to exit
- Daemonizing by default: Services like nginx or mysqld may exit unless configured to run in the foreground.
- Shell scripts that end: A startup script that backgrounds processes and then exits will stop the container. Use
exec
or run the service in the foreground. - Override mistakes:
docker run <image> bash
overrides the Dockerfile’sCMD
, so your app won’t start. - Health checks confusion: A failing HEALTHCHECK marks the container “unhealthy,” but doesn’t stop it by itself. Orchestrators may react to health status; Docker’s restart policy won’t automatically respond to health check failure.
- OOM kills: If memory limits are too low, the kernel may kill your process. Check
OOMKilled
indocker inspect
and raise limits. - Signal handling: If your app ignores SIGTERM, Docker will send SIGKILL after the timeout, which can corrupt state. Use an init process and graceful shutdown handlers.
Production-minded patterns
Foreground-first images
Build images that expose a single, foregrounded service with clear CMD
/ENTRYPOINT
. Avoid bash-centric entrypoints unless they add real value.
Include an init where necessary
Use --init
or bundle tini
/dumb-init
so PID 1 behaves correctly, especially for applications that spawn workers.
Right-size resource limits
Set CPU and memory limits that match your workload. Include observability (metrics, logs) to catch restarts early.
Use restart policies and health checks together
Combine --restart unless-stopped
with a HEALTHCHECK. While Docker doesn’t restart on health failure by itself, tooling and orchestrators can act on it, and you gain visibility.
# Dockerfile
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
Docker Compose equivalents
Compose lets you encode the “keep it running” patterns in one file.
version: "3.9"
services:
web:
image: nginx:alpine
command: ["nginx", "-g", "daemon off;"]
restart: unless-stopped
api:
build: ./api
init: true
environment:
- NODE_ENV=production
restart: on-failure
devbox:
image: ubuntu:24.04
command: ["sleep", "infinity"]
tty: true
stdin_open: true
Troubleshooting sequence when a container won’t stay up
- Confirm the real command:
docker inspect -f '{{.Path}} {{.Args}}' <container>
- Read logs:
docker logs -n 100 -f <container>
- Check exit causes:
docker inspect -f '{{json .State}}' <container> | jq
- Try a shell in the image:
docker run --rm -it <image> sh -lc '<your command>'
- Remove backgrounding and use
exec
in entrypoint scripts. - Add
--init
and test signal handling (docker stop
should exit cleanly). - Apply restart policy only after the process reliably stays in the foreground.
Summary
Containers stop when their main process exits. To keep them running, ensure your service runs in the foreground, use proper CMD
/ENTRYPOINT
, consider an init process, and apply restart policies for resilience. For development, long-running commands like sleep infinity
are convenient; for production, foreground-first images with correct signal handling are the gold standard.
If you need help hardening your container runtimes or standardizing images for your teams, the CloudProinc.com.au team works with organisations to make containers predictable, observable, and production-ready.
Discover more from CPI Consulting
Subscribe to get the latest posts sent to your email.