Docker for Developers: Complete Guide to Containerization
Docker has revolutionized how we develop, ship, and run applications. This comprehensive guide will take you from Docker basics to advanced containerization strategies.
Why Docker?
Problems Docker Solves
- "It works on my machine" syndrome
- Environment inconsistencies
- Complex dependency management
- Deployment complications
- Resource isolation issues
Benefits
- Consistency: Same environment everywhere
- Portability: Run anywhere Docker runs
- Efficiency: Lightweight compared to VMs
- Scalability: Easy horizontal scaling
- Isolation: Secure application separation
Docker Fundamentals
Core Concepts
- Image: Read-only template for containers
- Container: Running instance of an image
- Dockerfile: Instructions to build an image
- Registry: Storage for Docker images
- Volume: Persistent data storage
Basic Commands
# Pull an image
docker pull nginx:latest
# Run a container
docker run -d -p 80:80 --name my-nginx nginx
# List running containers
docker ps
# Stop a container
docker stop my-nginx
# Remove a container
docker rm my-nginx
# List images
docker images
# Remove an image
docker rmi nginx:latest
Creating Your First Dockerfile
Node.js Application Example
# Use official Node.js runtime as base 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 . .
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
# Change ownership of app directory
RUN chown -R nextjs:nodejs /app
USER nextjs
# Expose port
EXPOSE 3000
# Define health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
# Start the application
CMD ["npm", "start"]
Multi-stage Build
# 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 AS production
WORKDIR /app
# Copy only necessary files from builder
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
# Install only production dependencies
RUN npm ci --only=production && npm cache clean --force
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nextjs -u 1001
USER nextjs
EXPOSE 3000
CMD ["npm", "start"]
Docker Compose for Development
Basic docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://user:password@db:5432/myapp
volumes:
- .:/app
- /app/node_modules
depends_on:
- db
- redis
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: user
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:
Development with Hot Reload
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
environment:
- CHOKIDAR_USEPOLLING=true
command: npm run dev
Best Practices
Dockerfile Optimization
1. Use Specific Tags
# Bad
FROM node
# Good
FROM node:18.17.0-alpine
2. Minimize Layers
# Bad
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
# Good
RUN apt-get update && \
apt-get install -y curl git && \
rm -rf /var/lib/apt/lists/*
3. Leverage Build Cache
# Copy package files first
COPY package*.json ./
RUN npm ci
# Then copy source code
COPY . .
4. Use .dockerignore
node_modules
npm-debug.log
.git
.gitignore
README.md
.env
.nyc_output
coverage
.nyc_output
Security Best Practices
1. Use Non-root User
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
USER nextjs
2. Scan for Vulnerabilities
# Scan image for vulnerabilities
docker scout cves my-app:latest
# Use Trivy
trivy image my-app:latest
3. Use Minimal Base Images
# Use Alpine or distroless images
FROM node:18-alpine
# or
FROM gcr.io/distroless/nodejs18-debian11
Development Workflows
Local Development Setup
# Start development environment
docker-compose up -d
# View logs
docker-compose logs -f app
# Execute commands in container
docker-compose exec app npm run test
# Stop environment
docker-compose down
Database Migrations
# Run migrations
docker-compose exec app npm run migrate
# Seed database
docker-compose exec app npm run seed
Testing in Containers
# Test stage in multi-stage build
FROM node:18-alpine AS test
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run test
RUN npm run lint
Production Deployment
Production docker-compose.yml
version: '3.8'
services:
app:
image: my-app:latest
restart: unless-stopped
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=${DATABASE_URL}
depends_on:
- db
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./ssl:/etc/nginx/ssl
depends_on:
- app
db:
image: postgres:15-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
CI/CD Pipeline
# GitHub Actions example
name: Build and Deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build Docker image
run: docker build -t my-app:${{ github.sha }} .
- name: Run tests
run: docker run --rm my-app:${{ github.sha }} npm test
- name: Push to registry
run: |
echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
docker push my-app:${{ github.sha }}
Monitoring and Logging
Health Checks
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
Logging Configuration
services:
app:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
Resource Limits
services:
app:
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
Troubleshooting
Common Issues
1. Container Exits Immediately
# Check logs
docker logs container-name
# Run interactively
docker run -it my-app:latest /bin/sh
2. Permission Issues
# Check file ownership
docker run --rm -v $(pwd):/app alpine ls -la /app
# Fix permissions
docker run --rm -v $(pwd):/app alpine chown -R $(id -u):$(id -g) /app
3. Network Issues
# Inspect network
docker network ls
docker network inspect bridge
# Test connectivity
docker run --rm alpine ping container-name
Advanced Topics
Custom Networks
networks:
frontend:
driver: bridge
backend:
driver: bridge
services:
app:
networks:
- frontend
- backend
db:
networks:
- backend
Secrets Management
secrets:
db_password:
file: ./db_password.txt
services:
db:
secrets:
- db_password
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
Conclusion
Docker transforms development workflows by providing:
- Consistent Environments: Eliminate environment-related bugs
- Easy Scaling: Scale applications horizontally
- Simplified Deployment: Deploy anywhere Docker runs
- Resource Efficiency: Better resource utilization
- Development Speed: Faster onboarding and development
Master these Docker concepts and practices to build more reliable, scalable applications. Start containerizing your projects today!
Ready to containerize your applications? Start with a simple Dockerfile and gradually adopt more advanced patterns.