Hosting Equipo Avantys 8 min

CI/CD en VPS: Automatiza tus Despliegues

Configura despliegues automáticos en tu VPS. GitHub Actions, GitLab CI, webhooks y scripts para deploy sin intervención manual.

// Compartir

CI/CD en VPS: Automatiza tus Despliegues
CI/CD en VPS

¿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?

Flujo CI/CD en VPS

Definiciones

TérminoSignificadoEjemplo
CIContinuous IntegrationTests automáticos en cada commit
CDContinuous DeploymentDeploy automático tras pasar tests
PipelineSecuencia de pasosBuild → Test → Deploy

Despliegue manual vs CI/CD

ManualCI/CD
SSH al servidorPush a Git
git pullAutomático
Reiniciar servicios manualmenteAutomático
Propenso a errores humanosConsistente
”Se me olvidó…”Siempre igual

Opciones de CI/CD

Opciones CI/CD para VPS
OpciónComplejidadCosteIdeal para
GitHub ActionsBajaGratis (2000 min/mes)Proyectos en GitHub
GitLab CIBajaGratis (400 min/mes)Proyectos en GitLab
Webhook + ScriptMuy bajaGratisMáximo control
JenkinsAltaGratis (self-hosted)Empresas grandes

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

  1. Ve a tu repositorio → Settings → Secrets and variables → Actions
  2. Añade estos secrets:
    • VPS_HOST: IP de tu VPS
    • VPS_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 privada
  • SSH_USER: Usuario SSH
  • SSH_HOST: IP del VPS
  • SSH_KNOWN_HOSTS: Resultado de ssh-keyscan tu-vps

Webhook + Script (Máximo control)

El enfoque más simple

  1. GitHub/GitLab envía webhook a tu VPS
  2. 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

  1. Repositorio → Settings → Webhooks → Add webhook
  2. Payload URL: http://tu-vps:9000/deploy
  3. Content type: application/json
  4. Secret: tu secret
  5. 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"
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:

  1. Configura GitHub Actions básico
  2. Añade health check
  3. 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.

Hablar con Avantys
// Boletín

Suscríbete al boletín

Guías nuevas, sin spam. Cancela cuando quieras.