1. Containers vs Virtual Machines
A virtual machine (VM) runs a full guest operating system on a hypervisor. Each VM has its own kernel, drivers, and userland. VMs are strong isolation boundaries but heavy: gigabytes of disk, seconds to minutes to boot, and significant CPU overhead for the hypervisor.
A container is an isolated process (or process tree) on the host kernel. Linux kernel features provide the isolation:
- Namespaces control what a process can see: PID, network, mounts, hostname, IPC, and user ID mappings.
- cgroups (control groups) limit what a process can consume: CPU, memory, block I/O, and network bandwidth.
- Capabilities and seccomp restrict which privileged syscalls a container may invoke.
┌─────────────────────────────────────────────────────────────────┐
│ VM model │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Guest OS │ │ Guest OS │ │ Guest OS │ each with own kernel │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ └─────────────┴─────────────┘ │
│ Hypervisor (KVM, Hyper-V) │
│ Host OS + hardware │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Container model │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ App proc │ │ App proc │ │ App proc │ share host kernel │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ └─────────────┴─────────────┘ │
│ containerd + runc (namespaces + cgroups) │
│ Host OS + hardware │
└─────────────────────────────────────────────────────────────────┘
| Aspect | VM | Container |
|---|---|---|
| Isolation | Separate kernel per VM | Shared kernel, namespace boundaries |
| Startup | Seconds to minutes | Milliseconds |
| Size | GBs (full OS) | MBs (app + minimal runtime) |
| Density | Tens per host | Hundreds per host |
| Best for | Multi-tenant, mixed OS, strong isolation | Microservices, CI/CD, cloud-native apps |
2. Docker Architecture
Docker follows a client-server model. The CLI sends API requests; the daemon manages resources; the runtime actually starts processes.
docker CLI dockerd (daemon) container runtime
────────── ──────────────── ─────────────────
docker build ──REST──▶ manages images, containerd
docker run containers, └── runc (OCI runtime)
docker ps networks, volumes │
│ ▼
└── talks to registry Linux namespaces
(Docker Hub, ECR, + cgroups
GHCR, private)
Key components
- docker - the client CLI. Sends commands to the daemon via Unix socket (
/var/run/docker.sockon Linux) or TCP. - dockerd - the daemon. Builds images, pulls/pushes to registries, creates networks and volumes, orchestrates container lifecycle.
- containerd - industry-standard container runtime. Manages image transfer, container execution, and snapshots.
- runc - OCI-compliant low-level runtime that creates the container process with namespaces and cgroups.
- Registry - stores and distributes images (Docker Hub, Amazon ECR, Google Artifact Registry, GitHub Container Registry).
Container lifecycle
docker create # allocate filesystem + config, do not start
docker start # run the main process
docker stop # SIGTERM, grace period (default 10s), then SIGKILL
docker rm # delete container and writable layer
docker run # create + start in one step
A stopped container retains its writable layer until removed. Image layers are never modified by running containers.
3. Images & Layers
A Docker image is a read-only stack of filesystem layers. Each Dockerfile instruction that modifies the filesystem creates a new layer. Layers are content-addressable and shared across images - if two images share a base, it is stored once on disk.
When you run a container, Docker adds a thin writable container layer on top using a copy-on-write (CoW) filesystem (overlay2 on modern Linux).
# Image layers (read-only) Container (read-write top layer)
┌─────────────────────┐
│ Layer 4: CMD │ ◀── Dockerfile instruction
├─────────────────────┤
│ Layer 3: COPY app │
├─────────────────────┤
│ Layer 2: RUN npm ci│
├─────────────────────┤
│ Layer 1: FROM node │ ◀── base image
└─────────────────────┘
+
┌─────────────────────┐
│ Writable layer │ ◀── deleted when container is removed
└─────────────────────┘
Inspecting images
docker images
docker image history myapp:1.0 --no-trunc
docker inspect myapp:1.0 --format '{{.RootFS.Layers}}'
docker system df # disk usage summary
docker image prune -a # remove unused images
Image naming and tags
# Format: [registry/][namespace/]repository[:tag|@digest]
docker.io/library/nginx:latest
ghcr.io/myorg/api:v2.1.0
123456789.dkr.ecr.us-east-1.amazonaws.com/app@sha256:abc123...
image@sha256:...) so a tag retag cannot silently change what you deploy.
4. Dockerfile
A Dockerfile is a declarative recipe for building an image. Each instruction creates a cached layer.
# syntax=docker/dockerfile:1
FROM node:20-bookworm-slim AS base
WORKDIR /app
# Install dependencies first (cache-friendly)
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
# Copy application source
COPY src/ ./src/
COPY public/ ./public/
# Non-root user
RUN addgroup --system app && adduser --system --ingroup app app
USER app
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s \
CMD curl -f http://localhost:3000/health || exit 1
CMD ["node", "src/server.js"]
Common instructions
| Instruction | Purpose |
|---|---|
FROM | Base image; starts a new build stage |
WORKDIR | Set working directory for subsequent instructions |
COPY / ADD | Copy files from build context; prefer COPY |
RUN | Execute command during build; creates a layer |
ENV / ARG | Runtime env vars vs build-time variables |
EXPOSE | Document which port the container listens on |
USER | Run subsequent commands and the container as this user |
ENTRYPOINT | Fixed executable; defines what the container is |
CMD | Default arguments; overridden by docker run args |
CMD vs ENTRYPOINT
# Pattern: ENTRYPOINT is the binary, CMD is default args
ENTRYPOINT ["python", "app.py"]
CMD ["--port", "8080"]
# docker run myimage --debug → python app.py --debug
# docker run myimage python other.py → overrides ENTRYPOINT entirely
.dockerignore
# .dockerignore - exclude from build context (faster builds, smaller context)
node_modules
.git
.env
*.log
dist/
coverage/
Dockerfile*
Build commands
docker build -t myapp:1.0 .
docker build -t myapp:1.0 -f Dockerfile.prod .
docker build --build-arg NODE_ENV=production -t myapp:1.0 .
docker build --no-cache -t myapp:1.0 . # ignore layer cache
docker build --target build -t myapp:build . # stop at named stage
5. Docker CLI
The Docker CLI groups commands by resource type. These are the commands you will use daily.
Containers
docker run -d --name web -p 8080:80 --restart unless-stopped nginx:alpine
docker ps # running containers
docker ps -a # all containers
docker logs -f web --tail 100 # follow logs
docker exec -it web sh # interactive shell
docker inspect web # full JSON metadata
docker stop web && docker rm web # stop and remove
Images
docker pull nginx:1.25-alpine
docker tag myapp:1.0 registry.example.com/myapp:1.0
docker push registry.example.com/myapp:1.0
docker rmi myapp:1.0
docker save myapp:1.0 -o myapp.tar # export
docker load -i myapp.tar # import
Resource limits at run time
docker run -d \
--name api \
--cpus="1.5" \
--memory="512m" \
--memory-swap="512m" \
--pids-limit=100 \
myapp:1.0
Debugging
docker top web # running processes
docker stats web --no-stream # CPU/memory snapshot
docker diff web # filesystem changes vs image
docker events --filter container=web # real-time events
6. Networking
Docker provides several network drivers. The default bridge network works for single-host development; user-defined bridges and overlay networks support multi-container and multi-host scenarios.
| Driver | Scope | Use case |
|---|---|---|
bridge | Single host | Default; isolated containers on a virtual bridge |
host | Single host | Container uses host network stack directly |
none | Single host | No networking; fully isolated |
overlay | Multi-host | Swarm mode service discovery across nodes |
macvlan | Single host | Container gets a MAC address on physical network |
User-defined bridge (recommended for Compose and multi-container apps)
docker network create app-net
docker run -d --name db --network app-net \
-e POSTGRES_PASSWORD=secret postgres:16-alpine
docker run -d --name api --network app-net -p 8080:8080 myapp:1.0
# api can reach db by hostname: postgres://db:5432
Port publishing
# -p hostPort:containerPort
docker run -p 8080:80 nginx # bind 0.0.0.0:8080
docker run -p 127.0.0.1:8080:80 nginx # localhost only
docker run -P nginx # publish all EXPOSEd ports to random host ports
DNS and service discovery
On user-defined bridge networks, Docker provides embedded DNS. Containers resolve each other by --name or network alias. The default bridge network does not support automatic DNS between containers.
docker network inspect app-net
docker run --rm --network app-net nicolaka/netshoot ping -c 3 api
7. Volumes & Storage
Container filesystems are ephemeral by default. Data that must survive container restarts uses volumes or bind mounts.
| Mechanism | Managed by | Best for |
|---|---|---|
volume | Docker | Production data, databases, shared storage |
bind mount | Host path | Dev hot-reload, config files on known host paths |
tmpfs | Memory | Sensitive temp data, no disk persistence |
Named volumes
docker volume create pgdata
docker run -d --name db \
-v pgdata:/var/lib/postgresql/data \
postgres:16-alpine
# Inspect and backup
docker volume inspect pgdata
docker run --rm -v pgdata:/data -v $(pwd):/backup alpine \
tar czf /backup/pgdata-backup.tar.gz -C /data .
Bind mounts (development)
docker run -d \
-v /home/dev/myproject/src:/app/src:ro \
-v /home/dev/myproject/config.yml:/app/config.yml:ro \
myapp:dev
tmpfs (in-memory, no disk write)
docker run --tmpfs /run/secrets:size=10m,mode=1700 myapp:1.0
docker volume rm or docker volume prune explicitly. Bind mounts are never deleted by Docker.
8. Best Practices
- Pin base image versions - use
node:20-bookworm-slim, notnode:latest. - Minimize layers and image size - combine RUN commands, use slim or distroless bases, multi-stage builds for compiled apps.
- Order Dockerfile for cache hits - copy dependency manifests before source code.
- Run as non-root - set
USERafter installing dependencies. - One process per container - keeps logs, health checks, and scaling simple.
- Use .dockerignore - exclude
node_modules,.git, secrets, and build artifacts. - Add HEALTHCHECK - orchestrators and load balancers need a signal that the app is ready.
- Do not store secrets in images - inject at runtime via env vars, secrets mounts, or external secret stores.
- Scan images in CI - use
docker scout, Trivy, or Snyk before pushing to production registries. - Set resource limits - prevent a single container from starving the host.
Signal handling
PID 1 in a container must forward signals correctly. Use an init wrapper or ensure your app handles SIGTERM for graceful shutdown:
# Option 1: tini as entrypoint
FROM node:20-bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends tini
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["node", "server.js"]
# Option 2: exec form so node receives signals directly
CMD ["node", "server.js"]
9. Cheat Sheet
| Task | Command |
|---|---|
| Build image | docker build -t name:tag . |
| Run detached | docker run -d --name app -p 8080:80 image:tag |
| Follow logs | docker logs -f app |
| Shell into container | docker exec -it app sh |
| List images | docker images |
| Prune unused | docker system prune -a |
| Create network | docker network create mynet |
| Create volume | docker volume create myvol |
Full cheat sheet: Docker CLI Cheat Sheet