Listado de la etiqueta: pipeline

Docker en mi pipeline de CI/CD: builds reproducibles sin sorpresas

Antes de Docker, nuestro pipeline de CI era una caja negra. «Funcionó en mi máquina pero el build del CI falló». Después de adoptar Docker en el pipeline, los builds fallidos por diferencias de entorno desaparecieron. Si buildea en CI, buildea en producción. Siempre.

Por qué Docker transforma el CI/CD

El problema fundamental del CI tradicional: el servidor de CI tiene su propio entorno — versiones de runtime, librerías del sistema, variables — que puede diferir del de desarrollo y producción. Docker elimina ese problema: el build ocurre dentro de un contenedor con el entorno exacto que vos definís en el Dockerfile.

GitHub Actions: build, test y push a registry

# .github/workflows/ci-cd.yml
name: Build, Test and Deploy

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
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Container Registry
        if: github.event_name != 'pull_request'
        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=ref,event=branch
            type=semver,pattern={{version}}
            type=sha,prefix=sha-

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha    # cache de GitHub Actions
          cache-to: type=gha,mode=max

  deploy:
    needs: build-and-test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - name: Deploy to production
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.PROD_HOST }}
          username: ${{ secrets.PROD_USER }}
          key: ${{ secrets.PROD_SSH_KEY }}
          script: |
            cd /apps/miapp
            docker compose pull
            docker compose up -d --no-build
            docker image prune -f

GitLab CI: Docker-in-Docker

# .gitlab-ci.yml
stages:
  - build
  - test
  - push
  - deploy

variables:
  DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
  DOCKER_TLS_CERTDIR: "/certs"

# Build de la imagen
build:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker build --cache-from $CI_REGISTRY_IMAGE:latest -t $DOCKER_IMAGE .
    - docker push $DOCKER_IMAGE

# Tests dentro de contenedor
test:
  stage: test
  image: $DOCKER_IMAGE
  script:
    - dotnet test --no-build --verbosity normal

# Tag como latest en main
push-latest:
  stage: push
  image: docker:24
  services:
    - docker:24-dind
  only:
    - main
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker pull $DOCKER_IMAGE
    - docker tag $DOCKER_IMAGE $CI_REGISTRY_IMAGE:latest
    - docker push $CI_REGISTRY_IMAGE:latest

# Deploy al servidor
deploy-prod:
  stage: deploy
  only:
    - main
  script:
    - ssh -i $SSH_KEY deploy@$PROD_HOST
        "cd /apps/miapp &&
         docker compose pull &&
         docker compose up -d --no-build"

Optimizar el tiempo de build con cache

El mayor costo de tiempo en un pipeline Docker es el build. Con la caché bien configurada, un build que tardaba 8 minutos puede bajar a 40 segundos si solo cambió el código de la aplicación.

# La clave: copiar el .csproj ANTES que el código
# Así el restore se cachea hasta que cambien las dependencias

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

# Esta capa se cachea hasta que cambie el .csproj
COPY ["MiApi.csproj", "."]
RUN dotnet restore

# Esta capa se invalida con cada commit
COPY . .
RUN dotnet publish -c Release -o /app/publish

El resultado en números

Antes de implementar este pipeline: builds inconsistentes, ~20 minutos por build, deploys manuales con SSH y comandos a mano. Después: builds reproducibles, 2-4 minutos con cache (8 min cold), deploy automático en cada push a main, rollback trivial con docker compose up -d apuntando a la imagen anterior. Menos stress, más confianza en cada deploy.


Artículo anterior: Entornos consistentes | Serie Docker Completo | Próximo: Microservicios →


Artículo anterior: Cómo uso Docker para tener el mismo entorno en dev, test y producción | Serie Docker Completo | Próximo: Microservicios con Docker: lo que aprendí armando mi primera arquitectura →