Docker Fundamentals

From Linux namespaces to production-ready images - the core concepts every container engineer needs.

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:

┌─────────────────────────────────────────────────────────────────┐
│  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                           │
└─────────────────────────────────────────────────────────────────┘
AspectVMContainer
IsolationSeparate kernel per VMShared kernel, namespace boundaries
StartupSeconds to minutesMilliseconds
SizeGBs (full OS)MBs (app + minimal runtime)
DensityTens per hostHundreds per host
Best forMulti-tenant, mixed OS, strong isolationMicroservices, CI/CD, cloud-native apps
When to choose which: Use VMs when you need a different OS, kernel version, or hardware-level isolation. Use containers when you want fast, dense, reproducible application packaging on a shared Linux (or Windows container) host.

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

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...
Immutable tags: Pin images by digest in production (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

InstructionPurpose
FROMBase image; starts a new build stage
WORKDIRSet working directory for subsequent instructions
COPY / ADDCopy files from build context; prefer COPY
RUNExecute command during build; creates a layer
ENV / ARGRuntime env vars vs build-time variables
EXPOSEDocument which port the container listens on
USERRun subsequent commands and the container as this user
ENTRYPOINTFixed executable; defines what the container is
CMDDefault 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.

DriverScopeUse case
bridgeSingle hostDefault; isolated containers on a virtual bridge
hostSingle hostContainer uses host network stack directly
noneSingle hostNo networking; fully isolated
overlayMulti-hostSwarm mode service discovery across nodes
macvlanSingle hostContainer 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.

MechanismManaged byBest for
volumeDockerProduction data, databases, shared storage
bind mountHost pathDev hot-reload, config files on known host paths
tmpfsMemorySensitive 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
Data lifecycle: Removing a container does not remove its named volumes. Use docker volume rm or docker volume prune explicitly. Bind mounts are never deleted by Docker.

8. Best Practices

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

TaskCommand
Build imagedocker build -t name:tag .
Run detacheddocker run -d --name app -p 8080:80 image:tag
Follow logsdocker logs -f app
Shell into containerdocker exec -it app sh
List imagesdocker images
Prune unuseddocker system prune -a
Create networkdocker network create mynet
Create volumedocker volume create myvol

Full cheat sheet: Docker CLI Cheat Sheet