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.
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étrica | Sin CI/CD | Con CI/CD |
|---|---|---|
| Frecuencia de deploy | 1-2/semana | Múltiples/día |
| Tiempo de deploy | 30-60 min | 3-5 min |
| Bugs en producción | Más frecuentes | Detectados antes |
| Rollback | Manual y lento | Un click |
| Confianza del equipo | Baja | Alta |
El pipeline completo
- Push a
main→ GitHub Actions trigger - Validación
- Lint + Type Check
- Unit Tests
- Integration Tests
- Security Scan
- Build (si todo está verde)
- Build Docker Image
- Push a Registry
- 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.jsonse 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étrica | Objetivo |
|---|---|
| Tiempo total del pipeline | < 10 min |
| Tasa de éxito | > 95% |
| Deploy a producción | < 5 min post-merge |
| Frecuencia de deploy | Diaria o más |
| MTTR (Mean Time to Recovery) | < 15 min |
Errores comunes
- Pipeline de 30+ minutos: si tarda tanto, nadie lo espera y la gente mergea sin CI
- Tests flaky: un test que a veces pasa y a veces falla destruye la confianza
- No cachear: cada run instala todo desde cero — desperdicio de tiempo y dinero
- Sin rollback: si no puedes volver atrás en minutos, no estás listo para CD
- 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
Lead Developer & Founder en CloudLabs