Why Docker Matters
Docker solves the “works on my machine” problem by packaging applications with their dependencies into containers. Whether you’re building microservices, deploying to the cloud, or just want consistent development environments, Docker is essential.
Understanding Containers vs Virtual Machines
Virtual Machines:
- Full OS per VM
- Heavy resource usage (GBs of RAM, disk)
- Slow to start (minutes)
- Strong isolation
Containers:
- Share host OS kernel
- Lightweight (MBs)
- Fast startup (seconds)
- Process-level isolation
When to use what: VMs for complete isolation and different OS requirements; containers for application deployment and microservices.
Docker Architecture
Docker Client (docker CLI)
↓
Docker Daemon
↓
┌───┴───┐
Images Containers
- Images: Read-only templates (like classes)
- Containers: Running instances of images (like objects)
- Dockerfile: Instructions to build an image
- Docker Hub: Public registry for images
Your First Container
# Run a container
docker run hello-world
# Run interactively
docker run -it ubuntu bash
# Run in background
docker run -d nginx
# Port mapping
docker run -d -p 8080:80 nginx
# Access at http://localhost:8080
Essential Docker Commands
Container Management
# List running containers
docker ps
# List all containers (including stopped)
docker ps -a
# Stop a container
docker stop <container-id>
# Remove a container
docker rm <container-id>
# View logs
docker logs <container-id>
# Execute command in running container
docker exec -it <container-id> bash
Image Management
# List images
docker images
# Pull an image
docker pull node:18
# Remove an image
docker rmi <image-id>
# Build an image
docker build -t myapp:1.0 .
# Tag an image
docker tag myapp:1.0 myapp:latest
Writing Dockerfiles
Node.js Application
# Use official Node.js image
FROM node:18-alpine
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy application code
COPY . .
# Expose port
EXPOSE 3000
# Set user (security best practice)
USER node
# Start application
CMD ["node", "server.js"]
Multi-Stage Build (Smaller Images)
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]
Benefits:
- Smaller final image (no build tools)
- Faster deployment
- More secure (fewer dependencies)
Dockerfile Best Practices
1. Layer Caching
# ❌ Bad: Invalidates cache on any code change
COPY . .
RUN npm install
# ✅ Good: Cache dependencies separately
COPY package*.json ./
RUN npm ci
COPY . .
2. Use .dockerignore
node_modules
npm-debug.log
.git
.env
*.md
.vscode
.idea
dist
coverage
3. Alpine for Smaller Images
# Large: 900MB
FROM node:18
# Small: 170MB
FROM node:18-alpine
4. Non-Root User
# Create user
RUN addgroup -g 1001 -S nodejs \
&& adduser -S nodejs -u 1001
# Or use built-in node user
USER node
5. Healthchecks
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD node healthcheck.js
Docker Volumes: Persisting Data
# Named volume
docker run -v mydata:/app/data myapp
# Bind mount (development)
docker run -v $(pwd):/app myapp
# List volumes
docker volume ls
# Inspect volume
docker volume inspect mydata
# Remove unused volumes
docker volume prune
When to use what:
- Named volumes: Production databases, persistent app data
- Bind mounts: Local development (live code reloading)
- tmpfs: Temporary data, sensitive info that shouldn’t persist
Docker Networks
# Create network
docker network create mynetwork
# Run containers on same network
docker run -d --network mynetwork --name db postgres
docker run -d --network mynetwork --name api myapi
# API can access DB at hostname "db"
Network Types
- bridge: Default, isolated network
- host: Use host’s network (no isolation)
- none: No networking
- Custom bridge: Named networks with DNS
Docker Compose: Multi-Container Apps
docker-compose.yml
version: '3.8'
services:
# Database
db:
image: postgres:15
environment:
POSTGRES_PASSWORD: password
POSTGRES_DB: myapp
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
# Redis cache
redis:
image: redis:7-alpine
ports:
- "6379:6379"
# API server
api:
build: ./api
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://postgres:password@db:5432/myapp
REDIS_URL: redis://redis:6379
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
volumes:
- ./api:/app
- /app/node_modules
develop:
watch:
- path: ./api
action: sync
target: /app
volumes:
db-data:
Compose Commands
# Start all services
docker-compose up
# Start in background
docker-compose up -d
# Rebuild images
docker-compose up --build
# Stop services
docker-compose down
# Stop and remove volumes
docker-compose down -v
# View logs
docker-compose logs -f api
# Execute command
docker-compose exec api sh
Development Workflow
Hot Reload Setup
services:
dev:
build:
context: .
target: development
volumes:
- .:/app
- /app/node_modules
environment:
NODE_ENV: development
command: npm run dev
Multi-Stage Dockerfile
# Development stage
FROM node:18-alpine AS development
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]
# Production stage
FROM node:18-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
CMD ["node", "server.js"]
Production Deployment
1. Build Optimized Image
# Multi-stage build
docker build -t myapp:1.0 .
# Check image size
docker images myapp:1.0
2. Security Scanning
# Scan for vulnerabilities
docker scan myapp:1.0
# Or use Trivy
trivy image myapp:1.0
3. Push to Registry
# Tag for registry
docker tag myapp:1.0 registry.example.com/myapp:1.0
# Push
docker push registry.example.com/myapp:1.0
4. Production Compose
version: '3.8'
services:
api:
image: registry.example.com/myapp:1.0
restart: unless-stopped
ports:
- "3000:3000"
environment:
NODE_ENV: production
DATABASE_URL_FILE: /run/secrets/db_url
secrets:
- db_url
deploy:
replicas: 3
resources:
limits:
cpus: '0.5'
memory: 512M
secrets:
db_url:
external: true
Common Patterns
Database Initialization
services:
db:
image: postgres:15
volumes:
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
Wait for Service
# Install wait-for-it
COPY --from=wait-for-it:latest /wait-for-it.sh /usr/local/bin/
CMD ["/usr/local/bin/wait-for-it.sh", "db:5432", "--", "node", "server.js"]
Environment Variables from File
services:
api:
env_file:
- .env
- .env.local
Debugging Containers
Inspect Container
# View container details
docker inspect <container-id>
# View specific field
docker inspect -f '{{.State.Status}}' <container-id>
# View network settings
docker inspect -f '{{.NetworkSettings.IPAddress}}' <container-id>
Debug Running Container
# Shell into container
docker exec -it <container-id> sh
# View processes
docker top <container-id>
# View resource usage
docker stats <container-id>
# View logs
docker logs -f --tail 100 <container-id>
Debug Build Issues
# Build with no cache
docker build --no-cache -t myapp .
# See build context
docker build --progress=plain -t myapp .
# Run specific stage
docker build --target development -t myapp:dev .
Performance Optimization
1. Layer Optimization
# Combine commands to reduce layers
RUN apt-get update && apt-get install -y \
package1 \
package2 \
&& rm -rf /var/lib/apt/lists/*
2. Build Cache
# Use BuildKit for better caching
DOCKER_BUILDKIT=1 docker build -t myapp .
3. Image Registry Caching
# Pull cache from registry
FROM --platform=$BUILDPLATFORM registry.example.com/myapp:cache AS cache
Troubleshooting
Container Exits Immediately
# Check exit code
docker ps -a
# View logs
docker logs <container-id>
# Try running with shell
docker run -it myapp sh
Out of Disk Space
# Clean up
docker system prune -a --volumes
# View disk usage
docker system df
Network Issues
# Test connectivity
docker run --network mynetwork alpine ping db
# Inspect network
docker network inspect mynetwork
Docker in CI/CD
GitHub Actions
name: Build and Push
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Push to registry
run: |
echo ${{ secrets.REGISTRY_PASSWORD }} | docker login -u ${{ secrets.REGISTRY_USERNAME }} --password-stdin
docker push myapp:${{ github.sha }}
Best Practices Summary
- Use official base images from Docker Hub
- Multi-stage builds for smaller production images
- Run as non-root user for security
- .dockerignore to exclude unnecessary files
- Layer caching by ordering instructions properly
- Health checks for container monitoring
- Named volumes for persistent data
- Environment variables for configuration
- Docker Compose for local development
- Security scanning before production deployment
Next Steps
- Learn Kubernetes for orchestration at scale
- Explore Docker Swarm for simpler clustering
- Study container security best practices
- Implement monitoring with Prometheus/Grafana
- Try Buildpacks as an alternative to Dockerfiles
