Docker is an open-source platform that lets you package an application and everything it needs to run — code, runtime, libraries, environment variables — into a single portable unit called a container. You build it once; it runs the same way everywhere: your laptop, a colleague's machine, a test server, or production in the cloud.
If you've ever heard "works on my machine" and wished it worked on everyone's machine, Docker is the answer.
Docker in 30 seconds
| Concept | What it means |
|---|---|
| Image | A read-only blueprint (snapshot) for a container |
| Container | A running instance of an image |
| Dockerfile | A script that builds an image step by step |
| Docker Hub | Public registry of pre-built images |
| docker-compose | Tool to run multiple containers together |
| Volume | Persistent storage that survives container restarts |
| Network | Virtual network connecting containers |
# Pull an image from Docker Hub
docker pull nginx
# Run a container from that image
docker run -d -p 8080:80 nginx
# Visit http://localhost:8080 — Nginx is running!
Why does Docker exist?
Before Docker, deploying software meant:
- Writing an app that works on your local machine
- Shipping it to a server with a different OS, different library versions, different paths
- Spending hours (or days) making it work again
Docker solves this with containerization: isolate the app and its environment together so the environment travels with the code.
The "works on my machine" problem — solved
Developer laptop CI server Production
───────────────────── ───────────────── ─────────────────
Node.js 18 Node.js 16 Node.js 20
npm 9.x npm 8.x npm 9.x
Ubuntu 22.04 CentOS 7 Amazon Linux 2
With Docker, all three environments run the exact same container image. No more version mismatches.
Containers vs Virtual Machines
People often confuse Docker containers with virtual machines (VMs). They solve similar problems but work very differently.
| Feature | Container | Virtual Machine |
|---|---|---|
| What it virtualises | OS process isolation | Entire hardware |
| Includes OS kernel | No — shares host kernel | Yes — full OS inside |
| Startup time | Milliseconds | 30–60 seconds |
| Disk size | Megabytes | Gigabytes |
| Performance overhead | Minimal (near-native) | Higher (hypervisor layer) |
| Isolation level | Process-level | Full machine-level |
| Portability | Excellent | Good but image is huge |
| Best for | Microservices, CI/CD, dev env | Running different OS, legacy apps |
Rule of thumb: Containers are for applications; VMs are for entire operating systems.
Virtual Machine stack: Container stack:
┌─────────────────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ Application │ │ App A │ │ App B │ │ App C │
├─────────────────────┤ ├────────┴─┴────────┴─┴────────┤
│ Guest OS (full!) │ │ Docker Engine │
├─────────────────────┤ ├──────────────────────────────┤
│ Hypervisor │ │ Host OS Kernel │
├─────────────────────┤ ├──────────────────────────────┤
│ Host Hardware │ │ Host Hardware │
└─────────────────────┘ └──────────────────────────────┘
Core Docker concepts
Images
A Docker image is a read-only template. Think of it like a class in OOP — it defines what a container looks like but doesn't run on its own.
Images are built in layers. Each instruction in a Dockerfile adds a layer:
Base layer: ubuntu:22.04
Layer 2: apt-get install python3
Layer 3: pip install flask
Layer 4: COPY app.py /app/
Layer 5: CMD ["python3", "/app/app.py"]
Layers are cached and shared between images — if 10 projects all use ubuntu:22.04, that base layer is only stored once on disk.
Containers
A container is a running instance of an image — like an object instantiated from a class.
# Same image, three containers running simultaneously
docker run -d --name web1 nginx
docker run -d --name web2 nginx
docker run -d --name web3 nginx
Containers are:
- Isolated — each has its own filesystem, network, and process space
- Ephemeral by default — stopping a container discards its filesystem changes
- Lightweight — you can run dozens on a single laptop
Dockerfile
A Dockerfile is a text file that defines how to build your image. It's the recipe.
# Start from official Node.js 20 image
FROM node:20-alpine
# Set working directory inside the container
WORKDIR /app
# Copy dependency files first (layer caching optimisation)
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy the rest of the source
COPY . .
# Expose port 3000
EXPOSE 3000
# Command to run when container starts
CMD ["node", "server.js"]
Build and run:
docker build -t my-app:1.0 .
docker run -d -p 3000:3000 my-app:1.0
Volumes
Containers are ephemeral — when a container stops, its data is gone. Volumes solve this by mounting persistent storage from the host into the container.
# Named volume (Docker manages the location)
docker run -d -v my-db-data:/var/lib/postgresql/data postgres:16
# Bind mount (specific host path → container path)
docker run -d -v ./src:/app/src node:20-alpine
Volumes are essential for:
- Databases (PostgreSQL, MySQL, MongoDB)
- User-uploaded files
- Development: live-reloading code changes
Networks
Containers on the same Docker network can talk to each other by name:
# Create a network
docker network create myapp-net
# Both containers join the same network
docker run -d --network myapp-net --name db postgres:16
docker run -d --network myapp-net --name api my-api:1.0
# Inside the api container, the db is reachable at "db:5432"
Docker Compose
Running one container is easy. Running a full stack (app + database + cache + reverse proxy) with docker run gets tedious fast. Docker Compose solves this with a single YAML file.
# docker-compose.yml
version: "3.9"
services:
app:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgresql://postgres:password@db:5432/mydb
REDIS_URL: redis://cache:6379
depends_on:
- db
- cache
volumes:
- ./src:/app/src # live reload in development
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: password
POSTGRES_DB: mydb
volumes:
- db_data:/var/lib/postgresql/data
cache:
image: redis:7-alpine
volumes:
db_data:
Start the entire stack:
docker compose up -d # Start all services in background
docker compose logs -f # Follow logs
docker compose down # Stop and remove containers
docker compose down -v # Also remove volumes (wipes data)
Essential Docker commands
| Command | What it does |
|---|---|
docker pull <image> |
Download image from registry |
docker build -t name:tag . |
Build image from Dockerfile in current dir |
docker run -d -p 8080:80 nginx |
Run container (detached, port mapping) |
docker ps |
List running containers |
docker ps -a |
List all containers (including stopped) |
docker stop <id or name> |
Stop a container gracefully |
docker rm <id or name> |
Delete a container |
docker images |
List local images |
docker rmi <image> |
Delete an image |
docker exec -it <id> bash |
Open shell inside running container |
docker logs -f <id> |
Follow container logs |
docker inspect <id> |
Full JSON details of a container |
docker system prune |
Remove unused containers, images, networks |
docker volume ls |
List volumes |
How Docker works under the hood
Docker uses Linux kernel features — you don't need to know this to use Docker, but it helps you understand why containers are so fast:
| Kernel feature | What it provides |
|---|---|
| Namespaces | Isolation — each container sees its own PID, network, filesystem, hostname |
| cgroups | Resource limits — restrict CPU, memory, I/O per container |
| Union filesystem (OverlayFS) | Layer-based filesystem for images |
| seccomp | Syscall filtering for security |
On macOS and Windows, Docker runs a lightweight Linux VM (via HyperKit/WSL 2) because these kernel features are Linux-only.
Docker workflow in practice
A typical development→production workflow:
1. Write code
│
▼
2. Write Dockerfile (once)
│
▼
3. docker build → creates image
│
▼
4. docker run → test locally
│
▼
5. docker push → push to registry (Docker Hub, ECR, GCR)
│
▼
6. Production server: docker pull + docker run
(or Kubernetes/ECS/Cloud Run pulls and runs it)
Multi-stage builds (production optimisation)
Keep production images small by separating build and runtime stages:
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Production — only the compiled output
FROM node:20-alpine AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/server.js"]
The production image doesn't include dev dependencies, source files, or build tools — often 5–10× smaller.
When to use Docker
| Use case | Why Docker helps |
|---|---|
| Development environment | Same env for entire team, no "works on my machine" |
| CI/CD pipelines | Reproducible builds, isolated test environments |
| Microservices | Each service in its own container, independent scaling |
| Running open-source tools | docker run postgres — no install needed |
| Legacy app isolation | Old PHP 5.6 app in a container, won't pollute host |
| Cloud deployment | All major clouds run containers natively (ECS, GKE, ACI) |
| Onboarding | New dev up and running in 5 minutes with docker compose up |
Docker registries
A registry stores Docker images. When you docker pull, it downloads from a registry.
| Registry | Description |
|---|---|
| Docker Hub | Default public registry (hub.docker.com) |
| AWS ECR | Amazon's private container registry |
| Google Artifact Registry | Google Cloud's registry |
| GitHub Container Registry | ghcr.io, integrated with GitHub Actions |
| Azure Container Registry | Microsoft's private registry |
| Self-hosted | Run your own with registry:2 image |
# Push to Docker Hub
docker login
docker tag my-app:1.0 username/my-app:1.0
docker push username/my-app:1.0
# Push to GitHub Container Registry
docker tag my-app:1.0 ghcr.io/username/my-app:1.0
docker push ghcr.io/username/my-app:1.0
Docker security basics
| Best practice | Why |
|---|---|
| Use official base images | Scanned and maintained by vendors |
Pin image versions (node:20.12 not node:latest) |
Reproducible builds |
| Run as non-root user | Limit blast radius if container is compromised |
Scan images (docker scout, Trivy, Snyk) |
Find known CVEs |
| Don't store secrets in Dockerfile | Use env vars or secrets managers |
| Use multi-stage builds | Smaller attack surface |
| Read-only filesystems where possible | --read-only flag |
# Add non-root user
FROM node:20-alpine
RUN addgroup -S app && adduser -S app -G app
USER app
WORKDIR /home/app
COPY --chown=app:app . .
CMD ["node", "server.js"]
Docker vs related tools
| Tool | What it is | Relationship to Docker |
|---|---|---|
| Docker | Container runtime + build tool | The foundation |
| Kubernetes (K8s) | Container orchestration at scale | Runs Docker containers across a cluster |
| Docker Swarm | Docker's own simpler orchestration | Built into Docker, less popular than K8s |
| Podman | Daemonless Docker alternative | Drop-in replacement, rootless by default |
| containerd | Low-level container runtime | Docker uses it internally; K8s uses it directly |
| docker-compose | Multi-container local dev | Great for development, not for production scale |
| Helm | Kubernetes package manager | Deploys containerised apps to K8s |
| OCI | Open Container Initiative standard | Docker images follow OCI spec |
Common mistakes
| Mistake | Fix |
|---|---|
FROM ubuntu with full apt-get installs |
Use slim images (node:20-alpine) — 10× smaller |
Copying everything including node_modules |
Add node_modules to .dockerignore |
| Storing secrets in Dockerfile or image | Use --env, .env files (not committed), or secrets managers |
| Not using layer caching | Copy package.json before source files |
| Running as root inside container | Add USER nonroot |
| One giant monolith container | One process per container |
| No health checks | Add HEALTHCHECK instruction |
docker run with no resource limits |
Add --memory and --cpus flags |
Docker vs Kubernetes — when to use what
| Situation | Use |
|---|---|
| Local development | Docker Compose |
| Single server, simple deployment | Docker + docker-compose |
| Multiple servers, high availability | Kubernetes |
| Managed simplicity | AWS ECS, Google Cloud Run |
| Learning containers | Docker first, then K8s |
A common progression: Docker → Docker Compose → Kubernetes as your application scales.
Frequently asked questions
Is Docker free? Docker Desktop (the GUI app for Mac/Windows) requires a paid subscription for companies with more than 250 employees or $10M revenue. Docker Engine (the CLI) is always free and open source. For most developers and CI environments, Docker Engine is all you need.
Do I need Docker to use Kubernetes? No. Kubernetes can use any OCI-compatible container runtime (containerd, CRI-O). But the containers you run on Kubernetes are built with Docker (or Buildah/Podman), and Docker images are OCI-compatible, so they just work.
Is Docker the same as a container? No. A container is the concept (isolated process). Docker is a tool (one of several) for building and running containers. Other tools: Podman, containerd, LXC.
Can Docker run on Windows without WSL 2? Docker Desktop on Windows uses WSL 2 (Windows Subsystem for Linux) as its backend, which is required. On Windows Server, Docker can run Windows containers natively.
How is Docker different from a virtual environment (venv/conda)? A Python venv isolates Python packages only. Docker isolates everything — the OS libraries, the runtime version, system packages, network — not just Python. venv is fine for Python projects; Docker is for deploying services.
How much memory does Docker use? Each container uses only what its process needs. An Nginx container at idle might use 5–10 MB. A Node.js app might use 50–200 MB. Docker Engine itself uses ~200–400 MB. Docker Desktop on Mac/Windows runs a Linux VM that uses 2–4 GB by default (configurable).
Quick start: your first Docker project
# 1. Install Docker (docker.com/get-started)
# 2. Verify installation
docker --version
docker run hello-world
# 3. Run a database without installing it
docker run -d \
--name my-postgres \
-e POSTGRES_PASSWORD=mysecret \
-e POSTGRES_DB=mydb \
-p 5432:5432 \
postgres:16-alpine
# 4. Connect to it (from host, if psql installed)
psql -h localhost -U postgres -d mydb
# 5. Run a Redis cache
docker run -d --name my-redis -p 6379:6379 redis:7-alpine
# 6. Stop and remove when done
docker stop my-postgres my-redis
docker rm my-postgres my-redis
With Docker, every tool in your stack is a one-liner away — no installation, no version conflicts, no cleanup headaches.