Docker Fundamentals: Images, Containers, Volumes, and Networks

A practical primer on the building blocks of containerization, with production patterns.

By Admin · 5/28/2026

Stylized Docker whale and container stack

Docker Fundamentals: Images, Containers, Volumes, and Networks

Docker engine architecture: CLI talks to Daemon, which pulls and pushes to a Registry

Docker is the de-facto packaging format for modern services. Almost every CI runner, every Kubernetes pod, and every PaaS-style "git push to deploy" experience under the hood is built on Linux containers — and Docker is still the most ubiquitous tool for producing and running them.

To use it well, you need a clear mental model of four building blocks: images, containers, networks, and volumes. Most of the operational issues you will see in production come from confusing one for another, so it is worth slowing down and getting this right.

Why containers exist at all

Before containers, deploying a Node.js, Python, or Java service to a fresh Linux box meant installing a runtime, pinning its version, installing system libraries, copying source code, configuring a process manager, opening firewall ports, and hoping the box you tested on matches the box you are deploying to. Configuration drift was the rule, not the exception.

Containers turn that whole sequence into a single artifact: an image. The image carries the runtime, libraries, code, and configuration. Anywhere you can run a container, the image runs identically. That property — bit-for-bit reproducibility across environments — is what unlocked microservices, ephemeral CI runners, and immutable infrastructure.

Images vs containers

An image is an immutable, layered filesystem snapshot. A container is a running instance of an image with its own writable layer, process tree, and network namespace.

A useful analogy: an image is the class, a container is the instance. You can launch many containers from one image, and each container's writes go into its own thin overlay layer that disappears when the container is removed.

Layers are the reason builds are fast. Each instruction in your Dockerfile produces a layer; if the inputs to a layer have not changed, Docker reuses the cached one. Order your Dockerfile so the slow, rarely-changing steps (installing dependencies) come before the fast, frequently-changing steps (copying source code).

A minimal Dockerfile

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

Build and run:

docker build -t myapp:1.0 .
docker run -d -p 3000:3000 --name myapp myapp:1.0

Multi-stage builds

The image above ships with the Node toolchain even though only node is needed at runtime. Multi-stage builds let you compile in one stage and copy only the artifacts you need into a tiny runtime stage:

FROM node:20 AS build
WORKDIR /src
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

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

The result is often 60–80% smaller, which matters because image size directly affects pull time, cold-start latency, and attack surface.

How image layers actually work

Docker image layer model: image is read-only stack of layers, container adds writable layer on top, volumes persist outside

Every Dockerfile instruction that produces a filesystem change creates a new layer. Layers are stacked using a Union Filesystem (overlay2 on modern Linux), which presents the merged view as a single root filesystem to the container.

The diagram above shows the relationship between three things people often confuse: the image, the container, and a volume.

The image (left) is a stack of immutable, content-addressed layers. FROM brings in the base layers; each subsequent COPY, RUN, or ADD adds another. Because layers are content-addressed (identified by a hash of their contents), Docker can skip a layer's rebuild whenever its inputs have not changed — that is the entire mechanism behind build caching.

The container (right) is the same image plus one extra writable layer on top. Anything the running process writes — log files, scratch directories, runtime caches — lands in that writable layer. When you docker rm the container, the writable layer is deleted. The image underneath is untouched and the next docker run produces a fresh container with a clean writable layer.

A volume (bottom right) sidesteps the writable layer entirely. The container sees a regular filesystem path, but the bytes live on a host directory or a Docker-managed volume that persists across container deletions and recreations. This is how databases, uploads, and certificates survive deploys.

Three layer-related habits separate slow, bloated images from fast, lean ones:

  • Order Dockerfile instructions from least to most volatile. Base image and dependency installation rarely change; copy your application source last. Done right, a typical "edit one file" rebuild reuses the dependency layer and rebuilds in seconds.
  • Use .dockerignore to keep .git, node_modules, build artifacts, and secrets out of the build context. Anything in the build context that you do not need is wasted bandwidth between client and daemon and a security risk if it ends up in a layer.
  • Adopt BuildKit (the default in modern Docker). It parallelizes independent stages, caches dependency installs across builds via mount caches, and supports inline secret mounts — RUN --mount=type=secret,id=npmrc npm ci keeps the secret out of the layer.

Once the layer model clicks, most of Docker's "magic" stops feeling magical and the optimization techniques follow naturally.

Volumes — keeping state outside containers

Containers are ephemeral. The writable layer disappears when you docker rm them. To persist data — databases, uploads, certificates, anything you cannot afford to lose on restart — mount a volume:

docker volume create pgdata
docker run -d --name pg -v pgdata:/var/lib/postgresql/data postgres:16

There are two main kinds of mounts:

  • Named volumes (-v pgdata:/var/lib/postgresql/data) are managed by the Docker engine. Use them for databases and any data you want to keep across container recreations.
  • Bind mounts (-v $(pwd):/app) map a host path into the container. They are perfect for development (source-code hot reload) and for surfacing log directories, but they couple you to the host filesystem.

A common production mistake is forgetting that an anonymous volume is created when an image VOLUME directive points at a path you did not explicitly mount. You end up with a sprawl of docker volume ls entries that nobody knows what to do with.

Networks

By default Docker creates a bridge network per Compose project so containers can reach each other by service name. docker run without Compose attaches to the default bridge, which does not offer DNS-based discovery — a frequent source of "but it works locally" head-scratching.

Best practice: create explicit user-defined bridge networks per app and attach related containers to them. Containers on the same user-defined network can resolve each other by container name.

docker network create app-net
docker run -d --network app-net --name api myapi:1.0
docker run -d --network app-net --name worker myworker:1.0
# worker can now reach api at http://api:3000

Compose for local development

For anything beyond a single container, write a docker-compose.yml. It declares services, networks, and volumes in one file:

services:
  api:
    build: .
    ports: ["3000:3000"]
    environment:
      DATABASE_URL: postgres://app:app@db:5432/app
    depends_on: [db]
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app
      POSTGRES_DB: app
    volumes: [pgdata:/var/lib/postgresql/data]
volumes:
  pgdata:

docker compose up brings the whole stack up; docker compose down -v tears it down including volumes.

Production checklist

Once your app boots in a container, the next concerns are security, size, and supportability.

  • Pin base image tags. node:20-alpine is fine; node:latest is a footgun.
  • Run as a non-root user. Add USER node (or create a dedicated user) so a container breakout does not give an attacker root-like access to anything mounted.
  • Use multi-stage builds to keep images small and to keep the build toolchain out of production.
  • Add a HEALTHCHECK so the runtime can detect a wedged process and restart it.
  • Scan images with Trivy or Grype before promoting them to production.
  • Set resource limits (--memory, --cpus) so a runaway container cannot starve its neighbors.
  • Centralize logs. Containers should log to stdout/stderr; let the runtime ship them to your log aggregator instead of writing to disk.

Common pitfalls

A few mistakes show up over and over in code reviews:

  • Copying node_modules into the image instead of running npm ci inside it. The host's binaries may not match the container's libc.
  • Using COPY . . without a .dockerignore — every .git blob and node_modules/ directory ends up in the image.
  • Hard-coding configuration in the Dockerfile. Use environment variables and read them at runtime; the same image should run in dev, stage, and prod.
  • Building from a Linux distribution and then mixing in a different libc later (alpine uses musl, debian uses glibc — native modules compiled against one will not load against the other).

Container security and runtime hardening

A running container is a process tree wrapped in Linux namespaces and cgroups. Containment is real, but it is not a security boundary as strong as a virtual machine — the kernel is shared between every container on the host. A few habits significantly reduce the blast radius of a compromised container:

  • Drop Linux capabilities you do not use. Run with --cap-drop=ALL --cap-add=NET_BIND_SERVICE (or whichever specific caps you need). Most apps need none.
  • Read-only root filesystem. --read-only plus --tmpfs /tmp blocks the most common post-exploitation pattern of writing a binary into the container and executing it.
  • No privileged mode. --privileged disables almost every isolation mechanism. Use specific device mounts and capabilities instead.
  • Use a non-root user. Add USER nonroot in the Dockerfile or run with --user 1000:1000. A container breakout with UID 0 inside the container can escalate further; with a high non-zero UID, the attacker hits standard Linux permission walls.
  • Pin dependency versions and rebuild often. A six-month-old image with critical CVEs is a more common breach vector than novel zero-days. Continuous rebuild + scan + redeploy keeps you on patched layers.
  • Sign and verify images. Tools like cosign (Sigstore) attach signatures to images in your registry. Production runtime can be configured to refuse images that do not chain to a trusted signer.

These are not exotic measures; they are equivalent to closing your front door and locking your car. Each one removes a category of attack.

Inspecting a running container

Containers can feel opaque until you remember they are just Linux processes in a namespace. The most useful day-to-day commands:

docker ps                       # what is running
docker logs -f myapp            # follow stdout/stderr
docker exec -it myapp sh        # interactive shell inside the container
docker inspect myapp            # full JSON: env, mounts, networks, IPs
docker stats                    # live CPU/mem/IO per container
docker top myapp                # processes inside the container
docker diff myapp               # files changed in the writable layer

docker exec is the one that surprises new users most: any command works, including apt update or pip install, but those changes live in the writable layer and disappear when the container is removed. Treat that as a debugging convenience, never as a way to "patch" production.

Where to go next

Once you are comfortable with the four building blocks, the natural next steps are an orchestrator (Kubernetes for HA, ECS or Nomad if you want lighter weight), a registry strategy (signed images, vulnerability scanning, retention policy), and a CI pipeline that builds, scans, and pushes images on every merge.

A practical rule: do not adopt the next tool in your stack until you can explain, on a whiteboard, what containers, images, layers, volumes, and networks each are and how they relate. The teams that move fastest with Kubernetes are the ones whose engineers were already comfortable with raw Docker. Skipping that step does not save time; it just defers the learning to a louder, costlier moment.

Master images, containers, volumes, and networks first. Every other tool — Compose, Kubernetes, CI runners, BuildKit — is built on top of them, and time spent on the fundamentals pays back every time you have to debug a flaky deployment at 2am.

Topic cluster

More docker Articles

Latest related posts connected by shared tags.

Continue learning

Related internal resources

Jump deeper with documentation, cheat sheets, and the full roadmap.