Quick Answer: Install:
curl -fsSL https://get.docker.com | sh. Run a container:docker run -d --name web -p 8080:80 nginx. See running containers:docker ps. Stop:docker stop web. Multi-container app: createcompose.ymland rundocker compose up -d.
Need a VPS? Vultr (free credit), DigitalOcean ($200 free credit), or RackNerd (cheap annual deals).
Docker packages your application and all its dependencies into a container that runs the same everywhere — your laptop, a test server, or production. No more "it works on my machine."
This guide takes you from zero to deploying Docker in production.
Part 1: Install Docker
Linux (Ubuntu/Debian)
# One-line install
curl -fsSL https://get.docker.com | sh
# Add your user to the docker group (so you don't need sudo)
sudo usermod -aG docker $USER
# Log out and back in, then verify
docker --version
docker run hello-world
Mac
Download Docker Desktop for Mac from docker.com. Install and run. Docker commands work in Terminal.
Windows
Download Docker Desktop for Windows. Requires WSL2. After install, Docker commands work in PowerShell, CMD, and WSL.
Part 2: Core Concepts
Image = A blueprint (like a class)
Container = A running instance of an image (like an object)
Dockerfile = Instructions to build an image (like source code)
Registry = Where images are stored (Docker Hub, GitHub Container Registry)
Volume = Persistent storage that survives container restarts
Network = How containers talk to each other
The Lifecycle
Dockerfile → docker build → Image → docker run → Container
↓
docker stop → Stopped Container
↓
docker rm → Removed
Part 3: Running Containers
Basic Run
# Run and see output
docker run nginx
# Run in background (detached)
docker run -d nginx
# Run with name
docker run -d --name web nginx
# Run with port mapping (host:container)
docker run -d --name web -p 8080:80 nginx
# Visit http://localhost:8080
# Run with environment variables
docker run -d --name db \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_DB=myapp \
-p 5432:5432 \
postgres:16
# Run interactively (get a shell)
docker run -it ubuntu bash
docker run -it --rm ubuntu bash # Remove after exit
# Run with volume (persistent data)
docker run -d --name db \
-v pgdata:/var/lib/postgresql/data \
postgres:16
# Run with bind mount (share host files)
docker run -d --name web \
-v $(pwd)/html:/usr/share/nginx/html \
-p 8080:80 \
nginx
Manage Containers
# List running containers
docker ps
# List ALL containers (including stopped)
docker ps -a
# Stop
docker stop web
# Start a stopped container
docker start web
# Restart
docker restart web
# Remove
docker rm web
docker rm -f web # Force (even if running)
# Remove all stopped containers
docker container prune
Inspect and Debug
# Logs
docker logs web
docker logs -f web # Follow (live)
docker logs --tail 50 web # Last 50 lines
# Shell into a running container
docker exec -it web bash
docker exec -it web sh # If bash not available
# Run a command inside container
docker exec web cat /etc/nginx/nginx.conf
# Resource usage
docker stats
docker stats web
# Detailed info
docker inspect web
# Copy files in/out
docker cp file.txt web:/path/
docker cp web:/path/file.txt ./
Part 4: Images
Working with Images
# Search Docker Hub
docker search nginx
# Pull an image
docker pull nginx
docker pull nginx:1.25-alpine # Specific tag
docker pull postgres:16-alpine
# List local images
docker images
# Remove an image
docker rmi nginx
# Remove unused images
docker image prune -a
Image Tags
nginx → latest (default, not recommended for production)
nginx:1.25 → Specific version
nginx:1.25-alpine → Alpine-based (smaller, ~5MB base)
nginx:1.25-bookworm → Debian-based (~150MB base)
Always use specific tags in production. latest can change and break things.
Alpine vs Debian-Based Images
| Alpine | Debian/Ubuntu | |
|---|---|---|
| Size | ~5MB base | ~80-150MB base |
| Package manager | apk |
apt |
| Shell | sh (no bash) |
bash |
| Best for | Production, small images | Development, compatibility |
Part 5: Building Images (Dockerfile)
Basic Dockerfile
# Start from a base image
FROM node:20-alpine
# Set working directory inside the container
WORKDIR /app
# Copy dependency files first (Docker caches this layer)
COPY package*.json ./
# Install dependencies
RUN npm ci --production
# Copy application code
COPY . .
# Expose port (documentation — doesn't actually publish)
EXPOSE 3000
# Command to run when container starts
CMD ["node", "server.js"]
Build and Run
# Build
docker build -t myapp .
docker build -t myapp:v1 . # With tag
# Run
docker run -d --name myapp -p 3000:3000 myapp
Multi-Stage Build (Smaller Images)
# Stage 1: Build
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Production (only the built files)
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
EXPOSE 3000
CMD ["node", "dist/server.js"]
The final image only contains the build output — not the source code, dev dependencies, or build tools.
Python Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]
.dockerignore
Like .gitignore — files to exclude from the build context:
node_modules
.git
.env
*.log
dist
__pycache__
.DS_Store
Dockerfile Best Practices
- Use specific base image tags —
node:20-alpinenotnode:latest - Copy dependency files first — Docker caches layers, so
package.jsonchanges less often than source code - Use multi-stage builds — production image doesn't need build tools
- Don't run as root — add
USER nodeorUSER nobody - Use
.dockerignore— keep image small - One process per container — don't run nginx + app + db in one container
Part 6: Docker Compose
Why Compose?
Most applications need multiple containers (app + database + cache + reverse proxy). Compose lets you define them all in one file and manage them together.
Basic compose.yml
services:
web:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./html:/usr/share/nginx/html
depends_on:
- api
api:
build: ./api
environment:
DATABASE_URL: postgres://app:secret@db:5432/myapp
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Compose Commands
# Start all services
docker compose up -d
# Stop all services
docker compose down
# Stop and remove volumes (careful — deletes data)
docker compose down -v
# View logs
docker compose logs -f
docker compose logs api -f # Single service
# Rebuild images and start
docker compose up -d --build
# Scale a service
docker compose up -d --scale api=3
# Execute command in service
docker compose exec api bash
# List services
docker compose ps
# Pull latest images
docker compose pull
# Restart single service
docker compose restart api
More examples: Docker Compose Examples
Part 7: Volumes (Persistent Data)
Containers are ephemeral — data is lost when a container is removed. Volumes persist data.
Named Volumes (Recommended)
# Create
docker volume create mydata
# Use in a container
docker run -d -v mydata:/var/lib/mysql mysql:8
# List volumes
docker volume ls
# Inspect
docker volume inspect mydata
# Remove
docker volume rm mydata
# Remove ALL unused volumes
docker volume prune
Bind Mounts (Host Directories)
# Mount current directory into container
docker run -d -v $(pwd):/app myapp
# Read-only mount
docker run -d -v $(pwd)/config:/etc/nginx/conf.d:ro nginx
| Named Volume | Bind Mount | |
|---|---|---|
| Managed by | Docker | You |
| Location | Docker internal | Anywhere on host |
| Best for | Databases, persistent data | Config files, development |
| Portability | Easy to backup/restore | Host-specific |
Backup a Volume
docker run --rm -v mydata:/data -v $(pwd):/backup alpine \
tar czf /backup/mydata-backup.tar.gz -C /data .
Part 8: Networking
How Containers Talk to Each Other
Containers on the same Docker network can reach each other by container name:
services:
api:
image: myapi
# Can reach database at: postgres://db:5432
db:
image: postgres:16
# Container name "db" is the hostname
Network Types
# List networks
docker network ls
# Create a network
docker network create mynet
# Run container on a network
docker run -d --name web --network mynet nginx
# Connect running container to network
docker network connect mynet web
# Inspect (see connected containers)
docker network inspect mynet
| Network Driver | Use Case |
|---|---|
bridge |
Default. Containers on same host talking to each other |
host |
Container uses host's network directly (no isolation) |
none |
No networking |
overlay |
Multi-host networking (Docker Swarm) |
Part 9: Production Deployment
Health Checks
services:
api:
image: myapi
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
Resource Limits
services:
api:
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
Restart Policies
services:
api:
restart: unless-stopped
# Options: no, always, on-failure, unless-stopped
Logging
services:
api:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
Security
# Don't run as root
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"]
Production Checklist
- Use specific image tags (never
latest) - Set resource limits (memory + CPU)
- Configure health checks
- Set restart policy to
unless-stopped - Limit log size
- Don't run as root
- Use
.dockerignore - Pin dependency versions
- Use multi-stage builds
- Back up volumes regularly
Part 10: Cleanup
# Remove stopped containers
docker container prune
# Remove unused images
docker image prune
docker image prune -a # Including tagged images not in use
# Remove unused volumes
docker volume prune
# Remove unused networks
docker network prune
# Nuclear: remove everything unused
docker system prune -a --volumes
# WARNING: removes ALL unused images, containers, volumes, networks
# Check disk usage
docker system df
docker system df -v # Detailed
Part 11: Troubleshooting
Container Won't Start
# Check logs
docker logs container-name
# Run interactively to see errors
docker run -it image-name bash
# Check if port is already in use
ss -tlnp | grep :8080
Can't Connect to Container
# Check port mapping
docker port container-name
# Check if container is running
docker ps
# Check container IP
docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' container-name
Out of Disk Space
# Check Docker disk usage
docker system df
# Clean up
docker system prune -a
# Check overlay2 storage
du -sh /var/lib/docker/
Container Exits Immediately
# Check exit code
docker inspect --format='{{.State.ExitCode}}' container-name
# Run without detaching to see output
docker run image-name
# Check if CMD/ENTRYPOINT is correct
docker inspect image-name | grep -A5 Cmd