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 <container> --format '{{.State.ExitCode}}'. 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 <container> ps aux | grep 'Z'. 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}}' <container>. It returns healthy, unhealthy, or starting. If it's unhealthy, check the last 5 health check logs: docker inspect --format '{{json .State.Health.Log}}' <container>.
--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.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 |
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?
3 min read · try the examples if you haven't