Docker & DevOps — Patrones Modernos & Snippets Reutilizables
Esta sección reúne configuraciones prácticas de Docker, Docker Compose, CI/CD con GitHub Actions, Nginx reverse proxy y estrategias de deployment en producción.
🐳 Dockerfiles Optimizados
Node.js + TypeScript (Multi-stage Build)
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
COPY tsconfig.json ./
# Install dependencies
RUN npm ci --only=production && \
npm cache clean --force
# Copy source code
COPY src ./src
# Build TypeScript
RUN npm run build
# Production stage
FROM node:20-alpine
WORKDIR /app
# Copy only necessary files
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
USER nodejs
EXPOSE 3000
CMD ["node", "dist/index.js"]
Angular/React (Production Build)
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage with Nginx
FROM nginx:alpine
# Copy custom nginx config
COPY nginx.conf /etc/nginx/nginx.conf
# Copy built app
COPY --from=builder /app/dist /usr/share/nginx/html
# Add healthcheck
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --quiet --tries=1 --spider http://localhost:80/health || exit 1
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Next.js Standalone
FROM node:20-alpine AS base
# Dependencies stage
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
# Builder stage
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Production stage
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]
API REST + Database Connection
FROM node:20-alpine
WORKDIR /app
# Install dependencies first (better caching)
COPY package*.json ./
RUN npm ci --only=production
# Copy application code
COPY . .
# Environment variables
ENV NODE_ENV=production
ENV PORT=3000
# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s \
CMD node healthcheck.js
EXPOSE 3000
CMD ["node", "server.js"]
🐙 Docker Compose Completo
Stack Full (Frontend + Backend + DB + Nginx)
version: '3.8'
services:
# PostgreSQL Database
postgres:
image: postgres:16-alpine
container_name: app_postgres
restart: unless-stopped
environment:
POSTGRES_DB: myapp
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- app_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
interval: 10s
timeout: 5s
retries: 5
# Redis Cache
redis:
image: redis:7-alpine
container_name: app_redis
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- redis_data:/data
networks:
- app_network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
# Backend API
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: app_backend
restart: unless-stopped
environment:
NODE_ENV: production
DATABASE_URL: postgresql://${DB_USER}:${DB_PASSWORD}@postgres:5432/myapp
REDIS_URL: redis://redis:6379
JWT_SECRET: ${JWT_SECRET}
PORT: 3000
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
- app_network
volumes:
- ./backend/uploads:/app/uploads
expose:
- "3000"
# Frontend
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
args:
NEXT_PUBLIC_API_URL: ${API_URL}
container_name: app_frontend
restart: unless-stopped
depends_on:
- backend
networks:
- app_network
expose:
- "3000"
# Nginx Reverse Proxy
nginx:
image: nginx:alpine
container_name: app_nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
- ./nginx/logs:/var/log/nginx
depends_on:
- backend
- frontend
networks:
- app_network
volumes:
postgres_data:
driver: local
redis_data:
driver: local
networks:
app_network:
driver: bridge
Development con Hot Reload
version: '3.8'
services:
backend:
build:
context: ./backend
dockerfile: Dockerfile.dev
container_name: dev_backend
volumes:
- ./backend:/app
- /app/node_modules
environment:
NODE_ENV: development
DATABASE_URL: postgresql://dev:dev123@postgres:5432/devdb
ports:
- "3001:3000"
command: npm run dev
networks:
- dev_network
frontend:
build:
context: ./frontend
dockerfile: Dockerfile.dev
container_name: dev_frontend
volumes:
- ./frontend:/app
- /app/node_modules
- /app/.next
environment:
NEXT_PUBLIC_API_URL: http://localhost:3001
ports:
- "3000:3000"
command: npm run dev
networks:
- dev_network
postgres:
image: postgres:16-alpine
container_name: dev_postgres
environment:
POSTGRES_DB: devdb
POSTGRES_USER: dev
POSTGRES_PASSWORD: dev123
ports:
- "5432:5432"
volumes:
- dev_postgres_data:/var/lib/postgresql/data
networks:
- dev_network
volumes:
dev_postgres_data:
networks:
dev_network:
driver: bridge
🔧 Nginx Configurations
Reverse Proxy con SSL
# /etc/nginx/nginx.conf
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# Performance
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
# Rate limiting
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=general_limit:10m rate=30r/s;
# Upstream backends
upstream backend_api {
least_conn;
server backend:3000 max_fails=3 fail_timeout=30s;
}
upstream frontend_app {
least_conn;
server frontend:3000 max_fails=3 fail_timeout=30s;
}
# Redirect HTTP to HTTPS
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$server_name$request_uri;
}
# HTTPS Server
server {
listen 443 ssl http2;
server_name example.com www.example.com;
# SSL Configuration
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# Security Headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# API Routes
location /api {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://backend_api;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# WebSocket Support
location /ws {
proxy_pass http://backend_api;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Static files with caching
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf)$ {
proxy_pass http://frontend_app;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Frontend App
location / {
limit_req zone=general_limit burst=50 nodelay;
proxy_pass http://frontend_app;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
}
Load Balancer con Health Checks
upstream backend_cluster {
least_conn;
server backend1:3000 max_fails=3 fail_timeout=30s;
server backend2:3000 max_fails=3 fail_timeout=30s;
server backend3:3000 max_fails=3 fail_timeout=30s;
keepalive 32;
}
server {
listen 80;
location / {
proxy_pass http://backend_cluster;
proxy_http_version 1.1;
proxy_set_header Connection "";
# Health check
proxy_next_upstream error timeout http_500 http_502 http_503;
}
}
🚀 GitHub Actions CI/CD
Build, Test & Deploy to Production
# .github/workflows/deploy.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# Test Job
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm test -- --coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
# Build and Push Docker Image
build:
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'push'
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=sha,prefix={{branch}}-
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Deploy to Production
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Deploy to server
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /var/www/myapp
docker-compose pull
docker-compose up -d --remove-orphans
docker image prune -f
- name: Health check
run: |
sleep 30
curl -f https://myapp.com/health || exit 1
- name: Notify deployment
if: success()
uses: 8398a7/action-slack@v3
with:
status: custom
custom_payload: |
{
text: "✅ Deployment successful to production",
fields: [
{
title: "Commit",
value: "${{ github.sha }}",
short: true
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
Multi-Environment Deploy
name: Multi-Environment Deploy
on:
push:
branches: [main, staging, develop]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Determine environment
id: env
run: |
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
echo "environment=production" >> $GITHUB_OUTPUT
echo "url=https://app.example.com" >> $GITHUB_OUTPUT
elif [[ "${{ github.ref }}" == "refs/heads/staging" ]]; then
echo "environment=staging" >> $GITHUB_OUTPUT
echo "url=https://staging.example.com" >> $GITHUB_OUTPUT
else
echo "environment=development" >> $GITHUB_OUTPUT
echo "url=https://dev.example.com" >> $GITHUB_OUTPUT
fi
- name: Deploy to ${{ steps.env.outputs.environment }}
uses: appleboy/ssh-action@master
with:
host: ${{ secrets[format('SERVER_HOST_{0}', steps.env.outputs.environment)] }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /var/www/${{ steps.env.outputs.environment }}
git pull origin ${{ github.ref_name }}
docker-compose -f docker-compose.${{ steps.env.outputs.environment }}.yml up -d
- name: Verify deployment
run: |
sleep 20
curl -f ${{ steps.env.outputs.url }}/health || exit 1
📦 Docker Utilities
Docker Compose Commands Útiles
# Build sin cache
docker-compose build --no-cache
# Ver logs de un servicio específico
docker-compose logs -f backend
# Ejecutar comando en contenedor
docker-compose exec backend npm run migrate
# Escalar servicio
docker-compose up -d --scale backend=3
# Ver recursos usados
docker stats
# Limpiar todo (cuidado en producción)
docker system prune -a --volumes
Healthcheck Script (Node.js)
// healthcheck.js
const http = require('http');
const options = {
host: 'localhost',
port: process.env.PORT || 3000,
path: '/health',
timeout: 2000
};
const request = http.request(options, (res) => {
console.log(`STATUS: ${res.statusCode}`);
if (res.statusCode === 200) {
process.exit(0);
} else {
process.exit(1);
}
});
request.on('error', (err) => {
console.log('ERROR:', err);
process.exit(1);
});
request.end();
Entrypoint Script
#!/bin/sh
# entrypoint.sh
set -e
echo "🚀 Starting application..."
# Wait for database
echo "⏳ Waiting for database..."
while ! nc -z postgres 5432; do
sleep 1
done
echo "✅ Database is ready"
# Run migrations
echo "🔄 Running migrations..."
npm run migrate
# Start application
echo "✅ Starting server..."
exec "$@"
🔐 Secrets Management
.env.example
# Database
DB_USER=myuser
DB_PASSWORD=securepassword
DB_NAME=myapp
DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@postgres:5432/${DB_NAME}
# Redis
REDIS_URL=redis://redis:6379
# JWT
JWT_SECRET=your-super-secret-jwt-key
JWT_EXPIRES_IN=7d
# API Keys
API_KEY=your-api-key
STRIPE_SECRET_KEY=sk_test_xxx
# Frontend
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_WS_URL=wss://api.example.com
# Email
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-password
# Monitoring
SENTRY_DSN=https://xxx@sentry.io/xxx
🎯 Best Practices
- Multi-stage builds para imágenes pequeñas
- Non-root users en contenedores para seguridad
- Health checks en todos los servicios críticos
- Volumes para persistencia de datos
- Networks aisladas por stack
- Environment variables nunca hardcodeadas
- Logs centralizados y rotativos
- Backup automatizado de volúmenes
- CI/CD con tests antes de deploy
- Monitoring con health endpoints