What You'll Learn
A hands-on introduction to Docker: images, containers, Dockerfile, volumes, networks, and docker-compose — everything you need to containerize and run any application.
Why Docker? The Problem It Solves
Before Docker, the classic developer excuse was "it works on my machine". Different OS versions, library conflicts, and environment inconsistencies meant that code working on a developer's laptop would fail in staging or production.
Docker solves this by packaging your application and all its dependencies into a container — a lightweight, portable, self-sufficient unit that runs the same way everywhere.
Isolation
Each container runs in its own isolated environment
Portability
Build once, run anywhere — dev, staging, production
Speed
Starts in milliseconds vs minutes for VMs
Docker Architecture
Docker uses a client-server architecture:
- Docker Client — The CLI you type commands into (
docker build,docker run) - Docker Daemon (dockerd) — The background service that builds, runs, and manages containers
- Docker Registry — Where images are stored (Docker Hub, AWS ECR, GCR)
Essential Docker Commands
# === IMAGES ===
docker pull nginx:latest # Download image from Docker Hub
docker images # List local images
docker image inspect nginx # Detailed image info
docker rmi nginx:latest # Remove image
docker build -t myapp:1.0 . # Build image from Dockerfile
docker tag myapp:1.0 myrepo/myapp:1.0 # Tag for pushing
# === CONTAINERS ===
docker run nginx # Run container (foreground)
docker run -d nginx # Run detached (background)
docker run -d -p 8080:80 nginx # Map port host:container
docker run -d --name my-nginx nginx # Named container
docker run --rm nginx # Auto-remove when stopped
docker ps # Running containers
docker ps -a # All containers (including stopped)
docker stop my-nginx # Graceful stop
docker start my-nginx # Start stopped container
docker restart my-nginx # Restart
docker rm my-nginx # Remove container
docker rm -f my-nginx # Force remove (even if running)
# === INTERACT WITH CONTAINER ===
docker exec -it my-nginx bash # Open bash shell in container
docker exec -it my-nginx sh # Use sh if bash not available
docker logs my-nginx # View container logs
docker logs -f my-nginx # Follow logs in real-time
docker inspect my-nginx # Detailed container config/state
docker stats # Live resource usage stats
docker cp my-nginx:/etc/nginx.conf ./nginx.conf # Copy file from container
# === CLEANUP ===
docker system prune # Remove all unused data
docker system prune -a # Remove all unused images too
docker volume prune # Remove unused volumes
Writing a Dockerfile
A Dockerfile is a text file with instructions for building a Docker image. Each instruction creates a new layer in the image.
# Stage 1: Build
FROM node:20-alpine AS builder
# Set working directory
WORKDIR /app
# Copy dependency files first (cache optimization!)
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy application source
COPY . .
# Build the app
RUN npm run build
# ─────────────────────────────────────────────
# Stage 2: Production (smaller final image)
FROM node:20-alpine AS production
WORKDIR /app
# Create non-root user for security
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# Copy only production artifacts
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package.json .
# Set correct ownership
RUN chown -R appuser:appgroup /app
# Switch to non-root user
USER appuser
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s \
CMD node healthcheck.js || exit 1
# Start application
CMD ["node", "dist/server.js"]
Dockerfile Best Practices
- ✅ Use
.dockerignoreto exclude node_modules, .git - ✅ Copy package.json BEFORE source code (layer cache)
- ✅ Use multi-stage builds to reduce final image size
- ✅ Run as non-root user in production
- ✅ Pin specific image tags (not
:latest) - ✅ Add HEALTHCHECK instruction
- ✅ One process per container
Common Mistakes
- ❌ Running as root (security risk)
- ❌ Storing secrets in Dockerfile (use ENV vars or secrets)
- ❌ Using
:latesttag (non-deterministic) - ❌ Not using .dockerignore (bloated context)
- ❌ Combining RUN commands wastefully
- ❌ Installing dev dependencies in production
- ❌ Not using multi-stage builds
Docker Volumes — Persisting Data
# Named volumes (managed by Docker)
docker volume create mydata
docker run -v mydata:/var/lib/postgresql/data postgres
# Bind mounts (host path → container path)
docker run -v /host/path:/container/path nginx
docker run -v $(pwd)/nginx.conf:/etc/nginx/nginx.conf:ro nginx # Read-only
# Anonymous volumes (avoid — hard to manage)
docker run -v /var/lib/mysql mysql
# Volume commands
docker volume ls # List volumes
docker volume inspect mydata # Volume details
docker volume rm mydata # Remove volume
docker volume prune # Remove unused volumes
Docker Compose — Multi-Container Apps
Docker Compose lets you define and run multi-container applications using a YAML file. Essential for local development.
version: '3.8'
services:
# Nginx reverse proxy
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./certbot/conf:/etc/letsencrypt
depends_on:
- app
restart: unless-stopped
# Node.js application
app:
build:
context: .
dockerfile: Dockerfile
target: production
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://user:pass@postgres:5432/mydb
- REDIS_URL=redis://redis:6379
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
# PostgreSQL database
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: mydb
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
interval: 10s
timeout: 5s
retries: 5
# Redis cache
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:
networks:
default:
name: myapp-network
docker-compose up -d # Start all services (detached)
docker-compose up --build # Rebuild images before starting
docker-compose down # Stop and remove containers
docker-compose down -v # Also remove volumes
docker-compose logs -f app # Follow logs for app service
docker-compose exec app sh # Shell into app container
docker-compose ps # Status of all services
docker-compose restart nginx # Restart specific service
docker-compose scale app=3 # Scale app to 3 instances
Production Tips
- Use Docker secrets — Never put passwords in docker-compose.yml. Use Docker Secrets or environment files.
- Resource limits — Always set memory/CPU limits:
mem_limit: 512m - Restart policies — Use
restart: unless-stoppedfor production services. - Health checks — Add healthchecks so Compose knows when a service is truly ready.
- Multi-stage builds — A typical Node.js image can go from 1.2GB to under 150MB with multi-stage builds.
Knowledge Check
Test what you've learned in this article.