Qué es CI/CD y por qué lo necesitas
CI (Continuous Integration) — recopilamos y probamos cada edición automáticamente.
CD (Continuous Delivery/Deployment) — después del éxito, lo llevamos al stand/producto sin manos.
Ventajas: menos errores manuales, lanzamientos rápidos, transparencia para el equipo y despliegues predecibles. 🧘♂️
Servidor: formación básica (VPS)
# 1) Docker y el plugin compose (Ubuntu)
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# 2) Catálogo de aplicaciones
sudo mkdir -p /opt/myapp && sudo chown -R $USER:$USER /opt/myapp
cd /opt/myapp
# 3) .env (en el servidor, no se confirma)
cat > .env << 'ENV'
PORT=3000
ENV
# 4) docker-compose.yml
cat > docker-compose.yml << 'YAML'
services:
web:
image: registry.example.com/myapp:latest
env_file: .env
ports:
- "80:3000"
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 10s
timeout: 3s
retries: 5
YAML
# 5) Primer lanzamiento (sin CI por ahora)
docker compose pull && docker compose up -d
Dockerfile (ejemplo mínimo para Node/PNPM)
# Dockerfile
FROM node:20-alpine AS deps
RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm i --frozen-lockfile
FROM node:20-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules node_modules
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/dist dist
COPY package.json ./
RUN corepack enable && corepack prepare pnpm@latest --activate && pnpm i --prod --frozen-lockfile
EXPOSE 3000
CMD ["node", "dist/server.js"]
GitHub Actions: compilación de imágenes e implementación de SSH
Añade .github/workflows/deploy.yml al repositorio:
name: CI/CD Deploy
on:
push:
branches: [ "main" ]
permissions:
contents: read
packages: write
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ${{ secrets.REGISTRY_URL }}
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build & push image
uses: docker/build-push-action@v6
with:
push: true
context: .
tags: ${{ secrets.REGISTRY_URL }}/${{ secrets.IMAGE_NAME }}:latest
- name: Deploy via SSH
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
set -e
cd /opt/myapp
docker compose pull
docker compose up -d
docker image prune -f
Secretos: REGISTRY_URL, REGISTRY_USER, REGISTRY_PASSWORD, IMAGE_NAME, SERVER_HOST, SERVER_USER, SERVER_SSH_KEY.
GitLab CI: una alternativa
# .gitlab-ci.yml
stages: [build, deploy]
variables:
IMAGE: $CI_REGISTRY_IMAGE:latest
build:
stage: build
image: docker:27.0
services: [ "docker:27.0-dind" ]
script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
- docker build -t $IMAGE .
- docker push $IMAGE
artifacts:
expire_in: 1 week
when: on_success
paths: []
deploy:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
script:
- ssh -o StrictHostKeyChecking=no $DEPLOY_USER@$DEPLOY_HOST "
set -e
cd /opt/myapp &&
docker compose pull &&
docker compose up -d &&
docker image prune -f
"
only:
- main
Cero tiempo de inactividad y retrocesos
Zero‑downtime:
restart: unless-stopped+healthcheck+up -d— el nuevo contenedor se inicia, el antiguo se apaga.Etiquetas de lanzamiento: además de
:latestenvíe:v1.4.2— es más fácil revertir.Rollback: cambie la etiqueta a
docker-compose.ymly repitapull/up -d.Esquema azul/verde (simplificado): mantén dos servicios
web_blueyweb_green, equilibra el tráfico a través de Nginx.
Comprobaciones mínimas en el pipeline
# ejemplo de un paso con pruebas en GitHub Actions
- name: Install deps & test
run: |
npm ci
npm run test -- --ci
Umbral para el CI «nocturno»: ejecutar las pruebas unitarias, compilar, pasar el linter (ESLint/flake8), verificar los tipos (tsc/mypy).
Monitoreo y registro
Añada
/healthy/metrics(formato Prometheus) al servicio.Recopilación de registros:
docker logs→ Loki/ELK; alertas de código de estado/latencia.En CI, artefactos con informes de pruebas y linter para ver regresiones.
Secretos y seguridad
Secretos solo en Secrets/Variables CI;
.env— en el servidor.Deshabilite el inicio de sesión SSH con contraseña, deje el inicio de sesión con clave, restrinja los puertos.
No almacenes claves privadas en el repositorio, ni siquiera en forma cifrada.
🧯 Problemas típicos y soluciones rápidas
Síntoma | Motivo | Fijo |
|---|---|---|
El CI se cae en el montaje | Poca RAM/tiempo de espera agotado | Caché de capas, base de imagen más ligera, aumentar el tiempo de espera |
La aplicación no se inicia | Puerto/ENV/migración | Comprueba |
Despliegue lento | Imagen pesada | Multi-stage, alpine, .dockerignore, almacenamiento en caché de dependencias |
«Funciona para mí» | Diferentes versiones de Node/Python | Fijar versiones en Docker, usar archivos de bloqueo |
Disco lleno | Imágenes/contenedores antiguos |
|
En el anexo «Kodik: aprendizaje de programación» — lecciones cortas y miniproyectos. ¡Nosotros somos interesantes!
Y también tenemos un canal de telegram, donde discutimos ideas geniales, compartimos experiencias y analizamos juntos las tareas: aprender no solo es útil, sino también divertido.
Total
CI/CD no es «gran magia», sino un conjunto de pasos simples. Compilar una imagen, enviarla al registro, reiniciar el servicio en VPS y tendrás versiones rápidas y repetibles sin una rutina manual. Empieza hoy y mañana el equipo olvidará cómo era el «despliegue manual».
¿En qué vas a ejecutar el despliegue automático: GitHub Actions o GitLab CI?
