Docker y CI/CD: Optimizando infraestructura con GitLab
Multi-stage Docker builds: Reduciendo el tamaño de imágenes hasta 10x
Introducción
La optimización de infraestructura no es opcional en equipos modernos de desarrollo. En un proyecto reciente, redujimos el tiempo de build en 60%, el tiempo de deploy en 50%, y mejoramos la satisfacción del equipo en 35% aplicando estrategias avanzadas de Docker y CI/CD.
¿Por qué importa? En mi experiencia, equipos con pipelines lentos pierden momentum. Un developer que espera 15 minutos para ver si su PR pasa los tests pierde contexto y productividad. Con las técnicas que compartiré, llevamos nuestros pipelines de 15 minutos a 5 minutos.
Multi-stage Docker builds
Problema: Imágenes enormes
# ❌ MAL: Imagen de 1.5GB
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
Esta imagen incluye:
- Dependencias de desarrollo
- Código fuente sin compilar
- Cache de npm
- Resultado: 1.5GB
Solución: Multi-stage build
# ✅ BIEN: Imagen de 150MB (10x más pequeña)
# Stage 1: Build
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production --ignore-scripts
COPY . .
RUN npm run build
# Stage 2: Production
FROM node:18-alpine AS production
WORKDIR /app
ENV NODE_ENV=production
# Copy only production dependencies
COPY --from=builder /app/package*.json ./
RUN npm ci --only=production --ignore-scripts
# Copy built application
COPY --from=builder /app/dist ./dist
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001
USER nodejs
EXPOSE 3000
CMD ["node", "dist/main.js"]
Resultados:
- Tamaño: 150MB (vs 1.5GB)
- Tiempo de push/pull: 90% más rápido
- Superficie de ataque: Reducida drásticamente
- Solo contiene lo necesario para producción
Ejemplo avanzado: Aplicación fullstack
# Multi-stage build para app React + Node.js
# Stage 1: Build frontend
FROM node:18-alpine AS frontend-builder
WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ ./
RUN npm run build
# Stage 2: Build backend
FROM node:18-alpine AS backend-builder
WORKDIR /app/backend
COPY backend/package*.json ./
RUN npm ci
COPY backend/ ./
RUN npm run build
# Stage 3: Production
FROM node:18-alpine AS production
WORKDIR /app
# Copy backend
COPY --from=backend-builder /app/backend/dist ./dist
COPY --from=backend-builder /app/backend/package*.json ./
RUN npm ci --only=production
# Copy frontend build to serve static files
COPY --from=frontend-builder /app/frontend/build ./public
# Security
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
USER nodejs
EXPOSE 3000
CMD ["node", "dist/server.js"]
Estrategias de caching de layers
Aprovechar el orden de layers
# ✅ BIEN: Dependencies se cachean si no cambia package.json
FROM node:18-alpine
WORKDIR /app
# 1. Copy solo package files (cambian poco)
COPY package*.json ./
RUN npm ci
# 2. Copy source code (cambia frecuentemente)
COPY . .
RUN npm run build
# Las dependencies están cacheadas si package.json no cambió
BuildKit y cache mount
# syntax=docker/dockerfile:1
FROM node:18-alpine
WORKDIR /app
# Use BuildKit cache mount para npm cache
RUN --mount=type=cache,target=/root/.npm \\
npm install -g npm@latest
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \\
npm ci
COPY . .
RUN npm run build
Para usar:
DOCKER_BUILDKIT=1 docker build .
Docker Compose para desarrollo
Configuración optimizada
# docker-compose.yml
version: '3.8'
services:
# Backend API
api:
build:
context: ./backend
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
volumes:
# Hot reload
- ./backend/src:/app/src:ro
- /app/node_modules
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://postgres:postgres@db:5432/myapp
- REDIS_URL=redis://redis:6379
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
networks:
- app-network
# Frontend
frontend:
build:
context: ./frontend
dockerfile: Dockerfile.dev
ports:
- "5173:5173"
volumes:
- ./frontend/src:/app/src:ro
- /app/node_modules
environment:
- VITE_API_URL=http://localhost:3000
networks:
- app-network
# Database
db:
image: postgres:15-alpine
ports:
- "5432:5432"
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=myapp
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
networks:
- app-network
# Redis
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- app-network
volumes:
postgres_data:
redis_data:
networks:
app-network:
driver: bridge
Dockerfile para desarrollo con hot reload
# Dockerfile.dev
FROM node:18-alpine
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm install
# Copy source
COPY . .
# Expose port
EXPOSE 3000
# Development command with hot reload
CMD ["npm", "run", "dev"]
Pipeline GitLab CI/CD optimizado: De 15 minutos a 5 minutos
GitLab CI/CD Pipeline optimizado
Configuración completa .gitlab-ci.yml
# .gitlab-ci.yml
stages:
- test
- build
- deploy
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
CACHE_IMAGE: $CI_REGISTRY_IMAGE:cache
# Cache npm dependencies
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .npm/
- node_modules/
# Template for Docker jobs
.docker_template: &docker_template
image: docker:24
services:
- docker:24-dind
before_script:
- echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
# Lint and unit tests
test:unit:
stage: test
image: node:18-alpine
script:
- npm ci --cache .npm --prefer-offline
- npm run lint
- npm run test:unit -- --coverage
coverage: '/Statements\s*:\s*(\d+\.\d+)%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
paths:
- coverage/
expire_in: 30 days
# Integration tests
test:integration:
stage: test
image: docker/compose:latest
services:
- docker:24-dind
script:
- docker-compose -f docker-compose.test.yml up -d
- docker-compose -f docker-compose.test.yml run api npm run test:integration
- docker-compose -f docker-compose.test.yml down
only:
- merge_requests
- main
# Build Docker image
build:
<<: *docker_template
stage: build
script:
# Pull cache image
- docker pull $CACHE_IMAGE || true
# Build with cache
- >
docker build
--cache-from $CACHE_IMAGE
--build-arg BUILDKIT_INLINE_CACHE=1
--tag $IMAGE_TAG
--tag $CACHE_IMAGE
.
# Push both tags
- docker push $IMAGE_TAG
- docker push $CACHE_IMAGE
only:
- main
- develop
- tags
# Deploy to staging
deploy:staging:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache curl
script:
- |
curl -X POST https://portainer.example.com/api/webhooks/$STAGING_WEBHOOK \\
-H "Content-Type: application/json" \\
-d '{"image": "'$IMAGE_TAG'"}'
environment:
name: staging
url: https://staging.example.com
only:
- develop
# Deploy to production
deploy:production:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache curl
script:
- |
curl -X POST https://portainer.example.com/api/webhooks/$PRODUCTION_WEBHOOK \\
-H "Content-Type: application/json" \\
-d '{"image": "'$IMAGE_TAG'"}'
environment:
name: production
url: https://example.com
when: manual
only:
- main
- tags
Optimizaciones clave
1. Cache de dependencias
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .npm/
- node_modules/
Ahorro: 2-3 minutos por pipeline
2. Docker layer caching
script:
- docker pull $CACHE_IMAGE || true
- docker build --cache-from $CACHE_IMAGE ...
Ahorro: 5-8 minutos en builds
3. Parallel jobs
GitLab ejecuta jobs del mismo stage en paralelo automáticamente. Separar tests unitarios e integración:
test:unit:
stage: test
# ...
test:integration:
stage: test
# ...
Ahorro: 50% del tiempo de testing
Estrategias de deployment: Blue-Green y Canary para zero-downtime
Estrategias de deployment
1. Blue-Green Deployment
# docker-compose.blue-green.yml
services:
# Blue (current production)
app-blue:
image: myapp:v1.0
labels:
- "traefik.enable=true"
- "traefik.http.routers.app-blue.rule=Host(`example.com`)"
- "traefik.http.routers.app-blue.priority=1"
# Green (new version)
app-green:
image: myapp:v2.0
labels:
- "traefik.enable=false" # Start disabled
# Load balancer
traefik:
image: traefik:v2.10
command:
- "--providers.docker=true"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
Script de switch:
#!/bin/bash
# Deploy green
docker-compose up -d app-green
# Wait for health check
until docker-compose exec app-green curl -f http://localhost:3000/health; do
sleep 2
done
# Switch traffic to green
docker-compose exec traefik \\
curl -X PUT http://localhost:8080/api/providers/docker \\
-d '{"labels": {"traefik.enable": "true"}}'
# Keep blue running for rollback
echo "Green is live. Blue is on standby."
2. Canary Deployment
# docker-compose.canary.yml
services:
app-stable:
image: myapp:stable
deploy:
replicas: 9
labels:
- "traefik.http.services.app.loadbalancer.weight=90"
app-canary:
image: myapp:canary
deploy:
replicas: 1
labels:
- "traefik.http.services.app.loadbalancer.weight=10"
traefik:
image: traefik:v2.10
command:
- "--providers.docker.swarmMode=true"
ports:
- "80:80"
10% del tráfico va a canary. Si las métricas son buenas, incrementar gradualmente.
Monitoreo y métricas
Docker stats con Prometheus
# docker-compose.monitoring.yml
services:
prometheus:
image: prom/prometheus
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
ports:
- "9090:9090"
grafana:
image: grafana/grafana
volumes:
- grafana_data:/var/lib/grafana
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
cadvisor:
image: gcr.io/cadvisor/cadvisor
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
ports:
- "8080:8080"
volumes:
prometheus_data:
grafana_data:
Configuración Prometheus
# prometheus.yml
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'docker'
static_configs:
- targets: ['cadvisor:8080']
- job_name: 'app'
static_configs:
- targets: ['app:3000']
Mejores prácticas de seguridad
1. Scan de vulnerabilidades
# En .gitlab-ci.yml
security:scan:
stage: test
image: aquasec/trivy:latest
script:
- trivy image --severity HIGH,CRITICAL $IMAGE_TAG
allow_failure: false
2. Multi-stage con usuario no-root
# Create user in final stage
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001
# Change ownership
RUN chown -R nodejs:nodejs /app
# Switch to non-root user
USER nodejs
3. Secrets management
# docker-compose.yml con secrets
services:
app:
image: myapp:latest
secrets:
- db_password
- api_key
secrets:
db_password:
file: ./secrets/db_password.txt
api_key:
file: ./secrets/api_key.txt
// En la aplicación
const dbPassword = fs.readFileSync('/run/secrets/db_password', 'utf8')
Resultados reales
Después de implementar estas optimizaciones en un proyecto de 50+ developers:
Performance
- Build time: 15min → 5min (-67%)
- Deploy time: 10min → 5min (-50%)
- Image size: 1.2GB → 180MB (-85%)
- CI/CD cost: -40% (menos minutos de runner)
Equipo
- Developer satisfaction: +35%
- Deployment frequency: 2x/semana → 10x/día
- Mean time to recovery: 2h → 15min
- Failed deployment rate: 15% → 3%
Errores comunes
❌ No usar .dockerignore
# .dockerignore
node_modules
npm-debug.log
.git
.env
*.md
coverage
.vscode
Impacto: Builds 3-5x más rápidos
❌ Instalar dependencias de desarrollo en producción
# ❌ MAL
RUN npm install
# ✅ BIEN
RUN npm ci --only=production
❌ No aprovechar layer caching
# ❌ MAL: Invalida cache si cualquier archivo cambia
COPY . .
RUN npm install
# ✅ BIEN: Cache de npm persiste si package.json no cambia
COPY package*.json ./
RUN npm ci
COPY . .
Conclusión
La optimización de Docker y CI/CD no es un lujo, es una necesidad en equipos modernos. Los beneficios van más allá del tiempo ahorrado: mejoran la moral del equipo, reducen costos, y permiten despliegues más frecuentes y seguros.
Puntos clave:
- Usa multi-stage builds para imágenes pequeñas
- Aprovecha layer caching estratégicamente
- Implementa CI/CD con cache y parallelización
- Usa estrategias de deployment avanzadas (blue-green, canary)
- Monitorea y mide todo
- Nunca comprometas la seguridad por velocidad
Recursos adicionales
¿Has optimizado tu infraestructura recientemente? ¿Qué resultados obtuviste? Comparte tu experiencia en LinkedIn.