Intermediate 6 min · March 06, 2026

Dockerfile CMD Shell Form — Why SIGTERM Fails

Shell-form CMD makes your app PID 2, so Kubernetes SIGTERM hits the shell instead.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
Quick Answer
  • Each instruction creates a read-only layer — a filesystem diff on top of the previous layer
  • Docker caches layers sequentially — changing one layer invalidates all layers after it
  • Order instructions from least-likely-to-change to most-likely-to-change for optimal caching
  • Multi-stage builds let you use heavy toolchains during compilation and ship only the output
  • FROM: selects the base image (alpine for size, debian/ubuntu for compatibility)
  • RUN: executes commands during build — chain with && to reduce layer count
  • COPY: copies files into the image — prefer over ADD unless you need tar extraction
  • CMD vs ENTRYPOINT: CMD provides default args, ENTRYPOINT sets the fixed executable
  • ARG vs ENV: ARG is build-time only, ENV persists at runtime — never put secrets in either

Dockerfiles eliminate environment drift. A Dockerfile is a plain-text script that defines every dependency, runtime, and configuration your application needs. Docker reads it and builds an image — a portable, immutable snapshot that runs identically on any machine.

The layer caching mechanism is the single most important concept. Each instruction creates a cached layer. Changing one instruction invalidates all subsequent layers. Order your instructions from least-likely-to-change to most-likely-to-change to maximize cache hits during development.

Three misconceptions cause the most production issues: CMD without exec form silently breaks graceful shutdown in Kubernetes, ENV and ARG are visible in docker history (never put secrets in them), and .dockerignore is not optional (COPY . . without it bakes secrets and gigabytes of junk into the image).

How Docker Builds an Image — Layers Are Everything

Before you write a single Dockerfile instruction, you need a mental model of what Docker is actually doing when it reads your file. Docker doesn't build one monolithic blob. It builds a stack of read-only layers, one per instruction. Each layer is a diff — only the filesystem changes from that step.

Why does this matter? Because Docker caches every layer. If you rebuild an image and nothing changed in a particular step, Docker reuses the cached layer instead of running it again. This turns a 3-minute build into a 4-second build. But the cache is sequential — as soon as one layer is invalidated (because something changed), every layer after it is also invalidated and rebuilt from scratch.

This single insight drives the most important Dockerfile design decision you'll ever make: order your instructions from least-likely-to-change to most-likely-to-change. Your base OS almost never changes. Your system dependencies change occasionally. Your app's package dependencies change sometimes. Your source code changes constantly. Structure your Dockerfile in that order and you'll get near-instant cached rebuilds during development.

Think of layers like a stack of transparent slides on an overhead projector. Each slide adds something. You can swap out the top slide without reprinting all the slides beneath it.

Layer size and the cleanup-in-same-layer rule: Each RUN instruction creates a new layer. If you download a 200MB package in one RUN and delete it in the next RUN, the 200MB still exists in the first layer — layers are additive. The delete only adds a whiteout marker. Always chain download and cleanup in the same RUN with && to avoid bloating the image with phantom files.

The Instructions That Actually Matter — And What They're Really Doing

There are 18 Dockerfile instructions. In practice, you'll use about 10 of them regularly. Rather than listing all 18 mechanically, let's focus on the ones that cause confusion or have non-obvious behaviour — because those are the ones that bite you in production.

FROM is always first. It picks your starting layer. FROM scratch gives you an empty image — useful for compiled Go or Rust binaries. FROM node:20-alpine gives you Node on Alpine Linux, which is ~7MB versus ~180MB for Debian-based images. Prefer Alpine for production; prefer the fuller images when you need debugging tools.

RUN executes a shell command during the build. Each RUN creates a new layer. Chain related commands with && and clean up in the same RUN to avoid bloating the image with intermediate files that persist in a layer even after you delete them later.

COPY vs ADD: Use COPY almost always. ADD does extra magic — it auto-extracts tar archives and can fetch URLs — but that magic makes builds unpredictable. Use ADD only when you explicitly need its archive extraction feature.

ENV sets environment variables available at both build time and runtime. ARG sets variables available only at build time. Never put secrets in ENV — they're visible in docker inspect and image history. Use runtime secret injection instead.

ENTRYPOINT vs CMD: ENTRYPOINT sets the executable that always runs. CMD provides default arguments to it. When you run docker run my-image --verbose, that --verbose replaces CMD but gets appended to ENTRYPOINT. Together they let you build images that behave like CLI tools.

The HEALTHCHECK instruction: HEALTHCHECK tells Docker how to determine if the container's process is healthy. Without it, Docker only checks if the process is running — not if it is functional. A process that is running but deadlocked appears healthy. HEALTHCHECK runs a command periodically and marks the container as unhealthy if it fails. This is critical for orchestrators like Docker Swarm and Kubernetes that use health status for routing decisions.

Multi-Stage Builds — The Pattern That Separates Pros from Beginners

Here's a scenario every developer hits: you need a compiler or build tool to produce your application binary, but you don't need that compiler in the final image running in production. Shipping the compiler anyway means a larger attack surface, a bigger image pulling over the network, and slower startup times in Kubernetes.

Multi-stage builds solve this elegantly. You define multiple FROM blocks in one Dockerfile. Each FROM starts a fresh image context. You build your application in an early 'builder' stage that has all the tools, then you COPY only the compiled output into a final, minimal 'runtime' stage. The builder stage is discarded — it never ships.

This pattern is transformative for compiled languages. A Go application that builds in a 800MB image with all the Go toolchain can ship as a 12MB Alpine or even a 3MB scratch image containing just the binary. But it's equally powerful for JavaScript — build your React app with node_modules in one stage, then copy only the /dist folder into an nginx image.

The key instruction is COPY --from=builder. The name builder is just a label you assign with AS in the FROM line. You can have as many stages as you need, and any stage can copy from any previous stage. You can even reference external images as copy sources with --from=nginx:alpine.

Build-time secrets in multi-stage builds: Multi-stage builds are the correct pattern for handling build-time secrets. Put the secret in the builder stage (using BuildKit --mount=type=secret), use it during compilation, and the secret never appears in the final runtime stage. The builder stage is discarded, and with it any trace of the secret.

Targeting a specific stage: Use docker build --target <stage-name> to build up to a specific stage. This is useful for debugging — build the builder stage and inspect it without building the runtime stage: docker build --target builder -t debug . && docker run --rm -it debug sh.

Production-Ready Dockerfile — Putting It All Together

Knowing individual instructions is one thing. Knowing how they compose into a secure, efficient, production-grade Dockerfile is what makes the difference in a real project. There are four production concerns beyond 'does it build': image size, security, build speed, and signal handling.

Image size: use a minimal base, chain RUN commands, use multi-stage builds, and add a .dockerignore file — this is the most commonly forgotten file. Without it, COPY . . sends your entire project directory (including node_modules, .git, test fixtures) to the Docker build context, which can make builds take minutes before a single instruction executes.

Security: never run as root. Add a non-root user with RUN addgroup and adduser, then switch to it with USER. If an attacker compromises your app, running as a non-root user limits the blast radius significantly.

Signal handling: always use exec form ["executable", "arg"] for CMD and ENTRYPOINT — not shell form executable arg. Shell form wraps your command in /bin/sh -c, which means your process gets PID 2, not PID 1. Kubernetes and Docker send SIGTERM to PID 1 when stopping a container. If your app isn't PID 1, it never receives the signal and gets hard-killed after the timeout.

Build speed: everything from section one — order layers by change frequency, separate dependency manifests from source code.

The .dockerignore file in detail: The .dockerignore file excludes files from the build context before they are sent to the Docker daemon. Without it, the entire directory (including .git, node_modules, .env, test fixtures) is sent to the daemon, increasing build time and risking secret exposure. Common patterns to exclude: node_modules/, .git/, .env, .log, coverage/, __pycache__/, *.pyc, .dockerignore itself.

Shell Form vs Exec Form — Signal Handling and Process Management
AspectShell Form (RUN command arg)Exec Form (RUN ["command", "arg"])
SyntaxCMD node server.jsCMD ["node", "server.js"]
Process spawningRuns inside /bin/sh -c — your app is a child processRuns directly — your app IS the process
PID in containerYour app gets PID 2 or higherYour app gets PID 1
Signal handlingSIGTERM from Docker/K8s may not reach your appSIGTERM reaches your app directly — clean shutdown works
Shell features availableYes — variable expansion, pipes, &&No — must handle logic in the command itself
Best used forRUN instructions that need shell featuresCMD and ENTRYPOINT — always prefer this
RiskGraceful shutdown often silently brokenMinimal — this is the safe default

Key Takeaways

  • Docker builds images as a stack of cached layers — order your COPY and RUN instructions from least-to-most frequently changing, always copying dependency manifests before source code, to get near-instant cached rebuilds.
  • Always use exec form (JSON array syntax) for CMD and ENTRYPOINT — shell form wraps your process in /bin/sh -c, bumping it to PID 2 and silently breaking graceful shutdown in Docker and Kubernetes.
  • Multi-stage builds let you use a full toolchain (800MB) during compilation and ship only the compiled output (12MB) to production — the build stage is discarded and never pushed to a registry.
  • The .dockerignore file is mandatory, not optional — without it, COPY . . silently bakes node_modules, .git history, and .env files into your image; add it before you write your first COPY instruction.
  • Never put secrets in ENV or ARG — they are permanently visible in docker history. Use BuildKit --mount=type=secret for build-time secrets and secrets managers for runtime secrets.
  • A production Dockerfile has six mandatory checks: non-root USER, no secrets in ENV/ARG, .dockerignore, exec-form CMD, multi-stage build, and HEALTHCHECK.

Interview Questions on This Topic

  • QWhat's the difference between CMD and ENTRYPOINT, and can you give a real example of when you'd use both together in the same Dockerfile?
  • QIf I change one line in my source code and rebuild, which layers get rebuilt and why? How would you structure a Dockerfile to make that rebuild as fast as possible?
  • QWhat's the difference between ARG and ENV — and why should you never put a secret in either one? What's the correct alternative?
  • QExplain how multi-stage builds work. How would you use them to reduce a Go application's image from 800MB to 12MB?
  • QYour container takes 30 seconds to stop in Kubernetes instead of shutting down gracefully. Walk me through the debugging process and the most likely root cause.
  • QWhat is the purpose of the HEALTHCHECK instruction? What happens if you omit it in a Docker Swarm or Kubernetes deployment?

Frequently Asked Questions

What is the difference between a Dockerfile and a Docker image?

A Dockerfile is the source code — a plain-text instruction file you write and version control. A Docker image is the compiled artifact produced when Docker reads and executes that Dockerfile. The relationship is the same as source code to a compiled binary: you share the Dockerfile, Docker builds the image, and you run containers from the image.

How do I reduce the size of my Docker image?

The three highest-impact changes are: (1) use a minimal base image like Alpine instead of full Debian — this alone drops your base from ~180MB to ~7MB; (2) use multi-stage builds so your build tools and compiler never ship to production; (3) chain RUN commands with && and clean up package manager caches in the same RUN instruction so intermediate files don't persist in a layer.

Why does my container ignore SIGTERM and take 30 seconds to stop?

You're almost certainly using shell form for your CMD or ENTRYPOINT (e.g., CMD node server.js). This wraps your app in /bin/sh -c, making the shell PID 1 and your app PID 2. Docker sends SIGTERM to PID 1 (the shell), which doesn't forward it to your app. After the timeout, Docker sends SIGKILL. Fix it by switching to exec form: CMD ["node", "server.js"].

What is the difference between ARG and ENV?

ARG is available only during docker build — it does not exist in the running container. ENV is available at both build time and runtime. Neither should contain secrets — both are visible in docker history --no-trunc. For build-time secrets, use BuildKit --mount=type=secret. For runtime secrets, use Docker secrets or a secrets manager.

How do I debug a multi-stage build that produces unexpected output?

Use the --target flag to build only up to a specific stage: docker build --target builder -t debug . Then run the stage interactively: docker run --rm -it debug sh. Inspect the filesystem to verify files are where you expect. Then build the full image and compare.

🔥

That's Docker. Mark it forged?

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

Previous
Docker Images and Containers
7 / 18 · Docker
Next
Docker Volumes and Networking