Docker Troubleshooting Guide: Diagnose and Fix Container Crashes in Production
Docker troubleshooting guide for production container crashes.
20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.
- ✓Basic Docker commands (run, ps, logs, exec)
- ✓Familiarity with Linux processes and filesystem
- ✓Access to a Docker host with production containers
When a container crashes, start with docker logs for the exit message, then docker inspect for exit code and resource limits. For OOM, check dmesg | grep -i oom. For disk, docker system df. For zombie processes, ps aux inside the host.
Think of a container like a shipping container on a cargo ship. If the container falls off (crashes), you check the manifest (logs) to see why. Maybe it was overloaded (OOM), or the ship's crane broke (host issue). Docker troubleshooting is the process of reading the manifest, checking the ship's logs, and inspecting the container's condition to figure out what went wrong.
Your container just died at 3 AM. No alert, just a 137 exit code. Now what? Most devs panic and restart — then it dies again. That's not troubleshooting, that's gambling.
The problem is that Docker abstracts away the OS, but it doesn't abstract away failures. Containers crash for the same reasons processes crash: out of memory, disk full, deadlocks, or misconfigured limits. The difference is you have fewer tools inside the container to debug.
By the end of this guide, you'll be able to diagnose any container crash in under 5 minutes. You'll know exactly which command to run, what to look for, and how to fix it permanently. No more guessing, no more restart loops.
Why Containers Crash: The Three Horsemen
Before you run any command, understand the three root causes of container failure: resource exhaustion (OOM, disk full), application bugs (segfaults, uncaught exceptions), and infrastructure issues (Docker daemon restart, network timeouts). Each leaves a different signature.
OOM kills leave exit code 137 and a kernel log. Disk full leaves exit code 1 with a 'no space left on device' error in logs. Segfaults leave exit code 139. Infrastructure issues often leave exit code 125 (daemon error) or 126 (command not executable).
Your first step is always: check the exit code. docker inspect . Then match it to the list above. This narrows your search from everything to one category.
docker logs alone for crash diagnosis. If the container was OOM-killed, logs may be empty because the kernel killed it before it could write. Always cross-reference with dmesg and exit code.Memory Troubleshooting: Beyond the Heap
Memory issues are the #1 cause of container crashes in production. But most devs only look at the app heap. That's like checking your car's fuel gauge when the engine seizes — you're missing the oil pressure.
Inside a container, memory is used by: the app heap, native code (JNI, C libraries), thread stacks, Metaspace (JVM), page cache, and tmpfs mounts. All of it counts against the memory limit. If the sum exceeds the limit, the kernel kills the container.
To see real memory usage: docker stats <container>. Look at the MEM USAGE / LIMIT column. If it's close to 100%, you're on the edge. Then use docker exec <container> cat /sys/fs/cgroup/memory/memory.usage_in_bytes to get the exact cgroup value.
For JVM apps, add -XX:NativeMemoryTracking=summary and use jcmd <pid> VM.native_memory to see non-heap usage. For Node.js, check process.memoryUsage().
--memory-reservation to 70% of your memory limit. This tells the kernel to try to keep usage below that value. It gives you a buffer before the OOM killer activates. Also set --kernel-memory to limit kernel memory separately.Disk Full: The Silent Killer
A full disk doesn't crash your container — it freezes it. Writes fail silently, logs stop, and health checks time out. Then your orchestrator kills the container for being unhealthy. The real fix isn't restarting; it's freeing space.
First, check disk usage on the host: df -h. If any mount is at 100%, that's your problem. Docker stores images, containers, and volumes under /var/lib/docker by default. Use docker system df to see how much space each component uses.
Common culprits: unused images (docker image prune -a), stopped containers (docker container prune), and dangling volumes (docker volume prune). For production, set up a cron job to run docker system prune -f --volumes weekly.
But sometimes the issue is inside the container — logs growing unbounded. Always mount a log directory to a dedicated volume with a size limit, or use a log shipper that rotates.
docker system prune -a in production without checking what it removes. It deletes all unused images, including ones that may be needed for rollback. Use docker image prune -a --filter "until=24h" to keep recent images.Zombie Processes and PID 1 Problems
Containers run with PID 1 — the init process. If your app doesn't handle signal forwarding and reaping zombie children, you get a mess. Zombie processes accumulate, PID 1 doesn't respond to SIGTERM, and your container becomes unkillable.
This is a classic rookie mistake: running a Java app directly as java -jar app.jar. Java doesn't reap zombies. If your app spawns child processes (e.g., forking a shell command), those children become zombies when they exit, because PID 1 (Java) doesn't call .wait()
The fix: use a lightweight init system like tini or dumb-init. Add it to your Dockerfile: ENTRYPOINT ["/usr/bin/tini", "--"] then CMD ["java", "-jar", "app.jar"]. Tini handles signal forwarding and reaping.
To check for zombies inside a container: docker exec . If you see zombie processes, you need an init system.
docker stop and you have to docker kill it, you probably don't have an init system. The app isn't receiving SIGTERM. Add tini and test: docker stop should exit within 10 seconds.Network Timeouts and DNS Resolution
Your container can't reach the database. But it worked yesterday. Network issues in Docker are often DNS-related. By default, containers use the host's DNS via /etc/resolv.conf. If that file is stale or the DNS server is unreachable, name resolution fails.
First, test DNS inside the container: docker exec <container> nslookup db.example.com. If it fails, check the container's /etc/resolv.conf: docker exec <container> cat /etc/resolv.conf. It should list valid nameservers.
Common fix: set --dns flag to a reliable DNS server like 8.8.8.8. Or use --dns-search to add search domains. For production, run a local DNS cache like dnsmasq in a sidecar container.
Another gotcha: Docker's default bridge network doesn't allow DNS resolution by container name. Use a user-defined network: docker network create mynet then docker run --network mynet. This enables DNS resolution using container names as hostnames.
Health Checks: The First Line of Defense
You shouldn't wait for a crash to know something's wrong. Health checks let Docker monitor your container and restart it automatically if it becomes unhealthy. But most people write health checks that are too simple — they check if the process is running, not if the app is actually working.
A good health check tests a real endpoint: curl -f http://localhost:8080/health. It should return HTTP 200 within a timeout. Set --health-interval=30s, --health-retries=3, and --health-start-period=30s to give the app time to start.
In your Dockerfile: HEALTHCHECK --interval=30s --timeout=3s --start-period=30s --retries=3 CMD curl -f http://localhost:8080/health || exit 1.
To see health status: docker inspect --format '{{.State.Health.Status}}' . It returns healthy, unhealthy, or starting. If it's unhealthy, check the last 5 health check logs: docker inspect --format '{{json .State.Health.Log}}' .
--health-start-period to avoid premature restarts. Many apps need 10-30 seconds to initialize. Without this, Docker may restart your container in a loop during startup.cgroups v2 Specific Error Catalog: 8+ Errors and Exact Fix Paths
Modern Linux distributions (Ubuntu 22.04+, Fedora 35+, RHEL 9+) use cgroups v2 by default. Docker's cgroups v2 support has matured, but several errors still bite engineers. Catalog:
cgroup2: unknown option— Docker < 20.10 on cgroups v2. Fix: upgrade Docker to 20.10+.permission denied writing to /sys/fs/cgroup— container lacks CAP_SYS_ADMIN or cgroupns mode restricts writes. Fix:docker run --privilegedor--security-opt systempaths=unconfined.write /sys/fs/cgroup/...: device or resource busy— cgroup fs already mounted with different options. Fix: ensuresystemd.unified_cgroup_hierarchy=1on kernel cmdline.containerd: failed to mount cgroup2— containerd < 1.5 incompatible with kernel cgroup2. Fix:apt-get install --only-upgrade containerd.io.runc: symlink /sys/fs/cgroup— runc < 1.0.0-rc93. Fix: upgrade containerd which bundles runc.OCI runtime create failed: unable to get device controller— missingmemoryoriocontroller. Fix: enableCONFIG_CGROUP_MEMORYandCONFIG_BLK_CGROUPin kernel.failed to write "max" to memory.max— swap limit requiresmemory.swap.maxwhich may be absent. Fix: set--memory-swapequal to--memoryor configure kernel withCONFIG_MEMCG_SWAP=y.cgroup: fork failed: resource temporarily unavailable— cgroup pids limit hit. Fix: increase--pids-limit(default: unlimited in Docker, but systemd may set 4096).
Always check docker info | grep cgroup to see which cgroup version Docker is using. For mixed cgroups v1/v2 hosts, set "cgroupns": "host" in daemon.json.
docker info | grep Cgroup after any kernel upgrade.Healthcheck Failure Debugging: Why Your Service Is Stuck in 'starting'
Docker health checks can fail in subtle ways. The most common: a service stuck in starting status indefinitely. This happens when --health-start-period is too short — the health check command runs before the app is ready, fails, and Docker keeps retrying until start_period elapses. After that, if the first check still fails, the container goes unhealthy. Symptoms: docker inspect --format '{{.State.Health.Status}}' shows starting for longer than expected. Debug with docker inspect --format '{{json .State.Health.Log}}' to see the last 5 health check outputs. If the health check command itself is failing (e.g., curl not found), install it in the Dockerfile or use a shell-based check like CMD pgrep nginx || exit 1. Cascading healthcheck failures: when service A depends on service B, and B's health check fails, A's health check may also fail (e.g., A checks B's endpoint). Break circular dependencies by making health checks independent. Use start_period generously (60s for Spring Boot, 30s for Node.js) and set retries to 3 with interval of 15-30s.
docker exec <container> curl -f http://localhost:8080/health. If it fails, tweak the command, not the start_period.Exit Code Reference Table: What Each Code Means and How to Respond
Docker exit codes are your first diagnostic signal. Here is the comprehensive reference:
| Exit Code | Signal / Meaning | Common Cause | Immediate Action | |
|---|---|---|---|---|
| 0 | Clean exit | App intentionally stopped (e.g., batch job completed, cron finished) | Expected — no action needed | |
| 1 | General error | App-specific failure (uncaught exception, config error) | Check docker logs for error message | |
| 125 | Daemon error | Docker daemon failed to start the container (invalid config, missing entrypoint) | Check docker inspect for error message in State.Error | |
| 126 | Command not executable | Entrypoint/CMD binary lacks +x permission or is a script without shebang | chmod +x the binary or fix shebang line | |
| 127 | Command not found | Entrypoint/CMD binary does not exist in the image | Verify PATH and binary location inside container | |
| 130 | SIGINT (Ctrl+C) | Container received Ctrl+C (interactive stop) | Expected when manually stopping — no action | |
| 137 | SIGKILL (OOM) | Container exceeded memory limit and was killed by OOM killer | Check `dmesg | grep -i oom`, increase memory limit or fix leak |
| 139 | SIGSEGV (segfault) | Memory access violation, null pointer dereference | Check app logs, try --security-opt seccomp=unconfined to rule out seccomp | |
| 143 | SIGTERM (graceful stop) | docker stop was issued, app received SIGTERM and exited | Expected during graceful shutdown — no action | |
| 255 | Exit status out of range | Docker or shell returned an invalid exit code (rare) | Check container logs and daemon logs for anomalies |
Pro tip: Use docker inspect to quickly see both exit code and whether OOM was the cause.
PID 1 Signal Handling: Why Your Container Ignores SIGTERM and Requests Get Dropped
When you run docker stop, Docker sends SIGTERM to PID 1 inside the container. If PID 1 doesn't handle SIGTERM, the default behavior is to ignore it. After 10 seconds, Docker sends SIGKILL. This causes delayed deploys (orchestrator waits for the stop timeout), dropped in-flight requests (SIGKILL doesn't allow cleanup), and zombie processes (no reaper). The root cause: running the application directly as PID 1 (e.g., CMD ["java", "-jar", "app.jar"]). Java, Node.js, Python, and Go default binaries don't reap zombies or forward signals. The solution: use a lightweight init system like tini or dumb-init. In Dockerfile: ENTRYPOINT ["/usr/bin/tini", "--"] and CMD ["your-app"]. Tini ensures SIGTERM is forwarded to the child process and waits for it to exit before allowing the container to die. It also reaps zombie processes. For Node.js apps specifically, the --init flag on docker run achieves the same: docker run --init node:18-alpine node app.js. This inserts tini automatically. Verify: docker exec — PID 1 should be tini, your app should be PID 2 or higher.
docker stop timeout, each delayed container adds 10 seconds to rolling updates. For a 50-service deployment, that's 8+ minutes of extra downtime. Init systems aren't optional — they're a deployment velocity multiplier.The 4GB Container That Kept Dying
-XX:MaxMetaspaceSize=256m and -XX:ReservedCodeCacheSize=128m in JVM flags. Also set --memory-reservation to 3GB to give the kernel better hints. No more OOM kills.- Container memory limits apply to the whole cgroup, not just the app heap.
- Always account for non-heap memory, and use
docker statsto see real usage before setting limits.
dmesg | grep -i oom | tail -10 to confirm OOM kill. 2. Run docker stats <container> to see memory usage before crash. 3. Increase memory limit or fix memory leak. 4. Add --memory-reservation to 70% of limit.docker run --security-opt seccomp=unconfined <image> to test if seccomp is blocking syscalls. 3. If it works, create a custom seccomp profile. 4. Otherwise, debug the app binary.docker exec <container> curl -f http://localhost:8080/health. 2. Check logs: docker logs --tail 50 <container>. 3. Check resource usage: docker stats <container>. 4. If CPU 100%, check for infinite loop. If memory 100%, OOM imminent.dmesg | grep -i oom | tail -10docker inspect <container> --format '{{.State.ExitCode}}'| File | Command / Code | Purpose |
|---|---|---|
| check_exit_code.sh | docker inspect my_app --format '{{.State.ExitCode}}' | Why Containers Crash |
| memory_debug.sh | docker stats my_app --no-stream | Memory Troubleshooting |
| disk_debug.sh | df -h /var/lib/docker | Disk Full |
| Dockerfile.with.tini | FROM openjdk:11-jre-slim | Zombie Processes and PID 1 Problems |
| dns_debug.sh | docker exec my_app nslookup db.example.com | Network Timeouts and DNS Resolution |
| Dockerfile.with.healthcheck | FROM nginx:alpine | Health Checks |
| check_cgroup_version.sh | docker info | grep -i cgroup | cgroups v2 Specific Error Catalog |
| healthcheck_debug.sh | docker inspect --format '{{.State.Health.Status}}' my_container | Healthcheck Failure Debugging |
| exit_code_check.sh | docker inspect my_container --format 'ExitCode={{.State.ExitCode}} OOM={{.State... | Exit Code Reference Table |
| Dockerfile.with.tini | FROM node:18-alpine | PID 1 Signal Handling |
Key takeaways
Interview Questions on This Topic
How does Docker's OOM killer decide which container to kill when multiple containers exceed their memory limits?
--oom-score-adj.Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.
That's Docker. Mark it forged?
7 min read · try the examples if you haven't