Docker
Container runtime, image building, and orchestration
Overview
Docker is the container runtime that made Linux containers practical. It packages an application and its dependencies into an image that runs identically on any machine with a Docker-compatible runtime. The Docker daemon (dockerd) manages the container lifecycle: pulling images, creating containers, managing networks and volumes, and cleaning up.
Core Docker Engine
The open-source container runtime (CE — Community Edition). Runs on Linux natively. On macOS and Windows, Docker runs inside a lightweight Linux VM managed by Docker Desktop.
Core Docker CLI
The docker command-line tool. Communicates with the Docker daemon over a Unix socket (/var/run/docker.sock) or TCP. All container operations — run, build, push, exec — go through this CLI.
Ecosystem Docker Hub
The default public registry. Official images (nginx, postgres, node, etc.) are maintained here. Free for public repos; paid tiers for private repos and teams.
Ecosystem Docker Compose
Define and run multi-container applications with a single YAML file. Handles networking, volumes, and dependency ordering between services.
How containers actually work
Docker containers are not VMs. They are isolated Linux processes using kernel features:
- Namespaces — isolate the container's view of PIDs, network, mounts, users, and hostnames. Each container thinks it has its own system.
- cgroups — limit CPU, memory, I/O, and other resources. Prevent one container from starving the host.
- Union filesystem (OverlayFS) — layers the container's writable layer on top of read-only image layers. Efficient storage and fast startup.
Because containers share the host kernel, they start in milliseconds (no boot sequence) and use far less memory than VMs. The trade-off is weaker isolation — a kernel exploit can escape the container boundary.
Docker on Windows
Docker requires a Linux kernel. On Windows, Docker Desktop provides a Linux environment through one of two backends:
Recommended WSL 2 Backend
Docker Desktop creates a lightweight Linux VM managed by the Windows Subsystem for Linux 2. WSL 2 uses a real Linux kernel running in a Hyper-V utility VM. Docker integrates directly into WSL 2 distros — you can run docker commands from your Ubuntu terminal natively.
- Fast filesystem performance for Linux-native paths (
/home) - Lower memory footprint (dynamic memory allocation)
- Seamless integration with VS Code Remote — WSL
- Cross-distro: one Docker daemon shared across all WSL 2 distros
Legacy Hyper-V Backend
Before WSL 2, Docker Desktop ran a full Hyper-V virtual machine (MobyLinuxVM) with a custom LinuxKit-based kernel. This is still available but largely obsolete.
- Requires Windows Pro/Enterprise (Hyper-V not available on Home)
- Fixed memory allocation (must pre-allocate RAM to the VM)
- Slower filesystem mounts (9P protocol between Windows and Linux)
- VirtualBox 6+ and VMware can coexist with Hyper-V via the Windows Hypervisor Platform API, but with degraded performance
In both backends, your Linux containers run inside a real Linux kernel inside a VM. Docker on Windows is always VM-based — the question is just how the VM is managed. WSL 2 makes this nearly invisible. Windows containers (running Windows Server Core or Nano Server) are a separate thing entirely and use the Windows kernel directly.
Filesystem performance gotcha
With WSL 2, accessing files across the Windows/Linux boundary is slow. If your source code lives on C:\Users\... and your container mounts it via /mnt/c/..., expect 5-10x slower I/O compared to storing code inside the WSL 2 filesystem (/home/user/projects). Always clone repos inside WSL for Docker workloads.
Dockerfile
A Dockerfile is a text file with instructions to build a Docker image. Each instruction creates a layer in the image. Layers are cached — only changed layers are rebuilt.
Multi-stage build example
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN npm run build
# Stage 2: Runtime (slim image, no build tools)
FROM node:20-alpine
WORKDIR /app
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]
Best practices
- Use specific base image tags —
node:20.11-alpinenotnode:latest. Reproducible builds. - Order instructions by change frequency — put
COPY package.jsonbeforeCOPY .so dependency installs are cached when only source code changes. - Multi-stage builds — keep build tools out of the final image. Reduces image size and attack surface.
- Run as non-root — always add
USERinstruction. Never run production containers as root. - Use
.dockerignore— excludenode_modules,.git,.env, etc. from the build context. HEALTHCHECK— lets Docker and orchestrators know if the app inside the container is actually healthy, not just that the process is running.
.dockerignore
node_modules
.git
.env
*.log
Dockerfile
docker-compose*.yml
.dockerignore
Running Containers
The docker run command creates and starts a container from an image. The flags you pass control networking, storage, restart behavior, and resource limits.
Essential docker run flags
# Run in background (detached), name it, auto-restart
docker run -d --name my-app --restart unless-stopped nginx:1.25
# Map host port 8080 to container port 80
docker run -d -p 8080:80 nginx:1.25
# Map multiple ports
docker run -d -p 8080:80 -p 8443:443 nginx:1.25
# Mount a host directory as a volume
docker run -d -v /host/data:/container/data nginx:1.25
# Mount a named volume (Docker manages the storage)
docker run -d -v my-volume:/var/lib/data postgres:16
# Read-only volume mount
docker run -d -v /host/config:/etc/app/config:ro my-app
# Set environment variables
docker run -d -e DATABASE_URL=postgres://db:5432/app -e NODE_ENV=production my-app
# Load env vars from a file
docker run -d --env-file .env my-app
# Limit resources
docker run -d --memory=512m --cpus=1.5 my-app
# Run interactively (e.g., for debugging)
docker run -it --rm ubuntu:22.04 bash
Restart policies
| Policy | Behavior |
|---|---|
no | Default. Container stays stopped after exit. |
always | Always restart on failure. Also restarts on daemon startup, even if the container was manually stopped with docker stop. |
unless-stopped | Like always, but does not restart if you explicitly docker stop it. Restarts on daemon startup only if it was running before. |
on-failure[:max] | Restart only on non-zero exit code. Optional retry limit: on-failure:5. |
Use --restart unless-stopped for most production containers. It survives host reboots and daemon restarts, but respects manual docker stop. Use always only for critical infrastructure that must run no matter what.
Volume types
Recommended Named Volumes
-v my-data:/var/lib/data
Docker manages the storage location (usually /var/lib/docker/volumes/). Portable, easy to back up with docker volume commands. Best for databases and persistent app data.
Bind mount Host Paths
-v /host/path:/container/path
Maps a specific host directory into the container. Good for development (live code reloading) and config files. Not portable across hosts.
Common Commands
Container lifecycle
# List running containers
docker ps
# List all containers (including stopped)
docker ps -a
# Start a stopped container
docker start my-app
# Stop a running container (sends SIGTERM, then SIGKILL after 10s)
docker stop my-app
# Stop with custom timeout
docker stop -t 30 my-app
# Kill immediately (SIGKILL)
docker kill my-app
# Remove a stopped container
docker rm my-app
# Force-remove a running container
docker rm -f my-app
# Remove all stopped containers
docker container prune
Interacting with running containers
# Execute a command inside a running container
docker exec -it my-app bash
# Run a one-off command (no shell)
docker exec my-app cat /etc/hostname
# View logs
docker logs my-app
# Follow logs in real time
docker logs -f --tail 100 my-app
# View resource usage
docker stats my-app
# Copy files between host and container
docker cp my-app:/var/log/app.log ./app.log
docker cp ./config.yml my-app:/etc/app/config.yml
# Inspect container details (IP, mounts, env, etc.)
docker inspect my-app
Image management
# List local images
docker images
# Pull an image
docker pull nginx:1.25-alpine
# Remove an image
docker rmi nginx:1.25-alpine
# Remove all unused images (dangling + unreferenced)
docker image prune -a
# Tag an image for a registry
docker tag my-app:latest registry.example.com/my-app:v1.2
# Push to a registry
docker push registry.example.com/my-app:v1.2
# Full system cleanup (containers, images, volumes, networks)
docker system prune -a --volumes
docker system prune -a --volumes deletes everything not currently in use: all stopped containers, all images without a running container, all unused volumes. It will destroy data. Use with extreme caution in production.
Building Images
Building locally
# Build from current directory
docker build -t my-app:v1.0 .
# Build with a specific Dockerfile
docker build -t my-app:v1.0 -f Dockerfile.production .
# Build with build arguments
docker build --build-arg NODE_ENV=production -t my-app:v1.0 .
# Build with no cache (fresh build)
docker build --no-cache -t my-app:v1.0 .
# Multi-platform build (requires buildx)
docker buildx build --platform linux/amd64,linux/arm64 -t my-app:v1.0 --push .
Building via GitHub Actions CI/CD
# .github/workflows/docker-build.yml
name: Build and Push Docker Image
on:
push:
branches: [main]
tags: ['v*']
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Log in to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v6
with:
images: myorg/my-app
tags: |
type=semver,pattern={{version}}
type=sha,prefix=
- name: Build and push
uses: docker/build-push-action@v7
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
Building via GitLab CI/CD
# .gitlab-ci.yml
build-image:
stage: build
image: docker:28
services:
- docker:28-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker build -t $CI_REGISTRY_IMAGE:latest .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
- docker push $CI_REGISTRY_IMAGE:latest
rules:
- if: $CI_COMMIT_BRANCH == "main"
Registries
A container registry stores and distributes Docker images. When you docker pull nginx, it pulls from Docker Hub by default.
| Registry | Type | Notes |
|---|---|---|
| Docker Hub | Public / paid private | Default registry. Rate-limited for anonymous pulls (100/6h). Official images live here. |
| GitHub Container Registry (ghcr.io) | Public / private | Tied to GitHub repos. Free for public images. Good for open-source projects. |
| GitLab Container Registry | Built-in | Every GitLab project gets a registry. Integrates with GitLab CI/CD pipelines. |
| Harbor | Self-hosted | Enterprise-grade. Vulnerability scanning, replication, RBAC. CNCF graduated project. |
| AWS ECR | Cloud | Integrated with ECS/EKS. Lifecycle policies for image cleanup. |
| Google Artifact Registry | Cloud | Replaces GCR. Supports Docker, npm, Maven, etc. |
| Azure ACR | Cloud | Integrated with AKS. Geo-replication for global deployments. |
# Log in to a private registry
docker login registry.example.com
# Pull from a specific registry (not Docker Hub)
docker pull ghcr.io/myorg/my-app:v1.0
# Push to GitLab registry
docker tag my-app:v1.0 registry.gitlab.com/myorg/myproject/my-app:v1.0
docker push registry.gitlab.com/myorg/myproject/my-app:v1.0
Docker Compose
Docker Compose defines multi-container applications in a single docker-compose.yml (or compose.yml) file. It creates a shared network, manages volumes, and handles startup order.
# docker-compose.yml
services:
app:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://postgres:secret@db:5432/myapp
REDIS_URL: redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
restart: unless-stopped
volumes:
- ./uploads:/app/uploads
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
restart: unless-stopped
cache:
image: redis:7-alpine
restart: unless-stopped
volumes:
pgdata:
Common Compose commands
# Start all services (detached)
docker compose up -d
# Rebuild images and start
docker compose up -d --build
# Stop all services
docker compose down
# Stop and remove volumes (destroys data!)
docker compose down -v
# View logs across all services
docker compose logs -f
# Scale a service
docker compose up -d --scale worker=3
# Execute a command in a running service
docker compose exec app bash
# Pull latest images
docker compose pull
Docker Swarm
Docker Swarm is Docker's built-in container orchestrator. It turns a group of Docker hosts into a single virtual host with service scheduling, rolling updates, and load balancing. It's significantly simpler than Kubernetes but less powerful.
Strength Simplicity
If you already know Docker and Docker Compose, Swarm is trivial to learn. A docker-compose.yml file can be deployed as a Swarm stack with docker stack deploy. No extra tooling, no YAML explosion.
Limitation Ecosystem
Swarm lost the orchestration war to Kubernetes. Docker Inc. effectively abandoned active development. The community is small, third-party integrations are rare, and many features (autoscaling, advanced networking, service mesh) simply don't exist.
# Initialize Swarm on the first manager node
docker swarm init --advertise-addr 10.0.1.10
# Join a worker node
docker swarm join --token SWMTKN-1-xxx 10.0.1.10:2377
# Deploy a stack from a compose file
docker stack deploy -c docker-compose.yml myapp
# List services
docker service ls
# Scale a service
docker service scale myapp_web=5
# Rolling update
docker service update --image my-app:v2.0 myapp_web
# Drain a node (for maintenance)
docker node update --availability drain worker-02
Swarm is a reasonable choice for small teams running a handful of services that don't need autoscaling, service mesh, or a large ecosystem of operators and controllers. If you're already running Docker Compose and just need multi-host replication and rolling updates, Swarm gets you there with near-zero learning curve. For anything beyond that, use Kubernetes.
Management Tools
Portainer
Web-based GUI for managing Docker environments. Provides a dashboard for containers, images, volumes, networks, and stacks. Supports both standalone Docker and Swarm clusters.
# Deploy Portainer CE (Community Edition)
docker volume create portainer_data
docker run -d -p 9443:9443 --name portainer \
--restart unless-stopped \
-v /var/run/docker.sock:/var/run/docker.sock \
-v portainer_data:/data \
portainer/portainer-ce:latest
Alternatives to Portainer
Open source Dockge
Lightweight, self-hosted Docker Compose manager. Focuses on compose file management rather than full Docker administration. Real-time terminal output, simple UI. Good for managing multiple docker-compose.yml stacks.
Open source Yacht
Simple web UI for managing Docker containers. Template-based container deployment. Less feature-rich than Portainer but simpler to use for basic operations.
CLI lazydocker
Terminal UI for Docker. Shows containers, images, volumes, and logs in a curses-style interface. Great for SSH sessions where you want visual overview without a web browser.
CLI ctop
Top-like interface for container metrics. Real-time CPU, memory, network, and I/O stats per container. Lightweight monitoring without deploying Prometheus/Grafana.
Auto-Updates
Watchtower
Watchtower monitors running containers and automatically updates them when a new image is pushed to the registry. It pulls the latest image, gracefully stops the old container, and starts a new one with the same configuration.
# Run Watchtower (monitors all containers by default)
docker run -d --name watchtower \
--restart unless-stopped \
-v /var/run/docker.sock:/var/run/docker.sock \
containrrr/watchtower
# Only monitor specific containers
docker run -d --name watchtower \
-v /var/run/docker.sock:/var/run/docker.sock \
containrrr/watchtower my-app nginx
# Check every 5 minutes, send notifications, cleanup old images
docker run -d --name watchtower \
-v /var/run/docker.sock:/var/run/docker.sock \
-e WATCHTOWER_POLL_INTERVAL=300 \
-e WATCHTOWER_CLEANUP=true \
-e WATCHTOWER_NOTIFICATIONS=slack \
-e WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL=https://hooks.slack.com/... \
containrrr/watchtower
Watchtower auto-updates are convenient but risky for production. A bad image pushed to :latest will be deployed automatically. Use pinned image tags (e.g., nginx:1.25.3) for critical services and let Watchtower handle only non-critical or dev environments. For production, use CI/CD pipelines with explicit version bumps and approval gates.
Alternatives to Watchtower
Pull-based Ouroboros
Similar to Watchtower but with a slightly different notification system. Largely abandoned in favor of Watchtower. Not recommended for new deployments.
CI/CD Pipeline-driven updates
The production-grade approach: CI/CD pipeline builds image, pushes to registry, then triggers deployment via SSH, Ansible, or Kubernetes. Full control, audit trail, rollback capability.
Compose Diun
Docker Image Update Notifier — does not auto-update. Instead, it monitors registries and notifies you (Slack, email, webhook) when new images are available. You decide when and how to update. Safer than Watchtower for production.
Docker vs Podman
| Aspect | Docker | Podman |
|---|---|---|
| Architecture | Client-server: CLI talks to dockerd daemon running as root | Daemonless: each container is a child process of the Podman command. No persistent daemon. |
| Root required | Daemon runs as root by default. Rootless mode available and production-ready since Engine 20.10, but less commonly used. | Rootless by default. No privileged daemon needed. |
| CLI compatibility | docker | podman — drop-in replacement. alias docker=podman works for most commands. |
| Compose | Docker Compose (built-in) | podman compose (built-in subcommand in Podman 5+, delegates to docker-compose or podman-compose) or podman generate kube for Kubernetes YAML |
| Systemd integration | Containers managed by Docker daemon | Containers can run as systemd services via Quadlet unit files (recommended) or the deprecated podman generate systemd. Native integration. |
| Kubernetes | No direct path | podman generate kube and podman play kube — generate/run Kubernetes YAML natively |
| Build tool | docker build (BuildKit) | podman build (Buildah under the hood) |
| Image format | OCI / Docker | OCI / Docker (fully compatible) |
| Licensing | Docker Engine: Apache 2.0. Docker Desktop: paid for enterprises (>250 employees OR >$10M revenue) and government entities. | Fully open source (Apache 2.0). No paid tiers. |
Podman is preferred on RHEL/CentOS/Fedora systems where it ships by default. It's also the better choice for security-conscious environments that want rootless containers without a privileged daemon. For most developer workstations, Docker Desktop remains more polished (especially on macOS/Windows). The images and Dockerfiles are fully interchangeable — the choice is about the runtime and daemon model, not the containers themselves.
Production Checklist
- Pin image tags — never use
:latestin production. Use specific version tags for reproducibility. - Run as non-root — add
USERin Dockerfile. Drop all capabilities except what's needed. - Health checks — define
HEALTHCHECKin Dockerfile orhealthcheck:in Compose. Orchestrators need this for restart decisions. - Restart policy — set
--restart unless-stoppedfor production containers. - Log management — configure log drivers (json-file with rotation, or forward to syslog/fluentd). Unbounded
json-filelogs will fill your disk. - Resource limits — set
--memoryand--cpusto prevent a single container from starving the host. - Named volumes for data — never store persistent data in the container's writable layer.
- Scan images — use
docker scout, Trivy, or Grype to scan for CVEs before deploying. - Minimal base images — use Alpine or distroless variants. Fewer packages = smaller attack surface.
- Don't expose Docker socket — mounting
/var/run/docker.sockinto a container gives it root-equivalent access to the host. Only do this for trusted management tools (Portainer, Watchtower). - Backup volumes — Docker volumes are not backed up automatically. Use
docker run --rm -v my-vol:/data -v $(pwd):/backup alpine tar czf /backup/vol.tar.gz -C /data . - Prune regularly — schedule
docker system prunevia cron to reclaim disk from old images and containers.