Docker for Developers: Beyond the Basics
Docker has become essential in modern development, but many developers stay with the basics. In this post, I share advanced techniques I use daily to optimize workflows and improve efficiency.
Multi-Stage Builds: The Game Changer
Multi-stage builds allow creating smaller and more secure images:
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Production stage
FROM node:18-alpine AS production
WORKDIR /app
# Copy only production dependencies
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]
Result: 800MB → 150MB
Advanced Docker Compose
1. Profiles for Different Environments
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
depends_on:
- db
- redis
profiles: ["app"]
db:
image: postgres:15
environment:
POSTGRES_DB: myapp
POSTGRES_USER: user
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
profiles: ["app", "db-only"]
redis:
image: redis:7-alpine
profiles: ["app", "cache"]
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
depends_on:
- app
profiles: ["production"]
volumes:
postgres_data:
Usage:
# Development
docker-compose --profile app up
# Database only for testing
docker-compose --profile db-only up
# Production with proxy
docker-compose --profile production up
2. Override Files for Environments
# docker-compose.override.yml (development)
version: '3.8'
services:
app:
volumes:
- .:/app
- /app/node_modules
environment:
- NODE_ENV=development
command: npm run dev
# docker-compose.prod.yml (production)
version: '3.8'
services:
app:
restart: unless-stopped
environment:
- NODE_ENV=production
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M
Performance Optimizations
1. Smart Layer Caching
FROM node:18-alpine
WORKDIR /app
# Copy only package.json first (caching)
COPY package*.json ./
RUN npm ci --only=production
# Copy code after
COPY . .
# Build only if code changed
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
2. Optimized .dockerignore
# Dependencies
node_modules
npm-debug.log
# Development files
.git
.gitignore
README.md
.env
.nyc_output
coverage
.npm
# Local build files
dist
build
# IDE files
.vscode
.idea
*.swp
*.swo
# Logs
logs
*.log
# Cache
.cache
.parcel-cache
Development with Hot Reload
Development Dockerfile:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
# Install nodemon globally
RUN npm install -g nodemon
COPY . .
EXPOSE 3000
# Use nodemon in development
CMD ["nodemon", "--legacy-watch", "src/index.js"]
Docker Compose with volumes:
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules # Anonymous volume for node_modules
environment:
- NODE_ENV=development
Health Checks and Monitoring
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
CMD ["npm", "start"]
Implementing health endpoint:
// src/health.js
app.get('/health', (req, res) => {
const healthcheck = {
uptime: process.uptime(),
message: 'OK',
timestamp: Date.now(),
checks: {
database: checkDatabase(),
redis: checkRedis(),
memory: process.memoryUsage()
}
};
res.status(200).json(healthcheck);
});
BuildKit and Advanced Features
1. Parallel Builds
# syntax=docker/dockerfile:1
FROM node:18-alpine AS base
WORKDIR /app
COPY package*.json ./
FROM base AS dependencies
RUN npm ci --only=production
FROM base AS build
RUN npm ci
COPY . .
RUN npm run build
FROM dependencies AS final
COPY --from=build /app/dist ./dist
CMD ["node", "dist/index.js"]
2. Cache Mounts
# syntax=docker/dockerfile:1
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
# Mount cache for npm
RUN --mount=type=cache,target=/root/.npm \
npm ci --only=production
COPY . .
CMD ["npm", "start"]
Security in Containers
1. Non-root User
FROM node:18-alpine
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
WORKDIR /app
COPY . .
RUN npm ci --only=production
# Change ownership of files
RUN chown -R nextjs:nodejs /app
# Switch to non-root user
USER nextjs
EXPOSE 3000
CMD ["npm", "start"]
2. Vulnerability Scanning
# Image scan
docker scan myapp:latest
# Using Trivy
trivy image myapp:latest
# CI/CD integration
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy image myapp:latest
Automation Scripts
Makefile for Docker:
.PHONY: build run test clean
IMAGE_NAME = myapp
VERSION = $(shell git rev-parse --short HEAD)
build:
docker build -t $(IMAGE_NAME):$(VERSION) .
docker tag $(IMAGE_NAME):$(VERSION) $(IMAGE_NAME):latest
run:
docker-compose up -d
test:
docker run --rm $(IMAGE_NAME):latest npm test
clean:
docker system prune -f
docker volume prune -f
push:
docker push $(IMAGE_NAME):$(VERSION)
docker push $(IMAGE_NAME):latest
dev:
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up
prod:
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Debugging in Containers
1. Debug with Node.js
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN npm install
# Expose debug port
EXPOSE 3000 9229
# Command with debug enabled
CMD ["node", "--inspect=0.0.0.0:9229", "src/index.js"]
2. Docker Compose for debug:
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
- "9229:9229" # Debug port
volumes:
- .:/app
- /app/node_modules
environment:
- NODE_ENV=development
Monitoring and Logs
1. Centralized Logging
version: '3.8'
services:
app:
build: .
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
fluentd:
image: fluent/fluentd:latest
volumes:
- ./fluent.conf:/fluentd/etc/fluent.conf
depends_on:
- elasticsearch
2. Metrics with Prometheus
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
- "9090:9090" # Metrics
prometheus:
image: prom/prometheus
ports:
- "9091:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
Conclusion
Docker offers much more than basic containerization. With these advanced techniques, you can:
- Reduce image size significantly
- Speed up builds with smart caching
- Improve security with proper practices
- Automate workflows for development and deployment
- Monitor applications effectively
The investment in learning these techniques pays off quickly in productivity and code quality.
Want to discuss more about Docker and DevOps? Find me on LinkedIn or check my projects on GitHub.