En este tema aprenderemos cómo integrar Docker dentro de un flujo de trabajo de Integración Continua y Entrega/Despliegue Continuo (CI/CD). Docker se ha convertido en una pieza fundamental de los pipelines modernos porque permite construir artefactos reproducibles (imágenes) que se comportan igual en cualquier entorno. Comprender esta integración es esencial para automatizar la construcción, las pruebas, la publicación y el despliegue de tus aplicaciones de forma fiable.

Objetivos de Aprendizaje

  • Entender qué es CI/CD y por qué Docker encaja de forma natural en estos procesos.
  • Conocer las fases típicas de un pipeline: construir (build), probar (test) y publicar (push) imágenes.
  • Crear un pipeline de ejemplo con GitHub Actions y con GitLab CI usando un Dockerfile.
  • Aplicar buenas prácticas de versionado y etiquetado (tagging) de imágenes en CI.
  • Automatizar el despliegue y gestionar las credenciales del registro de forma segura mediante secrets.

¿Qué es CI/CD y Por Qué Docker Encaja?

CI/CD es un conjunto de prácticas que automatizan el ciclo de vida del software. Conviene distinguir tres conceptos:

  • CI (Integración Continua): cada cambio que se sube al repositorio se construye y se prueba automáticamente, detectando errores cuanto antes.
  • CD (Entrega Continua): además de construir y probar, se prepara automáticamente un artefacto listo para desplegar, pero el despliegue final lo aprueba una persona.
  • CD (Despliegue Continuo): el artefacto validado se despliega de forma totalmente automática, sin intervención humana.

Docker encaja perfectamente en este modelo por varias razones:

Característica de Docker Beneficio en CI/CD
Imágenes inmutables El mismo artefacto se prueba y se despliega, sin sorpresas.
Reproducibilidad Se construye igual en el portátil del desarrollador y en el servidor de CI.
Aislamiento Cada job se ejecuta en un entorno limpio y controlado.
Portabilidad La imagen se ejecuta igual en cualquier nodo o nube.
Versionado por tags Cada build genera una versión identificable y trazable.

La idea central es: "construyes una imagen una sola vez y promueves ese mismo artefacto a través de los entornos" (pruebas, staging, producción).

Fases de un Pipeline con Docker

Un pipeline típico basado en Docker sigue tres fases principales:

  1. Build (Construir): se construye la imagen a partir del Dockerfile.
  2. Test (Probar): se ejecutan las pruebas dentro de un contenedor creado desde esa imagen.
  3. Push (Publicar): si las pruebas pasan, la imagen se etiqueta y se sube a un registro (registry) como Docker Hub, GitHub Container Registry o GitLab Registry.

Opcionalmente se añade una cuarta fase:

  1. Deploy (Desplegar): se actualiza el entorno de destino para que use la nueva imagen.

Ejemplo de Dockerfile de Partida

Trabajaremos con una aplicación de ejemplo muy sencilla. Este es el Dockerfile:

# Imagen base ligera con Node.js
FROM node:20-alpine

# Directorio de trabajo dentro del contenedor
WORKDIR /app

# Copiamos primero los manifiestos de dependencias para aprovechar la caché
COPY package*.json ./

# Instalamos solo las dependencias de producción
RUN npm ci --omit=dev

# Copiamos el resto del código fuente
COPY . .

# Puerto que expone la aplicación
EXPOSE 3000

# Comando que arranca la aplicación
CMD ["node", "server.js"]

Explicación de cada fragmento:

  • FROM node:20-alpine: parte de una imagen oficial de Node.js basada en Alpine Linux, que es muy ligera.
  • WORKDIR /app: define /app como directorio de trabajo; los comandos siguientes se ejecutan ahí.
  • COPY package*.json ./: copia solo los ficheros de dependencias antes que el código. Así Docker reutiliza la capa de npm ci si las dependencias no cambian, acelerando builds posteriores.
  • RUN npm ci --omit=dev: instala las dependencias de forma reproducible a partir del package-lock.json, excluyendo las de desarrollo.
  • COPY . .: copia el resto del proyecto al contenedor.
  • EXPOSE 3000: documenta que la aplicación escucha en el puerto 3000.
  • CMD ["node", "server.js"]: define el comando por defecto al arrancar el contenedor.

Pipeline con GitHub Actions

GitHub Actions permite definir pipelines en ficheros YAML dentro de .github/workflows/. Veamos un workflow completo que construye, prueba, etiqueta y publica la imagen.

name: Build, Test y Push de la imagen Docker

# Se ejecuta en cada push a main y en cada etiqueta de versión
on:
  push:
    branches:
      - main
    tags:
      - "v*.*.*"

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      # 1. Descargamos el código del repositorio
      - name: Checkout del código
        uses: actions/checkout@v4

      # 2. Configuramos Buildx para builds avanzados
      - name: Configurar Docker Buildx
        uses: docker/setup-buildx-action@v3

      # 3. Iniciamos sesión en el registro usando secrets
      - name: Login en el registro
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GHCR_TOKEN }}

      # 4. Construimos la imagen (sin publicar) y la cargamos localmente
      - name: Construir imagen
        run: docker build -t miapp:ci .

      # 5. Ejecutamos las pruebas dentro de un contenedor
      - name: Ejecutar pruebas
        run: docker run --rm miapp:ci npm test

      # 6. Construimos y publicamos con tags adecuados
      - name: Build y Push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: |
            ghcr.io/miorg/miapp:latest
            ghcr.io/miorg/miapp:${{ github.sha }}

Explicación detallada:

  • on: define los eventos que disparan el pipeline. Aquí se activa con pushes a la rama main y con etiquetas que sigan el patrón v*.*.* (por ejemplo v1.2.0).
  • runs-on: ubuntu-latest: el job se ejecuta en una máquina virtual Linux proporcionada por GitHub.
  • actions/checkout@v4: clona el repositorio dentro del runner.
  • docker/setup-buildx-action@v3: habilita Buildx, el motor de construcción extendido de Docker.
  • docker/login-action@v3: autentica contra el registro. Las credenciales nunca van en texto plano: secrets.GHCR_TOKEN es un secreto configurado en el repositorio.
  • docker build -t miapp:ci .: construye una imagen temporal llamada miapp:ci para las pruebas.
  • docker run --rm miapp:ci npm test: arranca un contenedor desde esa imagen y ejecuta los tests; --rm elimina el contenedor al terminar.
  • docker/build-push-action@v6: vuelve a construir y, esta vez, publica la imagen con dos etiquetas: latest y el hash del commit.

Pipeline con GitLab CI

GitLab CI se configura con un fichero .gitlab-ci.yml en la raíz del proyecto. Este ejemplo usa el ejecutor Docker-in-Docker (dind).

stages:
  - build
  - test
  - push

variables:
  IMAGE: $CI_REGISTRY_IMAGE/miapp

# Construye la imagen y la guarda con la etiqueta del commit
build:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  script:
    - docker build -t $IMAGE:$CI_COMMIT_SHORT_SHA .

# Ejecuta las pruebas dentro de la imagen recién construida
test:
  stage: test
  image: docker:24
  services:
    - docker:24-dind
  script:
    - docker build -t $IMAGE:test .
    - docker run --rm $IMAGE:test npm test

# Inicia sesión y publica la imagen en el registro de GitLab
push:
  stage: push
  image: docker:24
  services:
    - docker:24-dind
  script:
    - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
    - docker build -t $IMAGE:$CI_COMMIT_SHORT_SHA -t $IMAGE:latest .
    - docker push $IMAGE:$CI_COMMIT_SHORT_SHA
    - docker push $IMAGE:latest
  only:
    - main

Explicación detallada:

  • stages: define el orden de las fases (build, test, push).
  • variables: IMAGE se construye con CI_REGISTRY_IMAGE, una variable que GitLab rellena automáticamente con la ruta del registro del proyecto.
  • services: docker:24-dind: levanta el servicio Docker-in-Docker, necesario para ejecutar comandos docker dentro del job.
  • docker login --password-stdin: la contraseña se pasa por la entrada estándar en lugar de como argumento, evitando que aparezca en los logs. CI_REGISTRY_USER y CI_REGISTRY_PASSWORD son variables protegidas que GitLab inyecta de forma segura.
  • only: main: el job push solo se ejecuta para la rama main.

Versionado y Etiquetado (Tagging) de Imágenes

Etiquetar bien las imágenes es clave para la trazabilidad. Estas son las estrategias más habituales:

Etiqueta Ejemplo Uso recomendado
latest miapp:latest Última versión estable; cómoda pero poco precisa.
SHA del commit miapp:9f8c2a1 Trazabilidad exacta de qué código contiene la imagen.
Versión semántica miapp:1.4.2 Releases formales (SemVer: MAYOR.MENOR.PARCHE).
Rama o entorno miapp:staging Imagen asociada a un entorno concreto.

Buenas prácticas:

  • Evita depender solo de latest en producción: no indica qué versión real estás ejecutando.
  • Etiqueta siempre con el SHA del commit para poder reproducir exactamente cualquier despliegue.
  • Para releases, usa versionado semántico generado a partir de las etiquetas de Git.

Despliegue Automatizado

Una vez publicada la imagen, la fase de despliegue actualiza el entorno para que use la nueva versión. Un ejemplo sencillo, conectándose por SSH a un servidor, sería:

# Conexión al servidor de destino y actualización del contenedor
ssh deploy@servidor-produccion <<'EOF'
  docker pull ghcr.io/miorg/miapp:1.4.2
  docker stop miapp || true
  docker rm miapp || true
  docker run -d --name miapp -p 80:3000 ghcr.io/miorg/miapp:1.4.2
EOF

Explicación:

  • docker pull ...: descarga la nueva versión de la imagen en el servidor.
  • docker stop / docker rm: detiene y elimina el contenedor antiguo; || true evita que el script falle si no existía.
  • docker run -d ...: arranca el contenedor nuevo en segundo plano, mapeando el puerto 80 del host al 3000 del contenedor.

En entornos más avanzados, este paso se delega a orquestadores como Docker Swarm o Kubernetes, que veremos en los próximos temas.

Gestión Segura de Credenciales mediante Secrets

Nunca debes escribir contraseñas, tokens o claves directamente en los ficheros del pipeline, ya que quedarían visibles en el repositorio. En su lugar, usa el almacén de secretos de tu plataforma:

  • GitHub Actions: secretos en Settings → Secrets and variables → Actions. Se referencian como ${{ secrets.NOMBRE }}.
  • GitLab CI: variables protegidas y enmascaradas en Settings → CI/CD → Variables.

Reglas de oro:

  • Usa tokens de acceso con permisos mínimos (solo lectura/escritura del registro), no contraseñas de cuenta.
  • Marca las variables como "enmascaradas" para que no aparezcan en los logs.
  • Rota los tokens periódicamente y revócalos si dejan de usarse.
  • Prefiere --password-stdin frente a pasar la contraseña como argumento de línea de comandos.

Errores Comunes y Consejos

  • Usar latest en todas partes: dificulta saber qué versión está desplegada. Etiqueta con SHA o SemVer.
  • Credenciales en texto plano: nunca pongas tokens en el YAML; usa siempre secrets.
  • No cachear capas: copiar el código antes que los manifiestos de dependencias invalida la caché y ralentiza los builds. Copia primero package*.json (o equivalente).
  • Probar una imagen distinta de la que se publica: asegúrate de probar y publicar el mismo artefacto para evitar discrepancias.
  • Imágenes enormes: usa imágenes base ligeras (alpine) y multi-stage builds para reducir tamaño.
  • Olvidar --rm en los contenedores de prueba: acumula contenedores muertos en el runner.

Ejercicios

Ejercicio 1

Escribe un workflow de GitHub Actions que, en cada push a main, construya una imagen Docker de un proyecto y la etiquete con el SHA del commit. No es necesario publicarla todavía.

Ejercicio 2

Modifica el ejercicio anterior para que, antes de etiquetar, ejecute las pruebas dentro de un contenedor creado desde la imagen recién construida.

Ejercicio 3

Explica por qué es mala práctica desplegar siempre la etiqueta latest en producción y propón una alternativa.

Soluciones

Solución al Ejercicio 1:

name: Build de la imagen
on:
  push:
    branches:
      - main
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Construir imagen etiquetada con el SHA
        run: docker build -t miapp:${{ github.sha }} .

El workflow se dispara en cada push a main, clona el código y construye la imagen usando github.sha como etiqueta.

Solución al Ejercicio 2:

name: Build y Test
on:
  push:
    branches:
      - main
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Construir imagen
        run: docker build -t miapp:${{ github.sha }} .
      - name: Ejecutar pruebas
        run: docker run --rm miapp:${{ github.sha }} npm test

Se añade un paso que arranca un contenedor desde la imagen recién construida y ejecuta npm test. Si las pruebas fallan, el job falla y el flujo se detiene.

Solución al Ejercicio 3:

Desplegar siempre latest es mala práctica porque la etiqueta no identifica una versión concreta: el contenido al que apunta cambia con cada publicación. Esto impide saber qué código está realmente en producción, complica los rollbacks y puede provocar que dos servidores ejecuten versiones distintas con la misma etiqueta. La alternativa es etiquetar cada imagen con el SHA del commit o con una versión semántica (1.4.2) y desplegar esa etiqueta inmutable.

Conclusión

En este tema hemos visto cómo Docker se integra en los pipelines de CI/CD para construir, probar, etiquetar y publicar imágenes de forma automática y reproducible. Hemos creado ejemplos con GitHub Actions y GitLab CI, aprendido buenas prácticas de versionado y comprendido cómo proteger las credenciales del registro con secrets. Con una imagen publicada y versionada, el siguiente paso natural es ejecutarla a escala: en el próximo tema, Orquestando Contenedores con Docker Swarm, aprenderemos a desplegar y gestionar contenedores en un clúster.

© Copyright 2026. Todos los derechos reservados