¿Todavía subes archivos por FTP o haces SSH para hacer git pull? Hay una forma mejor. CI/CD automatiza tus despliegues: haces push y tu VPS se actualiza solo.
Esta guía te enseña a configurar despliegues automáticos desde cero.
¿Qué es CI/CD?
Definiciones
| Término | Significado | Ejemplo |
|---|---|---|
| CI | Continuous Integration | Tests automáticos en cada commit |
| CD | Continuous Deployment | Deploy automático tras pasar tests |
| Pipeline | Secuencia de pasos | Build → Test → Deploy |
Despliegue manual vs CI/CD
| Manual | CI/CD |
|---|---|
| SSH al servidor | Push a Git |
| git pull | Automático |
| Reiniciar servicios manualmente | Automático |
| Propenso a errores humanos | Consistente |
| ”Se me olvidó…” | Siempre igual |
Opciones de CI/CD
| Opción | Complejidad | Coste | Ideal para |
|---|---|---|---|
| GitHub Actions | Baja | Gratis (2000 min/mes) | Proyectos en GitHub |
| GitLab CI | Baja | Gratis (400 min/mes) | Proyectos en GitLab |
| Webhook + Script | Muy baja | Gratis | Máximo control |
| Jenkins | Alta | Gratis (self-hosted) | Empresas grandes |
GitHub Actions: La opción más popular
Estructura básica
# .github/workflows/deploy.yml
name: Deploy to VPS
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to VPS
uses: appleboy/[email protected]
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
script: |
cd /var/www/mi-app
git pull origin main
npm install
npm run build
pm2 restart mi-app
Configurar secrets en GitHub
- Ve a tu repositorio → Settings → Secrets and variables → Actions
- Añade estos secrets:
VPS_HOST: IP de tu VPSVPS_USER: Usuario SSH (ej: deploy)VPS_SSH_KEY: Clave privada SSH
Generar clave SSH para deploy
# En tu máquina local
ssh-keygen -t ed25519 -C "github-actions" -f ~/.ssh/github_deploy
# Copiar clave pública al VPS
ssh-copy-id -i ~/.ssh/github_deploy.pub usuario@tu-vps
# La clave privada (~/.ssh/github_deploy) va al secret VPS_SSH_KEY
cat ~/.ssh/github_deploy
Workflow completo con tests
name: CI/CD Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Run linter
run: npm run lint
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to VPS
uses: appleboy/[email protected]
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
script: |
cd /var/www/mi-app
git pull origin main
npm ci --production
npm run build
pm2 restart mi-app
Deploy con Docker
name: Deploy Docker
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and push to registry
run: |
echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
docker build -t miusuario/mi-app:latest .
docker push miusuario/mi-app:latest
- name: Deploy to VPS
uses: appleboy/[email protected]
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
script: |
docker pull miusuario/mi-app:latest
docker stop mi-app || true
docker rm mi-app || true
docker run -d --name mi-app -p 3000:3000 miusuario/mi-app:latest
GitLab CI
Estructura básica
# .gitlab-ci.yml
stages:
- test
- deploy
variables:
NODE_VERSION: "20"
test:
stage: test
image: node:20
script:
- npm ci
- npm test
- npm run lint
deploy:
stage: deploy
only:
- main
before_script:
- 'which ssh-agent || apt-get update -y && apt-get install openssh-client -y'
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
script:
- ssh $SSH_USER@$SSH_HOST "cd /var/www/mi-app && git pull && npm ci && npm run build && pm2 restart mi-app"
Variables en GitLab
Settings → CI/CD → Variables:
SSH_PRIVATE_KEY: Clave privadaSSH_USER: Usuario SSHSSH_HOST: IP del VPSSSH_KNOWN_HOSTS: Resultado dessh-keyscan tu-vps
Webhook + Script (Máximo control)
El enfoque más simple
- GitHub/GitLab envía webhook a tu VPS
- Script en el VPS recibe y ejecuta deploy
Script de deploy en el VPS
#!/bin/bash
# /var/www/scripts/deploy.sh
set -e
APP_DIR="/var/www/mi-app"
LOG_FILE="/var/log/deploy.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a $LOG_FILE
}
log "========== INICIO DEPLOY =========="
cd $APP_DIR
# Guardar estado actual por si hay que rollback
PREV_COMMIT=$(git rev-parse HEAD)
log "Commit anterior: $PREV_COMMIT"
# Pull cambios
log "Descargando cambios..."
git pull origin main
NEW_COMMIT=$(git rev-parse HEAD)
log "Nuevo commit: $NEW_COMMIT"
# Instalar dependencias
log "Instalando dependencias..."
npm ci --production
# Build
log "Construyendo..."
npm run build
# Reiniciar aplicación
log "Reiniciando aplicación..."
pm2 restart mi-app
# Verificar que funciona
sleep 5
if curl -s http://localhost:3000/health | grep -q "ok"; then
log "✓ Deploy exitoso"
else
log "✗ ERROR: La aplicación no responde. Haciendo rollback..."
git checkout $PREV_COMMIT
npm ci --production
npm run build
pm2 restart mi-app
log "Rollback completado"
exit 1
fi
log "========== FIN DEPLOY =========="
Servidor webhook simple (Node.js)
// /var/www/webhook/server.js
const http = require('http');
const crypto = require('crypto');
const { exec } = require('child_process');
const SECRET = process.env.WEBHOOK_SECRET;
const PORT = 9000;
const server = http.createServer((req, res) => {
if (req.method !== 'POST' || req.url !== '/deploy') {
res.writeHead(404);
return res.end('Not found');
}
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
// Verificar firma de GitHub
const signature = req.headers['x-hub-signature-256'];
const hmac = crypto.createHmac('sha256', SECRET);
const digest = 'sha256=' + hmac.update(body).digest('hex');
if (signature !== digest) {
res.writeHead(401);
return res.end('Invalid signature');
}
// Ejecutar deploy
exec('/var/www/scripts/deploy.sh', (error, stdout, stderr) => {
if (error) {
console.error('Deploy failed:', error);
res.writeHead(500);
return res.end('Deploy failed');
}
console.log('Deploy output:', stdout);
res.writeHead(200);
res.end('Deploy started');
});
});
});
server.listen(PORT, () => {
console.log(`Webhook server listening on port ${PORT}`);
});
# Ejecutar con PM2
pm2 start /var/www/webhook/server.js --name webhook
Configurar webhook en GitHub
- Repositorio → Settings → Webhooks → Add webhook
- Payload URL:
http://tu-vps:9000/deploy - Content type:
application/json - Secret: tu secret
- Events: Just the push event
Deploy de WordPress
Script para WordPress
#!/bin/bash
# /var/www/scripts/deploy-wordpress.sh
set -e
WP_DIR="/var/www/wordpress"
THEME_DIR="$WP_DIR/wp-content/themes/mi-tema"
cd $THEME_DIR
# Pull cambios del tema
git pull origin main
# Instalar dependencias del tema (si usa npm)
if [ -f "package.json" ]; then
npm ci
npm run build
fi
# Limpiar caché de WordPress
wp cache flush --path=$WP_DIR
# Limpiar caché de objeto (Redis)
wp redis flush --path=$WP_DIR 2>/dev/null || true
echo "Deploy WordPress completado"
GitHub Actions para WordPress
name: Deploy WordPress Theme
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Build theme
run: |
npm ci
npm run build
- name: Deploy via rsync
uses: burnett01/[email protected]
with:
switches: -avz --delete --exclude=node_modules
path: ./
remote_path: /var/www/wordpress/wp-content/themes/mi-tema/
remote_host: ${{ secrets.VPS_HOST }}
remote_user: ${{ secrets.VPS_USER }}
remote_key: ${{ secrets.VPS_SSH_KEY }}
- name: Clear cache
uses: appleboy/[email protected]
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
script: |
wp cache flush --path=/var/www/wordpress
Deploy con zero downtime
Estrategia blue-green
#!/bin/bash
# deploy-blue-green.sh
APP_DIR="/var/www"
CURRENT="$APP_DIR/current"
BLUE="$APP_DIR/blue"
GREEN="$APP_DIR/green"
# Determinar cuál es el activo
if [ -L "$CURRENT" ] && [ "$(readlink $CURRENT)" = "$BLUE" ]; then
DEPLOY_TO=$GREEN
ACTIVE=$BLUE
else
DEPLOY_TO=$BLUE
ACTIVE=$GREEN
fi
echo "Desplegando en: $DEPLOY_TO"
# Clonar/actualizar en el directorio inactivo
cd $DEPLOY_TO
git pull origin main
npm ci --production
npm run build
# Cambiar symlink (atómico, zero downtime)
ln -sfn $DEPLOY_TO $CURRENT
# Reiniciar (Nginx apunta a $CURRENT)
pm2 restart mi-app
echo "Deploy completado. Activo: $DEPLOY_TO"
Nginx con symlink
server {
root /var/www/current/public;
# ...
}
Notificaciones de deploy
Slack
# Añadir al final del workflow
- name: Notify Slack
if: always()
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,message,commit,author
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
Telegram
# En el script de deploy
notify_telegram() {
curl -s -X POST "https://api.telegram.org/bot$BOT_TOKEN/sendMessage" \
-d chat_id="$CHAT_ID" \
-d text="$1" \
-d parse_mode="HTML"
}
notify_telegram "✅ <b>Deploy exitoso</b>
Commit: $NEW_COMMIT
Servidor: $(hostname)"
Rollback automático
Script con rollback
#!/bin/bash
# deploy-with-rollback.sh
set -e
APP_DIR="/var/www/mi-app"
HEALTH_URL="http://localhost:3000/health"
cd $APP_DIR
# Guardar commit actual
PREV_COMMIT=$(git rev-parse HEAD)
# Función de rollback
rollback() {
echo "ERROR: Haciendo rollback a $PREV_COMMIT"
git checkout $PREV_COMMIT
npm ci --production
npm run build
pm2 restart mi-app
exit 1
}
# Trap para rollback en caso de error
trap rollback ERR
# Deploy
git pull origin main
npm ci --production
npm run build
pm2 restart mi-app
# Esperar y verificar
sleep 10
if ! curl -sf $HEALTH_URL > /dev/null; then
rollback
fi
echo "Deploy exitoso"
Preguntas frecuentes
¿Es seguro dar acceso SSH a GitHub Actions?
Sí, si usas una clave específica para deploy con permisos limitados. Crea un usuario 'deploy' que solo pueda acceder a las carpetas necesarias y ejecutar comandos específicos.
¿Qué pasa si el deploy falla a mitad del proceso?
Implementa rollback automático. Guarda el commit anterior y revierte si el health check falla. O usa estrategia blue-green para zero downtime y rollback instantáneo.
¿Puedo hacer deploy desde múltiples ramas?
Sí. Configura diferentes workflows o jobs para cada rama. Por ejemplo, 'develop' a staging y 'main' a producción.
¿GitHub Actions es gratis?
Para repositorios públicos, sí. Para privados, tienes 2000 minutos/mes gratis. Un deploy típico usa 1-3 minutos, así que da para bastantes deploys.
¿Webhook o GitHub Actions?
GitHub Actions es más completo (tests, builds, etc). Webhook es más simple si solo necesitas ejecutar un script. Para proyectos serios, GitHub Actions.
Nuestra recomendación
Para empezar:
- Configura GitHub Actions básico
- Añade health check
- Implementa notificaciones
Para producción:
- Tests antes de deploy
- Rollback automático
- Notificaciones Slack/Telegram
- Blue-green para zero downtime
¿Necesitas ayuda con CI/CD? La administración gestionada de Avantys incluye configuración de pipelines de despliegue.
Conclusión
CI/CD elimina el factor humano de los despliegues. Configúralo una vez y cada push a main despliega automáticamente, con tests y rollback si algo falla.
Empieza con un workflow simple y añade complejidad según necesites.
¿Necesitas un VPS para CI/CD? Explora los VPS de Avantys con rendimiento óptimo para despliegues.
¿Quieres que lo hagamos por ti?
En Avantys gestionamos tu web, hosting y crecimiento digital de punta a punta. Tú a lo importante.