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 →

