Home DevOps Docker Troubleshooting Guide: Diagnose and Fix Container Crashes in Production
Intermediate 3 min · July 11, 2026

Docker Troubleshooting Guide: Diagnose and Fix Container Crashes in Production

Docker troubleshooting guide for production container crashes.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.

Follow
Production
production tested
July 11, 2026
last updated
258
articles · all by Naren
Before you start⏱ 30 min
  • Basic Docker commands (run, ps, logs, exec)
  • Familiarity with Linux processes and filesystem
  • Access to a Docker host with production containers
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer

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.

✦ Definition~90s read
What is Docker Troubleshooting?

Docker troubleshooting is the systematic process of identifying and resolving container failures in production. It covers crashes, hangs, resource exhaustion, and networking issues using Docker-native tools and Linux internals.

Think of a container like a shipping container on a cargo ship.
Plain-English First

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.

check_exit_code.shBASH
1
2
3
4
5
6
7
8
#!/bin/bash
# io.thecodeforge — DevOps tutorial
# Check exit code of a stopped container
docker inspect my_app --format '{{.State.ExitCode}}'
# Output: 137
# Then check OOM logs
dmesg | grep -i oom | tail -5
# Output: [12345.678] oom-kill: constraint=CONSTRAINT_MEMCG ...
Output
137
[12345.678] oom-kill: constraint=CONSTRAINT_MEMCG ...
Production Trap:
Never trust 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_debug.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
# io.thecodeforge — DevOps tutorial
# Check memory usage inside container
docker stats my_app --no-stream
# Output:
# CONTAINER ID   NAME      CPU %     MEM USAGE / LIMIT   MEM %
# abc123         my_app    45.2%     3.8GiB / 4GiB       95%
# Check cgroup memory
docker exec my_app cat /sys/fs/cgroup/memory/memory.usage_in_bytes
# Output: 4080218931 (about 3.8GiB)
# Check OOM kills for this container
dmesg | grep -i 'my_app' | grep -i oom
# Output: (if any OOM kill happened)
Output
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM %
abc123 my_app 45.2% 3.8GiB / 4GiB 95%
4080218931
Senior Shortcut:
Set --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.

disk_debug.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash
# io.thecodeforge — DevOps tutorial
# Check disk usage on host
df -h /var/lib/docker
# Output:
# Filesystem      Size  Used Avail Use% Mounted on
# /dev/sda1        50G   48G  2.0G  96% /var/lib/docker
# Check Docker disk usage
docker system df
# Output:
# TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
# Images          10        5         20GB      10GB (50%)
# Containers      8         3         5GB       2GB (40%)
# Local Volumes   5         2         10GB      8GB (80%)
# Build Cache     0         0         0B        0B
# Prune unused resources
docker system prune -a --volumes -f
Output
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 50G 48G 2.0G 96% /var/lib/docker
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 10 5 20GB 10GB (50%)
Containers 8 3 5GB 2GB (40%)
Local Volumes 5 2 10GB 8GB (80%)
Build Cache 0 0 0B 0B
Never Do This:
Don't run 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.

Dockerfile.with.tiniDOCKERFILE
1
2
3
4
5
6
7
# io.thecodeforge — DevOps tutorial
FROM openjdk:11-jre-slim
# Install tini
RUN apt-get update && apt-get install -y tini && rm -rf /var/lib/apt/lists/*
# Use tini as init
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["java", "-jar", "app.jar"]
The Classic Bug:
If your container ignores 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.

dns_debug.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
# io.thecodeforge — DevOps tutorial
# Test DNS inside container
docker exec my_app nslookup db.example.com
# Output:
# Server:         8.8.8.8
# Address:        8.8.8.8#53
# Non-authoritative answer:
# Name:   db.example.com
# Address: 10.0.0.1
# Check resolv.conf
docker exec my_app cat /etc/resolv.conf
# Output:
# nameserver 8.8.8.8
# nameserver 8.8.4.4
Output
Server: 8.8.8.8
Address: 8.8.8.8#53
Non-authoritative answer:
Name: db.example.com
Address: 10.0.0.1
nameserver 8.8.8.8
nameserver 8.8.4.4
Production Trap:
Don't hardcode IP addresses in your app. Use container names or service discovery. If you must use IPs, they change on restart. Always use DNS or environment variables.

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>.

Dockerfile.with.healthcheckDOCKERFILE
1
2
3
4
5
6
7
8
# io.thecodeforge — DevOps tutorial
FROM nginx:alpine
# Install curl for health check
RUN apk add --no-cache curl
# Add health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=30s --retries=3 \
  CMD curl -f http://localhost/ || exit 1
EXPOSE 80
Senior Shortcut:
Use --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.
● Production incidentPOST-MORTEMseverity: high

The 4GB Container That Kept Dying

Symptom
A Java microservice container exited with code 137 every 2 hours during peak traffic. Restarting fixed it temporarily.
Assumption
Team assumed a memory leak in the Java heap. They increased heap size and memory limit, but it still crashed.
Root cause
The container had a memory limit of 4GB, but the JVM's non-heap memory (Metaspace, thread stacks, direct buffers) plus the OS page cache consumed the rest. The OOM killer killed the container when total memory exceeded the limit.
Fix
Set -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.
Key lesson
  • Container memory limits apply to the whole cgroup, not just the app heap.
  • Always account for non-heap memory, and use docker stats to see real usage before setting limits.
Production debug guideSystematic recovery paths for the failure modes engineers actually hit.3 entries
Symptom · 01
Container exits with code 137 (OOM)
Fix
1. Run 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.
Symptom · 02
Container exits with code 139 (segfault)
Fix
1. Check app logs for core dump. 2. Run 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.
Symptom · 03
Container runs but is unresponsive
Fix
1. Check health check endpoint: 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.
★ Docker Troubleshooting Guide Triage Cheat SheetFirst-response commands for when things go wrong — copy-paste ready.
Container exits with code `137`
Immediate action
Check OOM killer logs
Commands
dmesg | grep -i oom | tail -10
docker inspect <container> --format '{{.State.ExitCode}}'
Fix now
Increase memory limit or fix memory leak. Add --memory-reservation.
Container exits with code `139`+
Immediate action
Check for segfault
Commands
docker logs <container> | tail -20
docker run --security-opt seccomp=unconfined <image>
Fix now
If seccomp is the issue, create a custom profile. Otherwise debug the app.
Container exits with code `0` immediately+
Immediate action
Check entrypoint/CMD
Commands
docker inspect <container> --format '{{.Config.Cmd}}'
docker run -it <image> sh
Fix now
Fix entrypoint script or CMD syntax.
Container runs but is unresponsive+
Immediate action
Check health endpoint
Commands
docker exec <container> curl -f http://localhost:8080/health
docker stats <container>
Fix now
If CPU 100%, kill and restart. If memory high, increase limit. If deadlock, restart.
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
check_exit_code.shdocker inspect my_app --format '{{.State.ExitCode}}'Why Containers Crash
memory_debug.shdocker stats my_app --no-streamMemory Troubleshooting
disk_debug.shdf -h /var/lib/dockerDisk Full
Dockerfile.with.tiniFROM openjdk:11-jre-slimZombie Processes and PID 1 Problems
dns_debug.shdocker exec my_app nslookup db.example.comNetwork Timeouts and DNS Resolution
Dockerfile.with.healthcheckFROM nginx:alpineHealth Checks

Key takeaways

1
Exit codes are your first clue
137 = OOM, 139 = segfault, 0 = clean exit (maybe unexpected).
2
Memory limits apply to the whole cgroup, not just the app heap. Always account for non-heap memory.
3
Always use an init system (tini/dumb-init) to handle signals and reap zombie processes.
4
Health checks should test real app functionality, not just process existence.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How does Docker's OOM killer decide which container to kill when multipl...
Q02SENIOR
When would you choose `docker restart` vs. fixing the root cause in a pr...
Q03SENIOR
What happens when you run `docker stop` and the container doesn't respon...
Q04JUNIOR
What is the difference between `docker logs` and `docker inspect` for tr...
Q05SENIOR
A container in production is crashing every hour with exit code 137. You...
Q06SENIOR
How would you design a Docker host to handle 100 containers with varying...
Q01 of 06SENIOR

How does Docker's OOM killer decide which container to kill when multiple containers exceed their memory limits?

ANSWER
Docker uses the kernel's OOM killer, which scores processes based on oom_score_adj. By default, Docker sets oom_score_adj to -500 for containers, making them less likely to be killed than host processes. But if a container exceeds its limit, the kernel kills the process inside that container. The score is based on memory usage, runtime, and oom_score_adj. You can adjust with --oom-score-adj.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
Why does my container keep restarting with exit code 137?
02
What's the difference between `docker stop` and `docker kill`?
03
How do I check if a container is using too much memory?
04
My container exits immediately with code 0. What's wrong?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.

Follow
Verified
production tested
July 11, 2026
last updated
258
articles · all by Naren
🔥

That's Docker. Mark it forged?

3 min read · try the examples if you haven't

Previous
Docker Resource Limits and cgroups
29 / 32 · Docker
Next
Docker with GitHub Actions