Tutoriales 11 min de lectura

Desplegar aplicaciones con CI/CD en GitLab — guía práctica

Guía paso a paso para configurar pipelines de CI/CD en GitLab que compilen, prueben y desplieguen tu aplicación automáticamente en cada push a producción.

Interfaz de GitLab mostrando un pipeline de CI/CD con etapas de build, test y deploy completadas exitosamente
Interfaz de GitLab mostrando un pipeline de CI/CD con etapas de build, test y deploy completadas exitosamente

Desplegar manualmente es el proceso más propenso a errores en el desarrollo de software. Alguien se conecta por SSH al servidor, hace un git pull, reinicia servicios, cruza los dedos y espera que todo funcione. Cuando no funciona, nadie sabe qué cambió ni cómo volver atrás. CI/CD elimina ese proceso completo: tú haces push a git, el pipeline se encarga del resto.

En esta guía vas a configurar un pipeline de CI/CD en GitLab que compila tu aplicación en un contenedor Docker, ejecuta pruebas, construye la imagen de producción y la despliega en tu servidor — todo automáticamente con cada push a la rama main.

Arquitectura del pipeline

┌──────┐     ┌──────────────────────────────────────┐     ┌──────────┐
│ Push │────▶│          GitLab CI Pipeline           │────▶│ Servidor │
│ main │     │                                        │     │producción│
└──────┘     │ ┌──────┐ ┌──────┐ ┌───────┐ ┌──────┐ │     └──────────┘
             │ │Build │→│ Test │→│ Push  │→│Deploy│ │
             │ │image │ │      │ │registry│ │      │ │
             │ └──────┘ └──────┘ └───────┘ └──────┘ │
             └──────────────────────────────────────┘
  1. Build — Construye la imagen Docker de tu aplicación
  2. Test — Ejecuta linter y pruebas dentro del contenedor
  3. Push — Sube la imagen al registry (GitLab Container Registry)
  4. Deploy — Conecta al servidor y actualiza los contenedores

Requisitos previos

Un proyecto en GitLab con un Dockerfile funcional, un servidor de producción con Docker instalado y acceso SSH desde el runner de CI.

Paso 1: Configurar el GitLab Runner

El runner es la máquina que ejecuta los jobs del pipeline. Puedes usar los runners compartidos de GitLab.com o instalar uno propio en tu servidor:

# En tu servidor (o en un servidor dedicado a CI)
curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bash
sudo apt install gitlab-runner -y

Registra el runner con tu proyecto:

sudo gitlab-runner register

Te pedirá:

  • GitLab instance URL: https://gitlab.com (o tu instancia propia)
  • Registration token: lo encuentras en Settings → CI/CD → Runners de tu proyecto
  • Description: runner-produccion
  • Tags: docker,deploy
  • Executor: docker
  • Default Docker image: docker:24

Verifica que el runner está activo:

sudo gitlab-runner status

Docker-in-Docker (DinD)

Para que el runner pueda construir imágenes Docker, necesita acceso al daemon de Docker. La forma más simple es montar el socket de Docker. Edita /etc/gitlab-runner/config.toml y agrega en la sección [runners.docker]:

volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]

Reinicia: sudo gitlab-runner restart

Paso 2: Preparar el Dockerfile

Tu aplicación necesita un Dockerfile optimizado para producción. Ejemplo para una API con Node.js:

# Dockerfile
# --- Etapa 1: Build ---
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

# --- Etapa 2: Producción ---
FROM node:20-alpine
WORKDIR /app
RUN addgroup -S app && adduser -S app -G app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
USER app
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/main.js"]

Paso 3: El archivo .gitlab-ci.yml

Este es el corazón del pipeline. Crea el archivo en la raíz de tu proyecto:

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

variables:
  IMAGE_NAME: $CI_REGISTRY_IMAGE
  IMAGE_TAG: $CI_COMMIT_SHORT_SHA

# ==============================
# BUILD — Construir imagen Docker
# ==============================
build:
  stage: build
  image: docker:24
  tags: [docker]
  script:
    - docker build -t $IMAGE_NAME:$IMAGE_TAG -t $IMAGE_NAME:latest .
    - docker save $IMAGE_NAME:$IMAGE_TAG > image.tar
  artifacts:
    paths:
      - image.tar
    expire_in: 1 hour
  only:
    - main
    - merge_requests

# ==============================
# TEST — Ejecutar pruebas
# ==============================
test:lint:
  stage: test
  image: node:20-alpine
  tags: [docker]
  script:
    - npm ci
    - npm run lint
  only:
    - main
    - merge_requests

test:unit:
  stage: test
  image: node:20-alpine
  tags: [docker]
  services:
    - postgres:16-alpine
  variables:
    POSTGRES_DB: test
    POSTGRES_USER: test
    POSTGRES_PASSWORD: test
    DATABASE_URL: postgresql://test:test@postgres:5432/test
  script:
    - npm ci
    - npm run test
  only:
    - main
    - merge_requests

# ==============================
# PUSH — Subir imagen al registry
# ==============================
push:
  stage: push
  image: docker:24
  tags: [docker]
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker load < image.tar
    - docker push $IMAGE_NAME:$IMAGE_TAG
    - docker push $IMAGE_NAME:latest
  only:
    - main

# ==============================
# DEPLOY — Desplegar en producción
# ==============================
deploy:production:
  stage: deploy
  image: alpine:latest
  tags: [docker]
  before_script:
    - apk add --no-cache openssh-client
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | ssh-add -
    - mkdir -p ~/.ssh
    - echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
  script:
    - |
      ssh $SSH_USER@$SSH_HOST << 'DEPLOY'
        cd /opt/mi-app
        
        # Login al registry
        docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
        
        # Pull nueva imagen
        docker compose pull app
        
        # Deploy con zero-downtime
        docker compose up -d --no-deps app
        
        # Verificar health
        sleep 10
        if ! docker compose exec -T app wget -qO- http://localhost:3000/health; then
          echo "HEALTH CHECK FAILED — Rollback!"
          docker compose rollback app 2>/dev/null || docker compose up -d
          exit 1
        fi
        
        # Limpiar imágenes antiguas
        docker image prune -f
        
        echo "Deploy completado: $IMAGE_TAG"
      DEPLOY
  environment:
    name: production
    url: https://app.tuempresa.com
  only:
    - main
  when: manual  # Requiere click manual para desplegar

Conceptos clave

stages — Define el orden de ejecución. Todos los jobs de un stage se ejecutan en paralelo; el siguiente stage empieza cuando todos terminaron.

artifacts — Archivos que se pasan entre stages. La imagen construida en build se pasa a push como archivo tar.

services — Contenedores auxiliares que corren junto al job. Aquí PostgreSQL para las pruebas unitarias.

when: manual — El deploy a producción requiere un click manual en la interfaz de GitLab. Esto te da control sobre cuándo sale un cambio a producción.

environment — GitLab registra cada deploy con su commit, fecha y URL — historial completo de qué se desplegó y cuándo.

Paso 4: Variables de CI/CD

Configura las variables secretas en Settings → CI/CD → Variables de tu proyecto:

VariableValorProtegidaEnmascarada
SSH_PRIVATE_KEYLlave privada SSH para conectar al servidor
SSH_KNOWN_HOSTSFingerprint del servidor (ssh-keyscan tu-servidor)
SSH_USERUsuario SSH (deploy)
SSH_HOSTIP o dominio del servidor

Nunca pongas secretos en el .gitlab-ci.yml

Las llaves SSH, contraseñas y tokens van en las variables de CI/CD de GitLab — cifradas y solo disponibles durante la ejecución del pipeline. Marcarlas como "Protected" las limita a ramas protegidas (como main).

Paso 5: Preparar el servidor de producción

En tu servidor, crea un usuario dedicado para los deploys:

# Crear usuario deploy
sudo adduser --disabled-password deploy
sudo usermod -aG docker deploy

# Configurar llave SSH
sudo mkdir -p /home/deploy/.ssh
# Copia la llave pública que corresponde a SSH_PRIVATE_KEY
echo "ssh-ed25519 AAAA...tu-llave-publica" | sudo tee /home/deploy/.ssh/authorized_keys
sudo chown -R deploy:deploy /home/deploy/.ssh
sudo chmod 700 /home/deploy/.ssh
sudo chmod 600 /home/deploy/.ssh/authorized_keys

Crea la estructura de la aplicación:

sudo mkdir -p /opt/mi-app
sudo chown deploy:deploy /opt/mi-app

Crea el docker-compose.yml de producción en el servidor:

# /opt/mi-app/docker-compose.yml
services:
  app:
    image: registry.gitlab.com/tu-usuario/mi-app:latest
    restart: unless-stopped
    environment:
      DATABASE_URL: postgresql://app:${DB_PASSWORD}@db:5432/miapp
      NODE_ENV: production
    depends_on:
      db:
        condition: service_healthy
    networks:
      - proxy
      - backend
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.mi-app.rule=Host(`app.tuempresa.com`)"
      - "traefik.http.routers.mi-app.entrypoints=websecure"
      - "traefik.http.routers.mi-app.tls.certresolver=letsencrypt"
      - "traefik.http.services.mi-app.loadbalancer.server.port=3000"

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: miapp
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pg_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d miapp"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - backend

networks:
  proxy:
    external: true
  backend:

volumes:
  pg_data:

Paso 6: Ejecutar el primer pipeline

Haz un commit y push:

git add .gitlab-ci.yml Dockerfile
git commit -m "feat: agregar pipeline CI/CD"
git push origin main

Ve a CI/CD → Pipelines en GitLab. Verás tu pipeline ejecutándose con las 4 etapas. Si build y test pasan, la etapa de push sube la imagen al registry. El deploy espera tu click manual.

Haz click en el botón ▶️ del job deploy:production para desplegar.

Paso 7: Pipeline para merge requests

Una práctica esencial es ejecutar build y tests en cada merge request para que los problemas se detecten antes de mergear a main:

El pipeline ya está configurado con only: - merge_requests en las etapas de build y test. Cuando alguien abre un MR, GitLab ejecuta automáticamente el build y las pruebas. Si fallan, el MR se marca con ❌ y no se puede mergear.

Optimizaciones

Caché de dependencias

Evita descargar node_modules en cada pipeline:

test:unit:
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - node_modules/
  script:
    - npm ci
    - npm run test

Notificaciones a Slack

Agrega un job de notificación al final del pipeline:

notify:
  stage: .post
  image: alpine:latest
  script:
    - apk add --no-cache curl
    - |
      curl -X POST "$SLACK_WEBHOOK" \
        -H "Content-Type: application/json" \
        -d "{\"text\":\"✅ Deploy $CI_COMMIT_SHORT_SHA completado en producción por $GITLAB_USER_NAME\"}"
  only:
    - main
  when: on_success

Rollback rápido

Si un deploy sale mal, vuelve a la versión anterior con un click. En CI/CD → Environments → production, GitLab muestra el historial de deploys. Haz click en "Re-deploy" en la versión anterior.

O desde la terminal:

ssh deploy@tu-servidor "cd /opt/mi-app && docker compose pull app && docker compose up -d"

Cambiando el tag de la imagen al commit anterior.

Siguientes pasos

Con el pipeline básico funcionando, puedes expandir:

  • Stages de staging — deploy automático a un ambiente de pruebas antes de producción
  • Feature flags — desplegar código inactivo y activarlo gradualmente
  • Kubernetes — deploy a un cluster K8s con kubectl apply o Helm charts
  • Seguridad en el pipeline — escaneo de vulnerabilidades en imágenes Docker con Trivy
  • Monitoreo post-deploy — verificar métricas de la aplicación después de cada deploy
  • Automatización avanzada — pipelines que disparan migraciones de base de datos, limpieza de caché y notificaciones a stakeholders

DevOps profesional

¿Necesitas pipelines CI/CD para tu equipo?

Implementamos CI/CD con GitLab o GitHub Actions para que tu equipo despliegue con confianza, sin errores manuales y con rollback automático.

Solicitar evaluación

Preguntas frecuentes

Temas relacionados

#cicd#gitlab#devops#docker#automatizacion#tutorial

¿Te fue útil? Compártelo

Artículos relacionados

Ver todos

Consultoría gratuita

¿Necesitas automatizar tus despliegues?

Implementamos pipelines CI/CD profesionales para que tu equipo despliegue con confianza — sin errores manuales y con rollback automático.

Solicitar evaluación