¿Qué es CI/CD?
CI/CD significa Integración Continua (Continuous Integration) y Entrega/Despliegue Continuo (Continuous Delivery/Deployment). Cuando combinamos estas prácticas con Docker, obtenemos un flujo de trabajo donde cada cambio en el código pasa por un proceso automatizado que verifica su calidad antes de llegar a producción.
La magia de Docker en CI/CD radica en que el mismo contenedor que se construye y prueba en el pipeline es exactamente el que llega a producción, eliminando el famoso problema "funciona en mi máquina".
Arquitectura de un Pipeline CI/CD con Docker
Un pipeline típico con Docker sigue esta estructura:
- Commit: El desarrollador sube código al repositorio.
- Build: Se construye la imagen Docker con el código nuevo.
- Test: Se ejecutan pruebas unitarias e integración dentro del contenedor.
- Security Scan: Se analizan vulnerabilidades en la imagen.
- Push: La imagen se sube al registry (Docker Hub, GCR, ECR).
- Deploy: Se despliega el contenedor en el entorno objetivo.
Preparando nuestra Aplicación
Antes de crear el pipeline, necesitamos una aplicación lista para Docker. Usaremos una aplicación Node.js simple como ejemplo:
# Dockerfile para desarrollo (multi-stage)
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
FROM node:18-alpine AS production
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
EXPOSE 3000
USER node
CMD ["node", "dist/index.js"]
GitHub Actions + Docker
GitHub Actions es una de las herramientas más populares para CI/CD. Veamos un ejemplo completo:
# .github/workflows/docker-pipeline.yml
name: Docker CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build image
run: |
docker build \
--tag ${{ env.IMAGE_NAME }}:test \
--target test \
.
- name: Run tests
run: |
docker run --rm ${{ env.IMAGE_NAME }}:test npm test
security-scan:
runs-on: ubuntu-latest
needs: build-and-test
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.IMAGE_NAME }}:test
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload results to GitHub Security
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
push:
runs-on: ubuntu-latest
needs: security-scan
if: github.ref == 'refs/heads/main'
permissions:
contents: read
packages: write
steps:
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
Dockerfile Multi-Stage para Testing
Para que el pipeline funcione correctamente, necesitamos un Dockerfile que soporte diferentes objetivos:
# Dockerfile completo con múltiples stages
FROM node:18-alpine AS base
WORKDIR /app
COPY package*.json ./
# Stage para desarrollo
FROM base AS development
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]
# Stage para pruebas
FROM base AS test
RUN npm ci
COPY . .
# Ejecutar pruebas en el pipeline
CMD ["npm", "test"]
# Stage para producción
FROM base AS production
RUN npm ci --only=production && npm cache clean --force
COPY . .
RUN npm run build
FROM node:18-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=production /app/dist ./dist
COPY --from=production /app/node_modules ./node_modules
COPY package*.json ./
EXPOSE 3000
USER node
CMD ["node", "dist/index.js"]
Optimizaciones para CI/CD
Existen varias técnicas para hacer que los pipelines con Docker sean más rápidos y eficientes:
| Técnica | Descripción | Beneficio |
|---|---|---|
| BuildKit Cache | Activar caché de construcción con --mount=type=cache | Reduce tiempo de build hasta 80% |
| Dependency Caching | Cachear node_modules o pip packages | Evita descargas repetitivas |
| Multi-platform Builds | Construir para múltiples arquitecturas | Compatibilidad universal |
| Layer Caching | Reutilizar capas no modificadas | Builds incrementales rápidos |
cache-from: type=gha para activar el caché de GitHub Actions, que puede reducir drásticamente los tiempos de construcción en pipelines subsequentes.Ejemplo con GitLab CI
Para quienes usen GitLab, aquí está la configuración equivalente:
# .gitlab-ci.yml
stages:
- build
- test
- security
- push
- deploy
variables:
DOCKER_DRIVER: overlay2
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
build:
stage: build
image: docker:24-dind
services:
- docker:24-dind
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $IMAGE_TAG .
- docker push $IMAGE_TAG
test:
stage: test
image: $IMAGE_TAG
script:
- npm install
- npm test
trivy-scan:
stage: security
image:
name: aquasecurity/trivy:latest
entrypoint: [""]
script:
- trivy image --exit-code 0 --severity HIGH,CRITICAL $IMAGE_TAG
allow_failure: true
deploy-staging:
stage: deploy
script:
- kubectl config use-context staging
- kubectl set image deployment/app app=$IMAGE_TAG
environment:
name: staging
only:
- develop
deploy-production:
stage: deploy
script:
- kubectl config use-context production
- kubectl set image deployment/app app=$IMAGE_TAG
environment:
name: production
when: manual
only:
- main
Ver más: Ejemplo con Jenkinsfile// Jenkinsfile (Jenkins Declarative Pipeline)
pipeline {
agent {
docker {
image 'node:18-alpine'
args '-v /root/.npm:/root/.npm'
}
}
environment {
DOCKER_IMAGE = 'myapp'
REGISTRY = 'docker.io'
}
stages {
stage('Build') {
steps {
sh 'npm install'
sh 'npm run build'
}
}
stage('Test') {
steps {
sh 'npm test'
}
post {
always {
junit '**/test-results/*.xml'
}
}
}
stage('Build Docker Image') {
steps {
script {
def customImage = docker.build("${DOCKER_IMAGE}:${env.BUILD_NUMBER}")
}
}
}
stage('Push to Registry') {
steps {
script {
docker.withRegistry('https://registry.hub.docker.com', 'docker-hub-cred') {
def customImage = docker.build("${DOCKER_IMAGE}:${env.BUILD_NUMBER}")
customImage.push()
customImage.push('latest')
}
}
}
}
stage('Deploy') {
when {
branch 'main'
}
steps {
sh 'kubectl apply -f k8s/'
}
}
}
}
Mejores Prácticas de Seguridad
- No ejecutes como root: Usa
USERen el Dockerfile. - Escanea vulnerabilidades: Integra herramientas como Trivy, Snyk o Clair.
- Usa imágenes oficiales: Prefiere imágenes base con soporte activo.
- Minimiza capas: Combina instrucciones RUN cuando sea posible.
- No incluyas secretos: Usa build args solo para configuración no sensible.
- Firma tus imágenes: Implementa Docker Content Trust.
Integración con Kubernetes
El pipeline completo frecuentemente termina con un despliegue a Kubernetes. Aquí está cómo actualizar un deployment automáticamente:
# Script de despliegue (deploy.sh)
#!/bin/bash
set -e
NEW_IMAGE=$1
KUBECONFIG=${KUBECONFIG:-~/.kube/config}
NAMSPACE=${NAMESPACE:-default}
DEPLOYMENT=${DEPLOYMENT:-app}
echo "Desplegando imagen: $NEW_IMAGE"
echo "Namespace: $NAMSPACE"
echo "Deployment: $DEPLOYMENT"
# Actualizar la imagen en el deployment
kubectl set image deployment/$DEPLOYMENT \
$DEPLOYMENT=$NEW_IMAGE \
-n $NAMESPACE
# Esperar a que el rollout complete
kubectl rollout status deployment/$DEPLOYMENT \
-n $NAMSPACE \
--timeout=300s
echo "✅ Despliegue completado exitosamente"
# Mostrar los pods activos
kubectl get pods -n $NAMSPACE -l app=$DEPLOYMENT
Monitoreo Post-Despliegue
Un pipeline CI/CD robusto debe incluir verificaciones post-despliegue:
- Health checks: Verificar que el contenedor responde correctamente.
- Smoke tests: Ejecutar pruebas mínimas en el entorno de producción.
- Integración con APM: Monitorear métricas de rendimiento.
- Rollback automático: Revertir si algo falla.
¿Por qué es importante usar multi-stage builds en un pipeline CI/CD?
- A) Para que el código sea más legible
- B) Para reducir el tamaño de la imagen final y separar dependencias de construcción de producción
- C) Para ejecutar múltiples contenedores simultáneamente
- D) Para evitar usar Dockerfile
Conclusión
Un pipeline CI/CD con Docker transforma radicalmente la forma en que desarrollamos y desplegamos software. La combinación de contenedores con automatización de pipelines nos permite:
- Consistencia: Mismo entorno en todas las etapas.
- Velocidad: Despliegues automatizados en minutos.
- Confianza: Pruebas automatizadas en cada cambio.
- Escalabilidad: Facilita el escalado horizontal.
- Reversión: Rollback rápido ante problemas.
La inversión inicial en configurar un pipeline robusto se recupera rápidamente en forma de reducción de errores, tiempo de desarrollo y operaciones más fluidas.