Docker Containers: Build, Ship, and Run Anywhere
Docker revolutionized software development by making containers the standard unit of deployment. A container packages an application with all its dependencies — libraries, configuration files, binaries, and runtime — into a single artifact that runs identically on any Linux, Windows, or macOS machine. Docker containers are lightweight (megabytes, not gigabytes), start in milliseconds (not minutes), and consume only the resources they need. The Docker Engine runs on over 100 million machines worldwide according to Docker’s 2025 usage metrics.
Images vs. Containers
Understanding the distinction between images and containers is fundamental. An image is a read-only template containing the application, runtime, libraries, and configuration. A container is a runnable instance of an image — an isolated process with its own filesystem, network, and process namespace.
Think of images as classes in object-oriented programming and containers as instances. Multiple containers can run from the same image, each with its own writable layer for runtime modifications. When a container is deleted, the writable layer is discarded; the image remains unchanged.
Container images are built in layers, each corresponding to an instruction in the Dockerfile. Layers are cached — if you modify a later instruction but not an earlier one, Docker reuses the cached layers. This layer caching is the key to fast Docker builds.
Writing Effective Dockerfiles
A Dockerfile is a text file containing instructions for building an image. Every instruction creates a new layer:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]Best practices for Dockerfiles:
- Use specific base image tags —
python:3.12-slimnotpython:latest. Thelatesttag changes over time and breaks builds unpredictably. Slim variants reduce attack surface and image size. - Order instructions by cacheability — copy dependency files first (requirements.txt, package.json), run the install step, then copy the application code. This maximizes layer cache reuse when only application code changes.
- Combine RUN commands — each RUN instruction creates a layer. Chain commands with
&&to reduce layers:RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* - Use
.dockerignore— exclude node_modules, .git, pycache, and other non-essential files to keep build context small
Build and run:
docker build -t excellentwiki-api:latest .
docker run -d -p 8000:8000 --name api excellentwiki-api:latestEssential Docker Commands
| Command | Purpose |
|---|---|
docker ps | List running containers |
docker images | List local images |
docker logs -f api | Follow container logs |
docker exec -it api bash | Open interactive shell in container |
docker stop api && docker rm api | Stop and remove container |
docker system prune | Remove unused containers, images, and networks |
Docker provides docker inspect for detailed metadata about containers and images (JSON output), and docker stats for real-time resource usage monitoring.
Container Networking
Docker networks connect containers to each other and to the outside world:
- Bridge (default) — isolated network on the host, containers communicate via IP. Best for single-host deployments.
- Host — container uses the host’s network stack directly, no port mapping needed. Best for performance-sensitive workloads but reduces isolation.
- Overlay — spans multiple Docker hosts in a Swarm cluster. Containers on different hosts communicate as if on the same network.
docker network create app-network
docker run --network app-network --name db postgres:16
docker run --network app-network --name api -p 8000:8000 excellentwiki-apiContainers on the same bridge network can resolve each other by container name — postgres://db:5432/mydb resolves to the database container’s IP.
Volumes: Persistent Storage
Containers are ephemeral — files created inside a container are lost when the container is removed. Volumes provide persistent storage that survives container restarts and removals:
docker volume create pgdata
docker run -v pgdata:/var/lib/postgresql/data -e POSTGRES_PASSWORD=secret postgres:16Docker volumes are stored in the Docker host’s filesystem (/var/lib/docker/volumes/ on Linux). They support drivers for NFS, cloud storage (Cloudstor), and encryption. Bind mounts map a host directory directly into a container — useful for development hot-reloading but less portable.
Docker Compose for Multi-Container Applications
Docker Compose defines and runs multi-container applications with a single YAML file. Instead of running five docker run commands, you define all services, networks, and volumes in compose.yaml:
services:
api:
build: ./api
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://postgres:secret@db:5432/app
depends_on:
- db
- redis
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
web:
build: ./web
ports:
- "3000:3000"
depends_on:
- api
db:
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
redis:
image: redis:7-alpine
volumes:
pgdata:docker compose up -d # Start all services
docker compose logs -f # Follow all logs
docker compose down -v # Stop and remove volumesMulti-Stage Builds
Multi-stage builds use multiple FROM statements to separate the build environment from the runtime environment, producing smaller, more secure final images:
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Runtime
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]The final image contains only the Nginx runtime and the static files — no Node.js, no source code, no development dependencies. The image size drops from 1.2 GB (full Node + build tooling) to ~25 MB.
Container Registries
Container registries store and distribute images. Push images to a registry after building and pull them on deployment targets:
docker tag excellentwiki-api:latest registry.example.com/excellentwiki-api:v1.0.0
docker push registry.example.com/excellentwiki-api:v1.0.0Popular registries include Docker Hub, AWS Elastic Container Registry (ECR), Google Artifact Registry, Azure Container Registry (ACR), and GitHub Container Registry (GHCR). Each integrates with its platform’s IAM for access control.
Docker Compose for Local Development
Docker Compose is the standard tool for local development environments with multiple services. It replicates the production architecture locally, eliminating “works on my machine” issues. Hot-reloading is achieved by mounting the source code directory as a bind mount — changes to the source are reflected immediately without rebuilding the image.
services:
api:
build:
context: ./api
dockerfile: Dockerfile.dev
volumes:
- ./api:/app # Bind mount for hot-reload
environment:
- DATABASE_URL=postgresql://postgres:password@db:5432/app
- REDIS_URL=redis://redis:6379/0
ports:
- "8000:8000"
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
volumes:
- pgdata:/var/lib/postgresql/data
- ./db/init:/docker-entrypoint-initdb.d
environment:
POSTGRES_PASSWORD: password
redis:
image: redis:7-alpineUse profiles to run different service combinations: docker compose --profile dev up for development with hot-reload, docker compose --profile ci up for testing with ephemeral databases, and docker compose --profile prod up for production-like local testing with optimized images.
Security Best Practices
- Run as non-root — add
USER appuserin the Dockerfile. The default root user inside containers is a common vulnerability. - Scan for vulnerabilities —
docker scan(Snyk integration) checks images against CVE databases. Integrate scanning into CI/CD pipelines. - Minimize installed packages — use slim or Alpine base images. Each additional package is a potential attack vector.
- Sign images — Docker Content Trust (DCT) ensures images are signed by trusted publishers.
- Use secrets, not environment variables —
docker secret(Swarm) or--secretflag for build-time secrets. Environment variables leak to logs and debugging tools.
FAQ
What is the difference between a container and a VM? Containers share the host OS kernel and run as isolated processes. VMs include a full guest OS with its own kernel, requiring a hypervisor. Containers start in milliseconds and use less memory; VMs provide stronger isolation. You can run dozens of containers on a host that would only support a few VMs.
When should I use Docker Compose vs Kubernetes? Docker Compose is ideal for local development, testing, and single-host production deployments with few services. Kubernetes is needed for multi-host clusters, auto-scaling, rolling updates, service discovery, and complex orchestration. Many teams use Compose for development and Kubernetes for production.
How do I debug a container that crashes immediately?
Override the CMD to keep the container running: docker run -it --entrypoint bash myimage. This gives you an interactive shell to inspect files, check logs, and run commands manually.
Why is my container image so large?
Common causes: using a full base image (ubuntu:latest instead of ubuntu:22.04-slim), not cleaning up package manager caches (apt-get clean && rm -rf /var/lib/apt/lists/*), including dev dependencies in production builds, and not using multi-stage builds.
Can I run Docker containers on Windows? Yes. Docker Desktop for Windows runs containers using WSL 2 (Windows Subsystem for Linux) or Hyper-V isolation. Windows containers can run .NET Framework and native Windows applications. Most production deployments target Linux containers regardless of the host OS.
Kubernetes Guide — CI/CD Pipeline Guide — Cloud-Native Development
Related Concepts and Further Reading
Understanding docker containers requires familiarity with several interconnected ideas and principles that together form a complete picture. Exploring these related concepts deepens your knowledge and provides context that makes the core material more meaningful and applicable. Each concept builds on the others, creating a web of understanding that supports deeper learning and practical application. Taking time to explore how these elements connect reveals patterns that accelerate comprehension and retention of new information.
The relationship between docker containers and adjacent fields is worth particular attention. Many of the most important insights emerge at the boundaries between disciplines, where ideas from different areas combine to create new approaches and solutions that neither field could produce alone. Exploring these connections pays dividends in both breadth and depth of understanding, revealing patterns and principles that might otherwise remain hidden from view. Cross-disciplinary knowledge is increasingly valued as problems become more complex and interconnected.
For those looking to go beyond introductory material, several excellent resources provide deeper treatment of specific aspects of docker containers. Academic journals, industry publications, authoritative reference works, and online courses each offer different perspectives and levels of detail. The key is to match your reading to your current learning goals and build knowledge progressively, focusing on quality over quantity in your study materials. A well-chosen resource that matches your current level is worth more than dozens of resources that are too basic or too advanced.