A container is just a normal process running on the host, wrapped in isolation so it thinks it has its own machine — its own filesystem, network, and process tree. Bundled with it are all the dependencies our app needs. In simple language — it’s our app plus everything it needs to run, sealed in a box that behaves the same on any machine.
The why is the oldest complaint in software: “works on my machine”. Different OS, different library versions, a missing system package — and the app that ran fine on our laptop face-plants in production. A container ships the app and its exact environment together, so “my machine” and “the server” become identical boxes.
Container vs VM
The classic interview question. Both isolate workloads, but at very different depths.
A virtual machine virtualizes hardware. Each VM runs a full guest OS (its own kernel) on top of a hypervisor. Heavy — gigabytes, and tens of seconds to boot.
A container virtualizes the operating system. All containers share the host’s kernel and are isolated by kernel features (namespaces for “what you can see”, cgroups for “how much you can use”). No guest OS per container. So they’re tiny — megabytes — and start in milliseconds.
Think of it like this: a VM is building a whole separate house next door; a container is a locked, private room inside the same house — sharing the plumbing (the kernel) but with its own door.
Trade-off in one line: VMs give stronger isolation (separate kernels), containers give far more speed and density. Most backend services want containers.
Image vs container
Two words people mix up constantly. The relationship is simple:
- An image is the blueprint — a read-only, packaged snapshot of our app + its dependencies. Static, stored, shareable.
- A container is a running instance of an image. Live, has its own writable layer on top.
Think classes vs objects: the image is the class, the container is the object we instantiate from it. One image → many identical containers.
Dockerfile layers & caching
We build an image from a Dockerfile — a recipe of instructions. The key insight for interviews: each instruction creates a layer, and Docker caches layers. On a rebuild, if an instruction and everything above it are unchanged, Docker reuses the cached layer instead of redoing the work.
This is why instruction order matters so much. Put things that rarely change first and things that change often last. Classic example — copy the dependency manifest and install deps before copying our source code, so a one-line code change doesn’t bust the (slow) dependency-install cache.
FROM node:20-alpine
WORKDIR /app
# deps first — this layer is cached until package.json changes
COPY package*.json ./
RUN npm ci
# source last — changing code only rebuilds from here down
COPY . .
CMD ["node", "index.js"]
If we’d copied COPY . . before npm ci, every code edit would invalidate the cache and reinstall every dependency. Slow builds, all self-inflicted.
Multi-stage builds, briefly
Sometimes we need heavy tooling to build (compilers, dev dependencies) but none of it to run. A multi-stage build uses one stage to build and a second, clean stage that copies only the finished artifact — so the final image stays tiny and has no build tools to attack.
# stage 1: build with the full toolchain
FROM node:20 AS build
WORKDIR /app
COPY . .
RUN npm ci && npm run build
# stage 2: ship only the output on a slim base
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
Smaller image = faster deploys, less to download, smaller attack surface. Win, win, win.
Practical takeaway
A container is an isolated process sharing the host kernel, bundled with its dependencies — lighter and faster than a VM, which runs a whole guest OS. An image is the blueprint, a container is a running instance of it. Order Dockerfile instructions least-changing first so layer caching skips the slow steps, and reach for a multi-stage build to keep build tools out of the final image.