Cristhian Villegas
DevOps12 min read1 views

Curso Docker #9: Docker en CI/CD — GitHub Actions y Automatizacion

Curso Docker #9: Docker en CI/CD — GitHub Actions y Automatizacion

Bienvenido al Curso de Docker - Parte 9 de 10. Hasta ahora hemos construido imagenes y levantado contenedores manualmente. En el mundo real, nadie deberia hacer build y push a mano. Los pipelines de CI/CD (Integracion Continua / Despliegue Continuo) automatizan todo el proceso: desde el commit hasta produccion.

Logo de Docker

Fuente: Wikimedia Commons

En este articulo aprenderemos a integrar Docker con GitHub Actions para automatizar builds, tests, escaneo de seguridad y publicacion de imagenes.

Docker en pipelines de CI/CD: el flujo completo

Un pipeline tipico de Docker en CI/CD sigue estos pasos:

  1. Trigger: un push o pull request activa el pipeline
  2. Checkout: se descarga el codigo fuente
  3. Build: se construye la imagen Docker
  4. Test: se ejecutan tests dentro de un contenedor
  5. Scan: se escanea la imagen con Trivy en busca de vulnerabilidades
  6. Push: se publica la imagen en un registry (GHCR, Docker Hub, ECR)
  7. Deploy: se despliega la nueva imagen en produccion
bash
1# Flujo visual:
2# commit -> build -> test -> scan -> push -> deploy
3#   |         |        |       |       |        |
4#  git    Dockerfile  jest   trivy   GHCR   docker pull
Que es GHCR? GitHub Container Registry (ghcr.io) es el registro de imagenes Docker de GitHub. Es gratuito para repositorios publicos y se integra nativamente con GitHub Actions. Es la alternativa de GitHub a Docker Hub.

Configurar GitHub Actions para Docker

GitHub Actions usa archivos YAML en .github/workflows/. Empecemos con un workflow basico que construye y publica una imagen Docker:

yaml
1# .github/workflows/docker-build.yml
2name: Docker Build & Push
3
4on:
5  push:
6    branches: [main]
7  pull_request:
8    branches: [main]
9
10env:
11  REGISTRY: ghcr.io
12  IMAGE_NAME: ${{ github.repository }}
13
14jobs:
15  build:
16    runs-on: ubuntu-latest
17    permissions:
18      contents: read
19      packages: write
20
21    steps:
22      # 1. Checkout del codigo
23      - name: Checkout repository
24        uses: actions/checkout@v4
25
26      # 2. Configurar Docker Buildx
27      - name: Set up Docker Buildx
28        uses: docker/setup-buildx-action@v3
29
30      # 3. Login en GHCR
31      - name: Log in to GitHub Container Registry
32        uses: docker/login-action@v3
33        with:
34          registry: ${{ env.REGISTRY }}
35          username: ${{ github.actor }}
36          password: ${{ secrets.GITHUB_TOKEN }}
37
38      # 4. Extraer metadata (tags, labels)
39      - name: Extract Docker metadata
40        id: meta
41        uses: docker/metadata-action@v5
42        with:
43          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
44          tags: |
45            type=sha
46            type=ref,event=branch
47            type=semver,pattern={{version}}
48            type=raw,value=latest,enable={{is_default_branch}}
49
50      # 5. Build y push
51      - name: Build and push Docker image
52        uses: docker/build-push-action@v5
53        with:
54          context: .
55          push: ${{ github.event_name != 'pull_request' }}
56          tags: ${{ steps.meta.outputs.tags }}
57          labels: ${{ steps.meta.outputs.labels }}
58          cache-from: type=gha
59          cache-to: type=gha,mode=max

Este workflow hace lo siguiente:

  • Se ejecuta en cada push a main y en pull requests
  • Usa Buildx para builds avanzados con cache
  • Se autentica en GHCR con el token automatico de GitHub
  • Genera tags automaticos basados en el SHA del commit, la rama y versiones semanticas
  • Solo hace push en pushes a main (no en pull requests)
Tip: secrets.GITHUB_TOKEN es un token automatico que GitHub genera para cada ejecucion del workflow. No necesitas crear ningun secret manualmente para autenticarte en GHCR.

Cache de capas Docker en CI

Sin cache, cada build reconstruye todas las capas desde cero, lo cual es muy lento. GitHub Actions ofrece varias estrategias de cache:

yaml
1# Estrategia 1: GitHub Actions Cache (recomendada)
2- name: Build and push
3  uses: docker/build-push-action@v5
4  with:
5    context: .
6    push: true
7    tags: ${{ steps.meta.outputs.tags }}
8    cache-from: type=gha
9    cache-to: type=gha,mode=max
10
11# Estrategia 2: Registry Cache (para equipos grandes)
12- name: Build and push
13  uses: docker/build-push-action@v5
14  with:
15    context: .
16    push: true
17    tags: ${{ steps.meta.outputs.tags }}
18    cache-from: type=registry,ref=ghcr.io/mi-org/mi-app:cache
19    cache-to: type=registry,ref=ghcr.io/mi-org/mi-app:cache,mode=max
20
21# Estrategia 3: Cache local con acciones de cache
22- name: Cache Docker layers
23  uses: actions/cache@v4
24  with:
25    path: /tmp/.buildx-cache
26    key: ${{ runner.os }}-buildx-${{ github.sha }}
27    restore-keys: |
28      ${{ runner.os }}-buildx-
Estrategia Velocidad Tamano max Compartida entre PRs
GHA Cache Rapida 10 GB Si
Registry Cache Media Ilimitado Si
Local Cache Rapida 10 GB No

Tests automatizados en contenedores

Ejecutar tests dentro de contenedores garantiza que el entorno de testing es identico al de produccion. Hay varias formas de hacerlo:

yaml
1# Opcion 1: Build multi-stage con etapa de tests
2# En el Dockerfile:
3# FROM node:20-alpine AS test
4# WORKDIR /app
5# COPY . .
6# RUN npm ci
7# RUN npm test
8
9# En el workflow:
10- name: Run tests in Docker
11  run: |
12    docker build --target test -t mi-app:test .
13
14# Opcion 2: Docker Compose para tests de integracion
15- name: Run integration tests
16  run: |
17    docker compose -f docker-compose.test.yml up --build --abort-on-container-exit --exit-code-from tests
18    docker compose -f docker-compose.test.yml down -v

Un archivo docker-compose.test.yml para tests de integracion:

yaml
1# docker-compose.test.yml
2services:
3  db:
4    image: postgres:16-alpine
5    environment:
6      POSTGRES_DB: test_db
7      POSTGRES_USER: test_user
8      POSTGRES_PASSWORD: test_pass
9    healthcheck:
10      test: ["CMD-SHELL", "pg_isready -U test_user -d test_db"]
11      interval: 5s
12      timeout: 5s
13      retries: 5
14
15  tests:
16    build:
17      context: .
18      target: test
19    depends_on:
20      db:
21        condition: service_healthy
22    environment:
23      DATABASE_URL: postgresql://test_user:test_pass@db:5432/test_db
24      NODE_ENV: test
25    command: npm run test:integration
Importante: Usa --abort-on-container-exit para que Docker Compose se detenga cuando el contenedor de tests termine. Usa --exit-code-from tests para que el pipeline falle si los tests fallan.

Escaneo de seguridad en el pipeline

Integremos Trivy para escanear vulnerabilidades automaticamente:

yaml
1# Agregar al workflow despues del build
2- name: Run Trivy vulnerability scanner
3  uses: aquasecurity/trivy-action@master
4  with:
5    image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
6    format: 'sarif'
7    output: 'trivy-results.sarif'
8    severity: 'CRITICAL,HIGH'
9
10# Subir resultados a GitHub Security
11- name: Upload Trivy scan results
12  uses: github/codeql-action/upload-sarif@v3
13  if: always()
14  with:
15    sarif_file: 'trivy-results.sarif'
16
17# Fallar el pipeline si hay vulnerabilidades criticas
18- name: Fail on critical vulnerabilities
19  uses: aquasecurity/trivy-action@master
20  with:
21    image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
22    severity: 'CRITICAL'
23    exit-code: '1'

Los resultados aparecen directamente en la pestana Security de tu repositorio en GitHub, con detalles de cada vulnerabilidad y recomendaciones de correccion.

Builds multi-plataforma

Si necesitas soportar multiples arquitecturas (AMD64 para servidores y ARM64 para Apple Silicon o AWS Graviton), Docker Buildx lo hace facil:

yaml
1# Agregar QEMU para emulacion de arquitecturas
2- name: Set up QEMU
3  uses: docker/setup-qemu-action@v3
4
5- name: Set up Docker Buildx
6  uses: docker/setup-buildx-action@v3
7
8- name: Build and push multi-platform
9  uses: docker/build-push-action@v5
10  with:
11    context: .
12    platforms: linux/amd64,linux/arm64
13    push: true
14    tags: ${{ steps.meta.outputs.tags }}
15    cache-from: type=gha
16    cache-to: type=gha,mode=max

Docker crea un manifest list que contiene ambas imagenes. Cuando alguien hace docker pull, Docker automaticamente descarga la imagen correcta para su arquitectura.

bash
1# Verificar las plataformas de una imagen
2docker manifest inspect ghcr.io/mi-usuario/mi-app:latest
3
4# Resultado:
5# {
6#   "manifests": [
7#     { "platform": { "architecture": "amd64", "os": "linux" } },
8#     { "platform": { "architecture": "arm64", "os": "linux" } }
9#   ]
10# }
Nota: Los builds multi-plataforma son mas lentos porque ARM64 se emula via QEMU en runners AMD64. Para builds mas rapidos, puedes usar runners ARM64 nativos (disponibles en GitHub Actions para organizaciones).

Workflow completo de produccion

Aqui el workflow completo que combina todo lo aprendido: build, test, scan, push y notificacion:

yaml
1# .github/workflows/docker-production.yml
2name: Docker Production Pipeline
3
4on:
5  push:
6    branches: [main]
7    tags: ['v*']
8  pull_request:
9    branches: [main]
10
11env:
12  REGISTRY: ghcr.io
13  IMAGE_NAME: ${{ github.repository }}
14
15jobs:
16  # ---- Job 1: Tests ----
17  test:
18    runs-on: ubuntu-latest
19    steps:
20      - uses: actions/checkout@v4
21
22      - name: Run unit tests
23        run: |
24          docker compose -f docker-compose.test.yml up \
25            --build \
26            --abort-on-container-exit \
27            --exit-code-from tests
28
29      - name: Cleanup
30        if: always()
31        run: docker compose -f docker-compose.test.yml down -v
32
33  # ---- Job 2: Build & Push ----
34  build:
35    runs-on: ubuntu-latest
36    needs: test
37    permissions:
38      contents: read
39      packages: write
40      security-events: write
41    outputs:
42      image-tag: ${{ steps.meta.outputs.version }}
43
44    steps:
45      - uses: actions/checkout@v4
46
47      - name: Set up QEMU
48        uses: docker/setup-qemu-action@v3
49
50      - name: Set up Docker Buildx
51        uses: docker/setup-buildx-action@v3
52
53      - name: Log in to GHCR
54        if: github.event_name != 'pull_request'
55        uses: docker/login-action@v3
56        with:
57          registry: ${{ env.REGISTRY }}
58          username: ${{ github.actor }}
59          password: ${{ secrets.GITHUB_TOKEN }}
60
61      - name: Extract metadata
62        id: meta
63        uses: docker/metadata-action@v5
64        with:
65          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
66          tags: |
67            type=sha
68            type=ref,event=branch
69            type=semver,pattern={{version}}
70            type=semver,pattern={{major}}.{{minor}}
71            type=raw,value=latest,enable={{is_default_branch}}
72
73      - name: Build and push
74        uses: docker/build-push-action@v5
75        with:
76          context: .
77          platforms: linux/amd64,linux/arm64
78          push: ${{ github.event_name != 'pull_request' }}
79          tags: ${{ steps.meta.outputs.tags }}
80          labels: ${{ steps.meta.outputs.labels }}
81          cache-from: type=gha
82          cache-to: type=gha,mode=max
83
84      # ---- Seguridad ----
85      - name: Scan for vulnerabilities
86        uses: aquasecurity/trivy-action@master
87        with:
88          image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
89          format: 'sarif'
90          output: 'trivy-results.sarif'
91          severity: 'CRITICAL,HIGH'
92
93      - name: Upload scan results
94        uses: github/codeql-action/upload-sarif@v3
95        if: always()
96        with:
97          sarif_file: 'trivy-results.sarif'
98
99  # ---- Job 3: Deploy (solo en tags) ----
100  deploy:
101    runs-on: ubuntu-latest
102    needs: build
103    if: startsWith(github.ref, 'refs/tags/v')
104    environment: production
105    steps:
106      - name: Deploy to production
107        run: |
108          echo "Deploying ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build.outputs.image-tag }}"
109          # Aqui iria tu logica de deploy:
110          # - SSH al servidor y docker pull
111          # - kubectl set image
112          # - aws ecs update-service
113          # - etc.
Tip: Usa environment: production en el job de deploy para habilitar protecciones como aprobaciones manuales y secretos especificos del entorno en GitHub.

Resumen y siguiente articulo

En este articulo aprendimos a automatizar Docker con GitHub Actions:

  • Pipeline completo: build, test, scan, push y deploy automatizados
  • GHCR: publicar imagenes en GitHub Container Registry
  • Cache: GHA cache, registry cache y local cache para builds rapidos
  • Tests en contenedores: docker compose para tests de integracion
  • Escaneo de seguridad: Trivy integrado con GitHub Security
  • Multi-plataforma: builds para AMD64 y ARM64 con QEMU y Buildx
  • Workflow de produccion: pipeline completo listo para usar
Siguiente articulo: En la parte 10 y final construiremos un proyecto completo full-stack con Docker: React + Node.js + PostgreSQL + Redis, con todo lo aprendido en el curso. Referencia oficial: GitHub Actions with Docker.
Share:
CV

Cristhian Villegas

Software Engineer specializing in Java, Spring Boot, Angular & AWS. Building scalable distributed systems with clean architecture.

Comments

Sign in to leave a comment

No comments yet. Be the first!

Related Articles