← Back to home
Docker Development Workflow: From Dev to Production
Master Docker for development environments and production deployments
dockerdevopscontainersdevelopment
Docker Development Workflow: From Dev to Production
Docker revolutionizes how we develop, test, and deploy applications. Learn how to create an efficient Docker workflow for your projects.
Development Environment Setup
Basic Dockerfile
# Multi-stage build for Node.js app
FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production && npm cache clean --force
# Development stage
FROM base AS dev
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]
# Build stage
FROM base AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM base AS production
WORKDIR /app
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
USER nextjs
EXPOSE 3000
ENV NODE_ENV=production
CMD ["npm", "start"]
Docker Compose for Development
# docker-compose.yml
version: '3.8'
services:
app:
build:
context: .
target: dev
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
- /app/.next
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://user:password@db:5432/myapp
- REDIS_URL=redis://redis:6379
depends_on:
- db
- redis
restart: unless-stopped
db:
image: postgres:15-alpine
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./docker/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql
restart: unless-stopped
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf
depends_on:
- app
restart: unless-stopped
volumes:
postgres_data:
redis_data:
Development Best Practices
.dockerignore File
# .dockerignore
node_modules
npm-debug.log
Dockerfile
.dockerignore
.git
.gitignore
README.md
.env
.env.local
.env.production.local
.env.development.local
.nyc_output
coverage
.next
out
build
dist
Environment-Specific Configurations
# docker-compose.override.yml (automatically loaded in development)
version: '3.8'
services:
app:
build:
context: .
target: dev
volumes:
- .:/app
- /app/node_modules
environment:
- DEBUG=true
- HOT_RELOAD=true
db:
environment:
- POSTGRES_DB=myapp_dev
ports:
- "5432:5432"
# docker-compose.prod.yml (for production)
version: '3.8'
services:
app:
build:
context: .
target: production
restart: always
environment:
- NODE_ENV=production
db:
environment:
- POSTGRES_DB=myapp_prod
ports: [] # Don't expose ports in production
Multi-Stage Builds
Optimized Python Dockerfile
# Python multi-stage build
FROM python:3.11-slim as base
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
# Install system dependencies
RUN apt-get update && apt-get install -y \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Development stage
FROM base as development
WORKDIR /app
COPY requirements.txt requirements-dev.txt ./
RUN pip install -r requirements-dev.txt
COPY . .
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
# Production dependencies stage
FROM base as deps
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-deps -r requirements.txt
# Production stage
FROM python:3.11-slim as production
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
RUN groupadd -r appuser && useradd -r -g appuser appuser
WORKDIR /app
# Copy dependencies from deps stage
COPY --from=deps /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=deps /usr/local/bin /usr/local/bin
COPY . .
RUN chown -R appuser:appuser /app
USER appuser
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "myapp.wsgi:application"]
Container Orchestration
Docker Swarm Stack
# docker-stack.yml
version: '3.8'
services:
app:
image: myapp:latest
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL_FILE=/run/secrets/db_url
secrets:
- db_url
deploy:
replicas: 3
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
update_config:
parallelism: 1
delay: 10s
failure_action: rollback
rollback_config:
parallelism: 1
delay: 5s
resources:
limits:
memory: 512M
reservations:
memory: 256M
networks:
- app-network
db:
image: postgres:15
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER_FILE=/run/secrets/db_user
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
secrets:
- db_user
- db_password
volumes:
- postgres_data:/var/lib/postgresql/data
deploy:
replicas: 1
restart_policy:
condition: on-failure
networks:
- app-network
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
configs:
- source: nginx_config
target: /etc/nginx/nginx.conf
deploy:
replicas: 2
restart_policy:
condition: on-failure
networks:
- app-network
secrets:
db_url:
external: true
db_user:
external: true
db_password:
external: true
configs:
nginx_config:
file: ./nginx.conf
volumes:
postgres_data:
networks:
app-network:
driver: overlay
CI/CD Pipeline
GitHub Actions with Docker
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build test image
uses: docker/build-push-action@v4
with:
context: .
target: dev
tags: myapp:test
load: true
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Run tests
run: |
docker run --rm \
--network host \
-e DATABASE_URL=postgresql://postgres:postgres@localhost:5432/test_db \
myapp:test npm test
build-and-push:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Log in to Container Registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
target: production
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
needs: build-and-push
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to production
run: |
# Deploy using your preferred method
# (Docker Swarm, Kubernetes, etc.)
echo "Deploying to production..."
Health Checks and Monitoring
Health Check Implementation
# Add health check to Dockerfile
FROM node:18-alpine
# ... other instructions ...
# Add health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node healthcheck.js || exit 1
EXPOSE 3000
CMD ["npm", "start"]
// healthcheck.js
const http = require('http');
const options = {
hostname: 'localhost',
port: 3000,
path: '/health',
method: 'GET',
timeout: 2000
};
const request = http.request(options, (res) => {
if (res.statusCode === 200) {
process.exit(0);
} else {
process.exit(1);
}
});
request.on('error', () => {
process.exit(1);
});
request.on('timeout', () => {
request.destroy();
process.exit(1);
});
request.end();
Monitoring with Prometheus
# docker-compose.monitoring.yml
version: '3.8'
services:
prometheus:
image: prom/prometheus
ports:
- "9090:9090"
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.console.libraries=/etc/prometheus/console_libraries'
- '--web.console.templates=/etc/prometheus/consoles'
grafana:
image: grafana/grafana
ports:
- "3001:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana_data:/var/lib/grafana
- ./monitoring/grafana/dashboards:/etc/grafana/provisioning/dashboards
- ./monitoring/grafana/datasources:/etc/grafana/provisioning/datasources
cadvisor:
image: gcr.io/cadvisor/cadvisor
ports:
- "8080:8080"
volumes:
- /:/rootfs:ro
- /var/run:/var/run:rw
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
volumes:
prometheus_data:
grafana_data:
Development Commands
Useful Docker Commands
# Development workflow
docker-compose up --build # Build and start services
docker-compose up -d # Start in detached mode
docker-compose logs -f app # Follow app logs
docker-compose exec app sh # Shell into app container
docker-compose down -v # Stop and remove volumes
# Production deployment
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# Clean up
docker system prune -f # Remove unused containers/images
docker volume prune -f # Remove unused volumes
docker image prune -a -f # Remove unused images
# Debugging
docker-compose ps # Show running containers
docker-compose top # Show processes
docker stats # Show container resource usage
# Database operations
docker-compose exec db psql -U user myapp # Connect to database
docker-compose exec db pg_dump -U user myapp > backup.sql
Development Scripts
{
"scripts": {
"docker:dev": "docker-compose up --build",
"docker:prod": "docker-compose -f docker-compose.yml -f docker-compose.prod.yml up --build",
"docker:test": "docker-compose -f docker-compose.test.yml up --build --abort-on-container-exit",
"docker:clean": "docker system prune -f && docker volume prune -f",
"docker:shell": "docker-compose exec app sh",
"docker:logs": "docker-compose logs -f app",
"docker:db": "docker-compose exec db psql -U user myapp"
}
}
Security Best Practices
Secure Dockerfile
# Security-focused Dockerfile
FROM node:18-alpine
# Don't run as root
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production && npm cache clean --force
# Copy source code
COPY --chown=nextjs:nodejs . .
# Remove unnecessary packages
RUN apk del --purge
# Switch to non-root user
USER nextjs
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s CMD node healthcheck.js
# Start application
CMD ["npm", "start"]
Conclusion
Docker provides a powerful foundation for modern development workflows. By implementing proper containerization strategies, you can achieve:
- Consistent development environments
- Simplified deployment processes
- Better resource utilization
- Improved scalability and maintainability
Start with basic containerization and gradually adopt advanced patterns as your needs grow.