1. Solving "It Works on My Machine"
Historically, deploying software required ensuring the target production server had identical system libraries, runtime versions, environment variables, and configuration paths as the developer’s workstation. A minor discrepancy in an installed C shared library (e.g. glibc 2.31 vs 2.35) caused catastrophic runtime segfaults.
Containers bundle the application executable alongside its exact filesystem dependencies—every library, binary, and config file—into a single immutable, portable artifact that runs identically anywhere a Linux kernel exists.
2. Containers vs. Virtual Machines
A Virtual Machine runs a complete guest operating system—including its own dedicated kernel, memory manager, device drivers, and system services—on top of a hypervisor. This introduces heavy CPU/RAM overhead and multi-minute boot times.
A Container is simply a regular Linux host process that shares the host operating system’s kernel. Because there is no guest kernel to boot, containers instantiate in milliseconds and consume virtually zero idle memory overhead.
3. Linux Kernel Primitives: Namespaces and cgroups
Docker did not invent container technology; it built an ergonomic user experience around two foundational Linux kernel features:
- Namespaces (Isolation): Determines what a process can see. Namespaces isolate process IDs (PID), network interfaces and routing tables (NET), filesystem mount points (MNT), inter-process communication (IPC), hostnames (UTS), and user mappings (USER).
- Control Groups (cgroups): Determines what a process can use. Enforces strict hardware resource quotas on CPU shares, physical RAM limits, disk I/O bandwidth, and network priority.
4. OverlayFS & Copy-on-Write (CoW) Layers
Container images are composed of stacked read-only filesystem layers managed by OverlayFS.
When a container launches, Docker places a thin, mutable Read-Write container layer on top of the immutable image layers. If the container modifies an existing configuration file, OverlayFS copies the file up to the read-write layer (Copy-on-Write) before editing it. When the container is destroyed, that thin read-write layer is deleted, leaving the underlying base image pristine.
5. Writing Production Dockerfiles
A Dockerfile contains procedural instructions for compiling an image:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]
6. Multi-Stage Build Optimization
To keep production images tiny and secure, use multi-stage builds. Compile your code inside a heavy build stage equipped with compilers and SDKs, and copy only the compiled binary into a lightweight minimal production image:
# Stage 1: Build binary in Go
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /bin/app
# Stage 2: Minimal final runtime image (under 15MB!)
FROM alpine:3.19
COPY --from=builder /bin/app /bin/app
USER nobody
ENTRYPOINT ["/bin/app"]
7. Volumes and Bridge Networking
Containers are ephemeral by default—when a container stops, data written to its local filesystem vanishes. To persist database records, attach a Docker Volume:
$ docker run -d -v postgres_data:/var/lib/postgresql/data postgres:16
8. Multi-Container Orchestration with Docker Compose
Modern applications involve web servers, background workers, and databases working together. docker-compose.yml coordinates these dependencies declaratively:
services:
web:
build: .
ports:
- "8080:8080"
environment:
- DATABASE_URL=postgres://root:secret@db:5432/app
depends_on:
- db
db:
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata: