How Does Docker Work: Images, Containers and Layers Explained for Web Developers

Short answer: Docker works by packaging your application and everything it needs into a read-only image, then starting that image as an isolated process on your machine called a container. Instead of booting a whole operating system like a virtual machine does, Docker reuses the host’s Linux kernel and uses kernel features (namespaces, cgroups and a union filesystem) to give that process its own private view of the filesystem, network and processes. That is why a container starts in milliseconds while a VM takes minutes.

If that sentence raised more questions than it answered, good. This guide unpacks every piece of it in plain English, with short Dockerfile snippets and a complete walkthrough of containerising a simple Node.js app. No prior DevOps experience required.

The mental model: kitchen, recipe and meal

Before the technical details, here is the analogy that makes everything else click:

  • Dockerfile = the recipe. A plain text file of instructions.
  • Image = the meal-kit box. Frozen, read-only, shippable, identical for everyone who receives it.
  • Container = the meal on the plate. A running instance created from the box, which you can eat, season, or throw away.
  • Registry (Docker Hub, GitHub Container Registry, AWS ECR) = the warehouse where meal-kit boxes are stored and shared.

You can create a thousand plates from the same box. Deleting a plate does not touch the box. That single idea explains 80% of Docker’s behaviour.

docker containers

What Docker actually is (and what it is not)

Docker is an open platform for building, shipping and running applications inside containers. Practically speaking, Docker is made of a few moving parts:

Component Role
Docker CLI The commands you type: docker build, docker run, docker ps. It just sends API requests.
Docker daemon (dockerd) The background service that receives those requests and does the real work: building images, pulling layers, creating containers.
containerd + runc The lower-level runtime pieces that actually talk to the Linux kernel and spawn the isolated process.
Images and layers The read-only building blocks stored on disk and in registries.
Registry Remote storage for images so teams and servers can pull the exact same artifact.

What Docker is not: it is not a virtual machine, not a programming language, and not a replacement for your code. It is a packaging and isolation tool.

docker containers

How does Docker work under the hood? Step by step

Here is exactly what happens when you type a simple command like docker run -p 3000:3000 my-api:

  1. The CLI sends a request. The docker command is a thin client. It converts your command into an HTTP call to the Docker daemon over a local socket.
  2. The daemon looks for the image locally. If my-api is not on disk, it contacts the registry and downloads the image layer by layer. Layers you already have are skipped.
  3. The daemon assembles the filesystem. All the read-only layers are stacked into a single unified view using a union filesystem (usually OverlayFS).
  4. A thin writable layer is added on top. This is the container layer. Everything the container writes at runtime lands here, not in the image.
  5. The kernel creates isolation. Namespaces give the process its own PID table, network stack, mount points and hostname. Cgroups cap how much CPU and memory it can consume.
  6. Networking is wired up. A virtual network interface is attached to Docker’s bridge network, and your -p 3000:3000 flag adds a port forwarding rule from the host to the container.
  7. The entrypoint process starts. The command defined by CMD or ENTRYPOINT runs as PID 1 inside the container. When that process exits, the container stops.

No operating system boots. No hypervisor. Your app is simply a normal Linux process wearing a very convincing disguise.

The three kernel features that make containers possible

1. Namespaces = what the process can see

Namespaces partition kernel resources. A container gets its own PID namespace (so your Node process thinks it is PID 1 and cannot see host processes), its own network namespace (its own IP and ports), its own mount namespace (its own root filesystem), and more. Isolation of visibility.

2. Control groups (cgroups) = what the process can use

Cgroups limit and account for resource usage. That is how docker run --memory=512m --cpus=1.5 works. Isolation of consumption.

3. Union filesystem = how layers are stacked

OverlayFS merges multiple directories into one. The image layers are the lower, read-only directories. The container layer is the upper, writable one. Isolation of storage, with massive reuse between containers.

Image vs container: the difference that trips up every beginner

If you come from object-oriented programming, this is the cleanest way to see it: an image is a class, a container is an instance of that class.

Aspect Image Container
State Static, read-only Running (or stopped) instance with a writable layer
Created by docker build or docker pull docker run or docker create
Quantity One image on disk Unlimited containers from that one image
Persistence Permanent until deleted Disposable, data lost unless you mount a volume
Listed with docker images docker ps -a

Key consequence: if you write a file inside a running container and then delete the container, that file is gone. The image never changed. To keep data, you mount a volume or a bind mount, which maps a directory from the host into the container and bypasses the layered filesystem entirely.

docker containers

How the layered filesystem works

An image is not one big blob. It is a stack of layers, each one a compressed diff of the filesystem, identified by a content hash. Every instruction in a Dockerfile that changes the filesystem creates a new layer.

FROM node:22-alpine        # Layer 1: base OS + Node runtime
WORKDIR /app               # Metadata only
COPY package*.json ./      # Layer 2: two small files
RUN npm ci --omit=dev      # Layer 3: node_modules
COPY . .                   # Layer 4: your source code
CMD ["node", "server.js"]  # Metadata only

Three things follow from this design, and they matter enormously in daily work:

1. Layers are shared between images

If five of your projects all start with FROM node:22-alpine, that base layer is stored once on disk and once in the registry. Pulling the fifth image only downloads the layers you do not already have. This is why Docker images feel far lighter than their nominal size suggests.

2. Layers are cached during builds

During docker build, Docker checks each instruction against the cache. If the instruction and its inputs are unchanged, it reuses the existing layer and prints CACHED. The moment one layer is invalidated, every layer after it is rebuilt.

That is the entire reason we copy package.json before copying the source code. Source code changes on every commit, dependencies rarely do. Put the stable things first and the volatile things last:

# SLOW: any code change reinstalls all dependencies
COPY . .
RUN npm ci

# FAST: npm ci is cached until package-lock.json changes
COPY package*.json ./
RUN npm ci
COPY . .

3. Deleting a file in a later layer does not shrink the image

Layers are additive. If layer 3 adds a 500 MB tarball and layer 4 deletes it, the tarball is still inside layer 3 and still shipped with the image. The union filesystem just hides it with a “whiteout” marker. Download, extract and delete must happen in the same RUN instruction, or better, use a multi-stage build (see below).

Copy-on-write in one sentence

When a container modifies a file that lives in a read-only layer, the union filesystem copies that file up into the container’s writable layer first, then edits the copy. The original layer is never touched, which is exactly why one image can safely back hundreds of containers at the same time.

Walkthrough: containerising a simple Node.js app

Concepts stick when you run them. Here is the smallest useful example, start to finish.

Step 1: the application

server.js:

const http = require('http');

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ message: 'Hello from inside a container', pid: process.pid }));
});

server.listen(3000, '0.0.0.0', () => console.log('Listening on 3000'));

Important detail: bind to 0.0.0.0, not 127.0.0.1. Inside the container, localhost means the container itself, so the host would never reach your app. This is the single most common “why can I not open my app in the browser” bug.

Step 2: the Dockerfile

FROM node:22-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --omit=dev

COPY . .

ENV NODE_ENV=production
EXPOSE 3000

USER node
CMD ["node", "server.js"]

Line by line:

  • FROM picks the starting layer stack. The alpine variant is a minimal Linux distribution, typically around 50 MB instead of 350 MB.
  • WORKDIR sets the working directory inside the container filesystem and creates it if needed.
  • COPY moves files from your build context into the image.
  • RUN executes a command at build time and freezes the result into a layer.
  • ENV sets environment variables available to the process.
  • EXPOSE is documentation only. It does not publish anything; -p does that.
  • USER drops root privileges, a small change with a big security payoff.
  • CMD defines what runs at container start time. Use the JSON array form so signals like Ctrl+C reach your process correctly.

Step 3: a .dockerignore file

node_modules
npm-debug.log
.git
.env
Dockerfile
*.md

Everything in your project folder is sent to the daemon as the “build context” before the build starts. Excluding node_modules and .git often cuts build time dramatically and prevents your host’s native modules from leaking into a Linux image.

Step 4: build, run, inspect

docker build -t my-api:1.0 .
docker run -d -p 3000:3000 --name api my-api:1.0
curl http://localhost:3000
docker logs -f api
docker exec -it api sh
docker stop api && docker rm api

Try running docker build twice in a row. The second run finishes almost instantly and every step prints CACHED. Now change one character in server.js and build again: only the final COPY . . layer is rebuilt. You have just watched layer caching work.

Inside docker exec -it api sh, run ps aux. You will see your Node process as PID 1 and almost nothing else. That is the PID namespace doing its job.

Step 5: a multi-stage build for production

Multi-stage builds let you compile in a fat image and ship a thin one. Only the final stage becomes your image:

FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]

Build tools, dev dependencies, source maps and test files stay in the first stage and never reach production. It is common to cut image size by 60% or more this way.

Step 6: add a database with Docker Compose

One container is rarely enough. Compose describes a whole stack in one file:

services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://app:secret@db:5432/app
    depends_on:
      - db
  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

Run docker compose up and both containers start on a shared private network where each service is reachable by its name. Notice the host in DATABASE_URL is db, not an IP address: Docker’s embedded DNS resolves service names automatically. The named volume pgdata keeps your database intact even if you destroy and recreate the container.

docker containers

Why containers are lighter than virtual machines

A virtual machine virtualises hardware. The hypervisor emulates CPU, memory and disks, then a complete guest operating system boots on top, with its own kernel, init system, drivers and background services. Even an idle VM reserves gigabytes of RAM. What is a Container is a useful companion to this.

A container virtualises the operating system. There is no guest kernel and no boot sequence. Your app is a process on the host kernel, fenced in by namespaces and cgroups. The image only contains the userland files your app needs: libraries, runtime, binaries, your code.

Criterion Docker container Virtual machine
Kernel Shares the host kernel Ships its own guest kernel
Typical size 10 MB to 500 MB 1 GB to 20 GB
Startup time Milliseconds to a couple of seconds Tens of seconds to minutes
Density per host Dozens to hundreds A handful to a few dozen
Isolation strength Process level, strong but shared kernel Hardware level, strongest
Runs a different OS kernel No (Linux containers need a Linux kernel) Yes
Best for Apps, microservices, CI jobs, dev environments Full OS workloads, strict multi-tenant isolation

What about Docker Desktop on Mac and Windows?

Containers need a Linux kernel. On macOS and Windows, Docker Desktop quietly runs a small, optimised Linux VM and places your containers inside it. So there is one VM on your laptop, shared by every container, instead of one VM per application. The efficiency argument still holds, and on Linux servers there is no VM at all.

Common beginner mistakes to avoid

  • Expecting data to persist. Containers are disposable. Use volumes for databases, uploads and anything you care about.
  • Copying source code before installing dependencies. It destroys your build cache on every commit.
  • Using the latest tag in production. It is a moving target. Pin versions such as node:22-alpine so builds stay reproducible.
  • Baking secrets into images. Anyone who pulls the image can read every layer. Pass secrets at runtime via environment variables, secret mounts or a secrets manager.
  • Running everything as root. Add a USER instruction.
  • Forgetting -p. EXPOSE documents a port, it does not publish it.
  • Letting disk fill up. Old images, stopped containers and dangling build cache add up fast. Run docker system prune periodically and docker system df to see what is consuming space.
docker containers

Is Docker still worth learning in 2026?

Yes, and the reason is standardisation rather than hype. The OCI image format that Docker popularised is now the universal unit of deployment: Kubernetes, serverless container platforms, CI/CD runners, edge runtimes and most PaaS providers all consume the same artifact you build with a Dockerfile. Alternative build tools and runtimes exist (Podman, BuildKit-based builders, Buildpacks, nerdctl), but they all speak the same image format and read the same Dockerfile syntax. Learning how images, layers and containers work is therefore transferable knowledge, not a vendor lock-in.

For a web developer, the practical payoff is immediate:

  1. New team members run docker compose up and have the full stack working in minutes.
  2. “Works on my machine” stops being a debate because the runtime is part of the artifact.
  3. CI pipelines test in the exact environment that will run in production.
  4. Rolling back is as simple as redeploying the previous image tag.

Frequently asked questions

Is Docker just a VM?

No. A VM virtualises hardware and boots a complete guest operating system with its own kernel. A container is an isolated process that shares the host kernel, which is why it starts in milliseconds and weighs megabytes instead of gigabytes. On macOS and Windows a single lightweight Linux VM hosts all your containers, but that is an implementation detail of Docker Desktop, not of containers themselves.

Is Docker difficult to learn?

The core is small: a Dockerfile, docker build, docker run and volumes will cover most day-to-day web development work, and you can be productive after an afternoon. The difficulty curve appears later with networking between services, multi-stage optimisation, security hardening and orchestration. Learning the layer model early, as you did in this article, removes most of the confusion that beginners hit.

Why do some people say they are moving away from Docker?

Usually they mean moving away from one specific component, not from containers. Kubernetes dropped the Docker-specific shim in favour of containerd, some teams prefer Podman for its daemonless and rootless design, and others let Buildpacks or their cloud platform build images automatically. In all of those cases they still build and run OCI container images. The concepts in this guide remain valid regardless of the tool.

What is the difference between a Docker image and a container, in one sentence?

An image is the read-only, immutable template stored on disk, and a container is a running instance of that template with a thin writable layer added on top.

Do containers lose their data when they stop?

Stopping a container keeps its writable layer, so the data is still there when you restart it. Removing the container deletes that writable layer and everything in it. Mount a named volume or a bind mount for any data that must survive.

How do I make my Docker image smaller?

Use a slim or alpine base image, use multi-stage builds so build tools never reach the final stage, combine related RUN commands and clean caches in the same instruction, add a thorough .dockerignore, and install production dependencies only. For the wider picture, see Docker: Explained Simply for a 10-Year-Old.

Does Docker work on Windows and macOS?

Yes. Docker Desktop supports both, using WSL 2 on Windows and a managed Linux VM on macOS, including Apple Silicon. Be aware of CPU architecture: an image built on an ARM Mac will not run on an x86 server unless you build a multi-architecture image with docker buildx.

Key takeaways

  • A Dockerfile is a recipe, an image is the immutable result, a container is a running instance of that image.
  • Images are stacks of read-only layers that are cached, shared and deduplicated, which makes builds fast and pulls cheap.
  • Running a container means the kernel isolates a process with namespaces and cgroups, then presents a merged filesystem plus one thin writable layer.
  • Containers are lighter than VMs because they share the host kernel instead of booting their own.
  • Order your Dockerfile from least to most frequently changing, keep state in volumes, and never ship secrets inside an image.

Start with the Node.js example above on a real project this week. Once you have watched the cache skip four layers in a row, the rest of the container ecosystem stops feeling like magic and starts feeling like tooling.