Docker — Intermediate
Dockerfile Best Practices, Beyond the Basics
A bad Dockerfile creates large, slow, insecure images. A good one creates small, fast, secure images — this is where junior and senior engineers visibly differ, and it's a near-universal interview topic.
The most common mistake — wrong layer order. Docker caches layers; if layer N changes, every layer after it rebuilds. Dependencies change rarely, code changes every commit — always copy dependencies before code.
npm ci --omit=dev is the current flag (npm 9+); --production still works but is deprecated, and --only=production is deprecated further still — use --omit=dev in new Dockerfiles. If the app needs a build step (TypeScript, a bundler) before running, install with dev dependencies first, run the build, then prune: npm ci (full install) → npm run build → npm prune --omit=dev — installing production-only before building will fail if the build tooling itself lives in devDependencies, which it does in most real projects.
In the GOOD version, npm install only reruns when package.json actually changes — a code-only commit reuses the cached dependency layer entirely, turning a multi-minute rebuild into a few seconds.
Multi-Stage Builds — Production Standard
Multi-stage builds use multiple FROM instructions; the final image only contains what the last stage explicitly copies. A 1.2GB build image can become a 50MB production image this way.
.dockerignore — Always Create This
Without a .dockerignore, COPY . . sends the entire directory — including .git history, local node_modules, and secrets in .env — into the build context, slowing builds and risking secrets baked into image layers.
Docker Commands — Complete Reference
Troubleshooting — Real Production Issues
Exit codes — what they mean:
| Exit Code | Meaning | What to do |
|---|
|---|---|---|
| 0 | Clean exit — app stopped itself | Check if this was expected |
|---|---|---|
| 1 | Application error / unhandled exception | Check application logs |
| 137 | SIGKILL — usually OOMKilled (memory limit exceeded), but also a manual docker kill/kill -9. Confirm with docker inspect --format='{{.State.OOMKilled}}' rather than assuming | Increase memory limit or fix a memory leak, if OOM-confirmed |
| 139 | Segmentation fault — app crashed | Check for a null pointer or buffer overflow |
| 143 | SIGTERM received — graceful shutdown | Normal — container stopped gracefully |
| 125 | Docker daemon error | Check the Docker daemon logs |
| 126 | Command not executable (permission) | Check file permissions inside the container |
| 127 | Command not found | Check CMD/ENTRYPOINT and image contents |
Security — Container Hardening
If this image runs under Kubernetes, the same non-root/hardening intent can be enforced at the orchestrator level too — a securityContext block on the Pod/container spec that Kubernetes checks at container-start time, independent of (and as a backstop for) whatever the Dockerfile's own USER instruction says:
Docker Compose — Local Multi-Container Development
CI/CD Integration
Scanning before pushing — not after — means a critical vulnerability blocks the image from ever reaching the registry, rather than being caught later by a separate, disconnected scanning job after it's already deployable.

