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│ │ │ │
│ └──────┘ └──────┘ └───────┘ └──────┘ │
└──────────────────────────────────────┘
- Build — Construye la imagen Docker de tu aplicación
- Test — Ejecuta linter y pruebas dentro del contenedor
- Push — Sube la imagen al registry (GitLab Container Registry)
- 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:
| Variable | Valor | Protegida | Enmascarada |
|---|---|---|---|
SSH_PRIVATE_KEY | Llave privada SSH para conectar al servidor | ✅ | ✅ |
SSH_KNOWN_HOSTS | Fingerprint del servidor (ssh-keyscan tu-servidor) | ✅ | ❌ |
SSH_USER | Usuario SSH (deploy) | ✅ | ❌ |
SSH_HOST | IP 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 applyo 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.



