Every bug in this lesson has already been explained mechanically somewhere earlier in this domain — this is the field-reference version, the shape each one actually takes in real logs and real symptoms, so it's recognizable on sight instead of requiring the mechanism to be re-derived from scratch every time.
5 min read
:latest tag drift — "it worked yesterday"FROM node:20-slim
# ...
CMD ["node", "dist/index.js"]docker build -t my-app:latest .
docker push my-app:latest
# Three weeks later, someone else builds the "same" image:
docker build -t my-app:latest . # node:20-slim has since gotten a new patch release
docker push my-app:latest # :latest now silently points at a different imageNothing about this looks wrong in the Dockerfile — node:20-slim genuinely means "whatever the current 20.x slim image is," and Docker Hub updates that tag's underlying image over time. Two builds of the exact same Dockerfile, weeks apart, can produce different images, and any deployment still referencing :latest picks up whichever one was pushed most recently, non-reproducibly.
The fix: pin base images to a specific digest or minor version for anything beyond local experimentation, and never deploy :latest — tag builds with a git SHA or semantic version instead, so "what's actually running" is always a specific, reproducible answer.
FROM node:20.11.1-slimdocker build -t my-app:${GIT_SHA} .
docker push my-app:${GIT_SHA}OOMKilled with no error in the application logsA container exits, docker ps -a shows it exited with code 137, and the application's own logs show nothing — no exception, no stack trace, no final log line explaining what happened.
docker inspect my-app-container --format='{{.State.OOMKilled}}'
# trueExit code 137 is 128 + 9 — SIGKILL. The container didn't crash; the kernel killed it, because it exceeded a memory limit (--memory on docker run, resources.limits.memory in Kubernetes). SIGKILL can't be caught or logged by the application — from the app's perspective, it simply stops existing mid-instruction, which is exactly why the logs show nothing.
The fix: set memory limits based on actual measured usage (not a guess), and treat OOMKilled in docker inspect / kubectl describe pod as a distinct diagnosis from an application crash — the two look identical from "container exited unexpectedly" but have completely different causes and fixes (raise the limit or fix a memory leak, vs. fix the actual application bug).
CMD npm startdocker stop sends SIGTERM, waits a grace period (10 seconds by default), and if the process hasn't exited by then, sends SIGKILL. The problem: when CMD is written as a shell string (npm start), Docker runs it via /bin/sh -c "npm start" — and the shell, not npm, becomes PID 1. Signals sent to PID 1 don't automatically forward to child processes, so SIGTERM can hit the shell wrapper and never reach the actual Node process underneath it. Every deploy then waits the full grace period and gets a hard SIGKILL, and in-flight requests get dropped instead of finishing cleanly.
The fix: use the exec form of CMD (a JSON array, no shell involved) so the application process itself becomes PID 1 and receives signals directly:
CMD ["node", "dist/index.js"]For process managers that must stay involved (e.g., npm start genuinely needs to spawn a child), use a minimal init process like tini to forward signals correctly:
ENTRYPOINT ["tini", "--"]
CMD ["npm", "start"]CrashLoopBackOff that's actually a readiness problem, not a crashA Kubernetes Pod shows CrashLoopBackOff, and the natural read is "the application keeps crashing." Sometimes that's exactly right. But a second, easy-to-miss cause: a liveness probe configured with too short a initialDelaySeconds fires before a genuinely slow-booting app (loading a large in-memory dataset, warming caches) has finished starting, Kubernetes concludes the container is unhealthy, kills it, and the fresh container hits the exact same premature check on its next boot — a loop the application code never actually caused.
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5 # too short if boot genuinely takes 20s
periodSeconds: 10The fix: check kubectl describe pod for the specific reason before assuming an application bug — "Liveness probe failed" is a different diagnosis than an actual application panic in the logs. Raise initialDelaySeconds to comfortably exceed real boot time, or better, add a separate startupProbe that suppresses liveness checks entirely until the app reports it has finished booting.
.dockerignore that doesn't exist yetA team notices builds are getting slower and images are multiple gigabytes for what should be a small Node app. docker history my-app:latest shows a COPY . . layer alone accounting for 900MB.
docker history my-app:latest --format "{{.Size}}\t{{.CreatedBy}}"
# 900MB COPY . . # buildkitThe root cause, almost every time this shows up: no .dockerignore, so COPY . . includes node_modules from the host (built against the host's OS/architecture, often incompatible with the container's anyway), .git history, and every build artifact ever generated locally.
The fix: the .dockerignore covered in this domain's Dockerfile-best-practices lesson — excluding node_modules, .git, and build output from the build context stops this at the source rather than trying to clean it up after the fact in a later layer (which, as covered there, doesn't actually shrink the image anyway).
Every one of these bugs looks, at first glance, like "the application is broken" — a silent exit, a dropped connection, a restart loop. In every case the actual cause lives one layer down, in how the container runtime, the orchestrator, or the build context behaves, not in application logic at all. The habit that catches all five faster: before debugging the application code, check what the container/orchestrator itself is reporting (docker inspect, kubectl describe pod, docker history) — it usually states the real cause directly, in language that doesn't look like an application bug because it isn't one.
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Why is deploying an image tagged ':latest' risky for reproducibility?
2. Why does an OOMKilled container typically show nothing useful in the application's own logs?
3. Why might SIGTERM never reach an application started with a Dockerfile's shell-form CMD (e.g. 'CMD npm start')?
4. What is a non-obvious cause of a Kubernetes Pod stuck in CrashLoopBackOff that isn't actually an application crash?
Docker & Containers