Docker has become essential for modern software deployment. Learn how to containerize applications and deploy them reliably to production environments.
What is Docker?
Docker is a containerization platform that packages your application, dependencies, and configuration into a single unit called a container. This ensures your application runs consistently across different environments.
Why Docker?
- Consistency - "It works on my machine" problem solved
- Isolation - Each container runs independently
- Scalability - Easy to run multiple instances
- Efficiency - Lightweight compared to virtual machines
Docker Fundamentals
Images vs Containers
Docker Image: A blueprint containing your application code and dependencies Docker Container: A running instance of an image
Think of it like a class (image) and an object (container).
Basic Dockerfile
# Start from a 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 . .
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
CMD node healthcheck.js
# Run application
CMD ["node", "server.js"]
Production Best Practices
1. Multi-Stage Builds
Reduce image size by using multiple build stages:
# Build stage
FROM node:18 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
EXPOSE 3000
CMD ["node", "dist/server.js"]
This reduces final image size from 1GB to ~200MB.
2. Security Considerations
Use Non-Root User
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001
USER nodejs
Scan for Vulnerabilities
# Scan image for security issues
docker scan myapp:latest
# Use Trivy for detailed scanning
trivy image myapp:latest
Keep Base Images Updated
# Check for updates regularly
docker pull node:18-alpine
3. Optimize Image Size
Techniques:
- Use slim/alpine base images
- Remove unnecessary files
- Combine RUN commands to reduce layers
- Use .dockerignore file
node_modules
npm-debug.log
.git
.env
.vscode
__tests__
coverage
Docker Compose for Local Development
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://user:password@db:5432/myapp
- NODE_ENV=development
depends_on:
- db
volumes:
- .:/app
- /app/node_modules
db:
image: postgres:15-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: myapp
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
Deploying to Production
Option 1: Docker Swarm
# Initialize swarm
docker swarm init
# Deploy stack
docker stack deploy -c docker-compose.prod.yml myapp
Option 2: Kubernetes
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: myregistry/myapp:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "production"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
Option 3: Docker Hub + CI/CD
# GitHub Actions workflow
name: Deploy to Docker Hub
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build and push
uses: docker/build-push-action@v2
with:
push: true
tags: myregistry/myapp:latest
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
Monitoring and Logging
Health Checks
# Check container health
docker inspect --format='{{.State.Health.Status}}' myapp
View Logs
# Follow logs in real-time
docker logs -f myapp
# Get last 100 lines
docker logs --tail=100 myapp
Monitoring Tools
- Prometheus - Metrics collection
- Grafana - Visualization
- ELK Stack - Logging
- DataDog - Full observability
Common Pitfalls to Avoid
| Issue | Solution |
|---|---|
| Running as root | Create non-root user |
| Large images | Use multi-stage builds |
| Hardcoded secrets | Use environment variables |
| No health checks | Implement HEALTHCHECK |
| Outdated base images | Update regularly |
| No resource limits | Set CPU/memory limits |
Performance Tips
# Build with buildkit for better caching
export DOCKER_BUILDKIT=1
docker build -t myapp:latest .
# Use --cache-from to speed up builds
docker build --cache-from myapp:latest -t myapp:latest .
# Limit build context with .dockerignore
Conclusion
Docker is powerful but requires careful attention to best practices. Follow these guidelines for secure, efficient, scalable deployments.
Key Takeaways:
- Use multi-stage builds to reduce image size
- Never run containers as root
- Implement health checks
- Use .dockerignore to reduce context
- Monitor and log everything
- Keep base images updated
Ready to containerize your application?