DevOps CI/CD Docker

Cómo Implementar CI/CD desde Cero con GitHub Actions y Docker

Guía paso a paso para configurar un pipeline de integración y despliegue continuo con GitHub Actions, Docker y Cloud Run. De commit a producción en minutos.

Hans Vergara 9 min de lectura

Cómo Implementar CI/CD desde Cero con GitHub Actions y Docker

Desplegar manualmente es aceptable para un prototipo. Para cualquier cosa en producción, necesitas CI/CD. En esta guía, implementamos un pipeline completo desde cero: cada push a main ejecuta tests, construye una imagen Docker, y despliega automáticamente a producción.

¿Qué es CI/CD y por qué importa?

  • CI (Continuous Integration): cada cambio se valida automáticamente (tests, lint, build)
  • CD (Continuous Delivery): cada cambio validado se puede desplegar a producción con un click
  • CD (Continuous Deployment): cada cambio validado se despliega automáticamente

El impacto real

MétricaSin CI/CDCon CI/CD
Frecuencia de deploy1-2/semanaMúltiples/día
Tiempo de deploy30-60 min3-5 min
Bugs en producciónMás frecuentesDetectados antes
RollbackManual y lentoUn click
Confianza del equipoBajaAlta

El pipeline completo

  1. Push a main → GitHub Actions trigger
  2. Validación
    • Lint + Type Check
    • Unit Tests
    • Integration Tests
    • Security Scan
  3. Build (si todo está verde)
    • Build Docker Image
    • Push a Registry
  4. Deploy
    • Deploy a Staging
    • Smoke Tests
    • Deploy a Producción

Paso 1: Preparar el Dockerfile

Un Dockerfile optimizado para producción:

# Build stage
FROM node:22-alpine AS builder

WORKDIR /app

# Copiar solo package files primero (cache de npm install)
COPY package*.json ./
RUN npm ci

# Copiar código y buildear
COPY . .
RUN npm run build

# Production stage
FROM node:22-alpine AS production

WORKDIR /app

# Solo copiar lo necesario
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

# Seguridad: no correr como root
RUN addgroup -g 1001 -S appgroup && \
    adduser -S appuser -u 1001 -G appgroup
USER appuser

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

CMD ["node", "dist/index.js"]

Puntos clave:

  • Multi-stage build: la imagen final no tiene devDependencies ni código fuente
  • Layer caching: package.json se copia antes que el código — npm install se cachea
  • Non-root user: seguridad básica pero esencial
  • Healthcheck: el orquestador sabe si el container está sano

Paso 2: GitHub Actions — El workflow CI

Crea .github/workflows/ci-cd.yml:

name: CI/CD Pipeline

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  # ═══════════════════════════
  # Job 1: Validación
  # ═══════════════════════════
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'npm'
      
      - run: npm ci
      
      - name: Lint
        run: npm run lint
      
      - name: Type Check
        run: npm run type-check
      
      - name: Unit Tests
        run: npm run test -- --coverage
      
      - name: Upload coverage
        uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage/

  # ═══════════════════════════
  # Job 2: Security
  # ═══════════════════════════
  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          scan-ref: '.'
          severity: 'CRITICAL,HIGH'

  # ═══════════════════════════
  # Job 3: Build & Push Docker
  # ═══════════════════════════
  build:
    needs: [validate, security]
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    
    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Login 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=sha,prefix=
            type=raw,value=latest
      
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  # ═══════════════════════════
  # Job 4: Deploy
  # ═══════════════════════════
  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment: production
    
    steps:
      - name: Deploy to Cloud Run
        uses: google-github-actions/deploy-cloudrun@v2
        with:
          service: my-app
          region: us-central1
          image: ${{ needs.build.outputs.image-tag }}

Paso 3: Secretos y variables de entorno

Configura en GitHub → Settings → Secrets:

# Para Google Cloud
GCP_PROJECT_ID
GCP_SA_KEY (Service Account JSON)

# Para la app
DATABASE_URL
JWT_SECRET
API_KEYS

Nunca hardcodees secretos. Ni en el código, ni en el Dockerfile, ni en el workflow.

Paso 4: Ambientes y aprobaciones

GitHub Actions soporta environments con reglas:

deploy-staging:
  environment: staging  # auto-deploy

deploy-production:
  environment: production  # requiere aprobación manual
  needs: deploy-staging

Configura en GitHub → Settings → Environments:

  • Staging: deploy automático en cada push
  • Production: requiere aprobación de al menos 1 reviewer

Paso 5: Smoke tests post-deploy

Después del deploy, verifica que todo funciona:

smoke-test:
  needs: deploy
  runs-on: ubuntu-latest
  steps:
    - name: Health check
      run: |
        for i in {1..10}; do
          status=$(curl -s -o /dev/null -w "%{http_code}" https://api.myapp.com/health)
          if [ "$status" = "200" ]; then
            echo "Health check passed"
            exit 0
          fi
          echo "Attempt $i: status $status, retrying..."
          sleep 5
        done
        echo "Health check failed"
        exit 1
    
    - name: Critical endpoints
      run: |
        curl -f https://api.myapp.com/api/v1/status
        curl -f https://api.myapp.com/api/v1/version

Paso 6: Rollback automático

Si el smoke test falla, rollback:

rollback:
  needs: smoke-test
  if: failure()
  runs-on: ubuntu-latest
  steps:
    - name: Rollback to previous revision
      run: |
        gcloud run services update-traffic my-app \
          --to-revisions=LATEST=0 \
          --region=us-central1
        
        # Obtener la revisión anterior
        PREV=$(gcloud run revisions list --service=my-app \
          --region=us-central1 --format='value(REVISION)' \
          --limit=2 | tail -1)
        
        gcloud run services update-traffic my-app \
          --to-revisions=$PREV=100 \
          --region=us-central1

Optimizaciones avanzadas

Caché inteligente

- uses: actions/cache@v4
  with:
    path: |
      ~/.npm
      node_modules
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-

Ejecución paralela de tests

test:
  strategy:
    matrix:
      shard: [1, 2, 3, 4]
  steps:
    - run: npm run test -- --shard=${{ matrix.shard }}/4

Notificaciones

notify:
  needs: [deploy]
  if: always()
  runs-on: ubuntu-latest
  steps:
    - uses: 8398a7/action-slack@v3
      with:
        status: ${{ needs.deploy.result }}
        text: |
          Deploy ${{ needs.deploy.result == 'success' && '✅' || '❌' }}
          Commit: ${{ github.sha }}
          Author: ${{ github.actor }}
      env:
        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

Métricas de un buen pipeline

MétricaObjetivo
Tiempo total del pipeline< 10 min
Tasa de éxito> 95%
Deploy a producción< 5 min post-merge
Frecuencia de deployDiaria o más
MTTR (Mean Time to Recovery)< 15 min

Errores comunes

  1. Pipeline de 30+ minutos: si tarda tanto, nadie lo espera y la gente mergea sin CI
  2. Tests flaky: un test que a veces pasa y a veces falla destruye la confianza
  3. No cachear: cada run instala todo desde cero — desperdicio de tiempo y dinero
  4. Sin rollback: si no puedes volver atrás en minutos, no estás listo para CD
  5. Secretos en el código: parece obvio pero sigue pasando

Conclusión

CI/CD no es un lujo — es la base para entregar software confiable. Con GitHub Actions y Docker, puedes tener un pipeline profesional corriendo en una tarde. El ROI es inmediato: menos errores, deploys más rápidos, y un equipo con más confianza.

En CloudLabs, implementamos pipelines de CI/CD como parte de cada proyecto. No entregamos código sin un camino claro y automatizado a producción. Hablemos de tu infraestructura.

¿Te interesa este tema?

En CloudLabs implementamos estas soluciones para empresas reales. Conversemos sobre tu proyecto.

Hablemos →
Hans Vergara

Hans Vergara

Lead Developer & Founder en CloudLabs