Docker solves a problem every developer knows intimately: it works on my machine. Containers bundle an application with its entire runtime environment — dependencies, system libraries, configuration — into one portable unit that runs identically on laptops, CI, and production. This post builds the working mental model: what images and containers really are, how layers make everything fast, and how to write Dockerfiles that don't embarrass you.
The core distinction: images are blueprints, containers are instances#
- Image: an immutable template — your app's files plus its runtime, frozen at a moment in time
- Container: a running (or stopped) process created from an image
The analogy that sticks: images are classes, containers are objects. One image spawns unlimited containers; deleting a container never touches its image.
docker build -t myapp . # image from Dockerfile
docker run -p 3000:3000 myapp # container from image
docker ps # running containers
Containers feel like VMs but aren't. They share the host's kernel; isolation comes from kernel namespaces (separate views of processes, network, filesystem) and cgroups (resource limits). That's why containers start in milliseconds where VMs take minutes — there's no guest OS to boot, just a sandboxed process.
The layer cache: why builds get fast#
Images stack in layers — roughly one per Dockerfile instruction — and each layer is cached and shared:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./ # layer 3: only rebuilt when deps change
RUN npm ci # layer 4: cached unless package files change!
COPY . . # layer 5: changes constantly
RUN npm run build
CMD ["node", "dist/server.js"]
When any layer's input changes, it and everything after rebuilds — but everything before stays cached. This ordering is why professionals put dependency installation before source code copying: editing your code re-runs COPY . . but skips the expensive npm ci. Reverse those two steps and every code edit triggers full dependency reinstall — the single most common Dockerfile mistake.
The instructions that matter daily#
| Instruction | Does | Gotcha |
|---|---|---|
FROM |
base image | pin versions (node:22, not latest) |
COPY / ADD |
files in | prefer COPY (ADD has magic) |
RUN |
execute at build time | chains with && share one layer |
CMD |
default command at run time | one per image, overridable |
EXPOSE |
documentation of port | doesn't publish anything |
.dockerignore |
excludes build context | first defense against fat builds |
The RUN/CMD distinction trips everyone once: RUN happens when building the image (baked in forever); CMD is what executes when a container starts. Also add .dockerignore immediately (node_modules, .git, build artifacts) — without it, COPY . . ships gigabytes you already have elsewhere.
Slimming images: multi-stage builds#
Build tools and dev dependencies bloat images enormously. Multi-stage builds compile in one stage and copy only artifacts into a clean final stage:
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]
Final image: no source, no compilers, no devDependencies — often 5–10x smaller. Smaller means faster deploys, smaller attack surface, less registry cost. Alpine bases shrink further (mind musl/glibc edge cases).
The workflow worth internalizing#
- Write minimal Dockerfile → 2.
.dockerignore→ 3. Order layers expensive-and-stable-first → 4. Multi-stage for anything compiled → 5. Run exactly what production runs locally
That last point is Docker's real payoff: your local container runs the same OS libraries, same Node version, same everything as deployment — eliminating an entire category of environment bugs. And it composes naturally into multi-service setups, where the same principles scale from one container to whole stacks.
Interview-ready summary: images = immutable layered templates, containers = isolated processes sharing host kernel, layer caching = rebuild only what changed, multi-stage = separate build-time from run-time. Four sentences that demonstrate you've actually used the tool.
Related: compose for multi-container apps · Kubernetes concepts · interview study plan