--- title: "Despliegue continuo con webhooks de GitHub en Ubuntu 24.04" description: "Despliega tu aplicación en cada push a GitHub con webhook, un script con bloqueo y reversión automática, Nginx con HTTPS y firmas HMAC en Ubuntu 24.04." category: "CI/CD DevOps" language: es source: https://cubepath.com/es/docs/ci-cd-devops/implementacion-continua-con-webhook-y-scripts updated: 2026-09-25 --- # Despliegue continuo con webhooks de GitHub en Ubuntu 24.04 No siempre hace falta una plataforma de CI/CD completa para desplegar: para una aplicación en un único servidor basta con que GitHub avise al servidor en cada `push` y que este ejecute un script que actualiza el código y reinicia el servicio. En este tutorial instalarás [webhook](https://github.com/adnanh/webhook), una pequeña herramienta que recibe peticiones HTTP y ejecuta comandos, en Ubuntu 24.04. La configurarás para aceptar solo peticiones firmadas por GitHub desde la rama `main`, la publicarás detrás de Nginx con HTTPS y escribirás un script de despliegue que evita ejecuciones simultáneas y revierte el cambio si la aplicación no arranca. ## Requisitos previos Para seguir esta guía necesitas: - Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, y un usuario no root con privilegios `sudo`. - Una aplicación Node.js en un repositorio de GitHub, con `package.json` y `package-lock.json`, y Node.js instalado en el servidor en `/usr/bin` (paquetes de Ubuntu o NodeSource). - Un dominio o subdominio, como `deploy.your_domain`, con un registro DNS A que apunte al servidor. - Nginx instalado y los puertos 80 y 443 abiertos en el cortafuegos. El ejemplo usa una aplicación Node.js que se ejecuta como el servicio systemd `myapp.service`, pero el mismo esquema sirve para cualquier lenguaje: solo cambian los comandos de compilación del script. ## Paso 1: Preparar el usuario y el código en el servidor Todo el despliegue se ejecutará con un usuario sin privilegios llamado `deploy`, dueño del código de la aplicación. Créalo junto con el directorio de la aplicación: ```bash sudo useradd --create-home --shell /bin/bash deploy sudo mkdir -p /srv/myapp sudo chown deploy:deploy /srv/myapp ``` El servidor necesita leer el repositorio. Genera una clave SSH para el usuario `deploy`: ```bash sudo -u deploy install -d -m 700 /home/deploy/.ssh sudo -u deploy ssh-keygen -t ed25519 -N "" -C "deploy@myapp-server" -f /home/deploy/.ssh/id_ed25519 sudo cat /home/deploy/.ssh/id_ed25519.pub ``` Copia la clave pública y añádela en GitHub como *deploy key* del repositorio: **Settings > Deploy keys > Add deploy key**, sin marcar **Allow write access**. Una deploy key solo da acceso a ese repositorio, y en modo lectura. Clona el repositorio como `deploy`: ```bash sudo -u deploy git clone git@github.com:your_user/myapp.git /srv/myapp ``` La primera vez, SSH te pedirá confirmar la huella de `github.com`. Compárala con las huellas publicadas en la documentación de GitHub ("GitHub's SSH key fingerprints") antes de escribir `yes`. Comprueba que el clon funciona: ```bash sudo -u deploy git -C /srv/myapp log --oneline -1 ``` ```output a1b2c3d Initial commit ``` ## Paso 2: Crear el servicio de la aplicación Crea la unidad systemd de la aplicación: ```bash sudo nano /etc/systemd/system/myapp.service ``` Añade este contenido, ajustando `ExecStart` al punto de entrada de tu aplicación: ```ini [Unit] Description=myapp Node.js application After=network.target [Service] Type=simple User=deploy Group=deploy WorkingDirectory=/srv/myapp Environment=NODE_ENV=production EnvironmentFile=-/srv/myapp/.env ExecStart=/usr/bin/node server.js Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target ``` Instala las dependencias y arranca el servicio: ```bash sudo -u deploy bash -c 'cd /srv/myapp && npm ci --omit=dev' sudo systemctl daemon-reload sudo systemctl enable --now myapp.service ``` Comprueba que está activo: ```bash systemctl is-active myapp.service ``` ```output active ``` El usuario `deploy` tiene que poder reiniciar este servicio, y nada más. Crea una regla de sudoers limitada a ese comando: ```bash sudo visudo -f /etc/sudoers.d/deploy-myapp ``` Añade esta línea: ```text deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart myapp.service ``` Valida el archivo: ```bash sudo visudo -cf /etc/sudoers.d/deploy-myapp ``` ```output /etc/sudoers.d/deploy-myapp: parsed OK ``` ## Paso 3: Escribir el script de despliegue El script hace el trabajo real: trae el último commit de `main`, instala dependencias y reinicia el servicio. Además, usa `flock` para que dos `push` seguidos no desplieguen a la vez (el segundo espera a que termine el primero) y, si el servicio no queda activo tras el reinicio, vuelve al commit anterior. Crea el script: ```bash sudo nano /usr/local/bin/deploy-myapp.sh ``` Añade este contenido: ```bash #!/usr/bin/env bash set -euo pipefail APP_DIR="/srv/myapp" BRANCH="main" SERVICE="myapp.service" LOCK_FILE="/run/lock/deploy-myapp.lock" # Only one deployment at a time; later ones wait here. exec 9>"$LOCK_FILE" flock 9 cd "$APP_DIR" previous="$(git rev-parse HEAD)" git fetch --prune origin "$BRANCH" git reset --hard "origin/$BRANCH" current="$(git rev-parse HEAD)" if [[ "$previous" == "$current" ]]; then echo "Already at ${current:0:8}, nothing to deploy" exit 0 fi echo "Deploying ${previous:0:8} -> ${current:0:8}" npm ci --omit=dev sudo /usr/bin/systemctl restart "$SERVICE" sleep 5 if ! systemctl is-active --quiet "$SERVICE"; then echo "$SERVICE is not active after deploy, rolling back to ${previous:0:8}" >&2 git reset --hard "$previous" npm ci --omit=dev sudo /usr/bin/systemctl restart "$SERVICE" exit 1 fi echo "Deployed ${current:0:8}" ``` Hazlo ejecutable: ```bash sudo chmod 755 /usr/local/bin/deploy-myapp.sh ``` `git reset --hard` descarta cualquier cambio hecho a mano en los archivos del repositorio, pero no toca los archivos sin seguimiento, como un `.env` incluido en `.gitignore`. Ejecuta el script una vez a mano para comprobarlo: ```bash sudo -u deploy /usr/local/bin/deploy-myapp.sh ``` ```output Already at a1b2c3d4, nothing to deploy ``` ## Paso 4: Instalar y configurar webhook La herramienta `webhook` está en los repositorios de Ubuntu: ```bash sudo apt update sudo apt install webhook ``` Genera un secreto aleatorio. GitHub lo usará para firmar cada petición y `webhook` para comprobar la firma: ```bash openssl rand -hex 32 ``` Guarda el valor: lo necesitarás aquí y en GitHub. Crea el archivo de definición de los hooks: ```bash sudo mkdir -p /etc/webhook sudo nano /etc/webhook/hooks.json ``` Añade este contenido, sustituyendo `your_webhook_secret` por el valor generado: ```json [ { "id": "deploy-myapp", "execute-command": "/usr/local/bin/deploy-myapp.sh", "command-working-directory": "/srv/myapp", "response-message": "Deployment started", "trigger-rule-mismatch-http-response-code": 403, "trigger-rule": { "and": [ { "match": { "type": "payload-hmac-sha256", "secret": "your_webhook_secret", "parameter": { "source": "header", "name": "X-Hub-Signature-256" } } }, { "match": { "type": "value", "value": "refs/heads/main", "parameter": { "source": "payload", "name": "ref" } } } ] } } ] ``` El hook `deploy-myapp` solo ejecuta el script si se cumplen las dos reglas: la firma HMAC SHA-256 de la cabecera `X-Hub-Signature-256` es válida para el secreto, y el campo `ref` del evento es la rama `main`. Cualquier otra petición recibe un `403`. `webhook` responde de inmediato y ejecuta el script en segundo plano, así que GitHub no espera a que termine el despliegue. El archivo contiene el secreto, así que solo `root` y el grupo `deploy` deben poder leerlo: ```bash sudo chown root:deploy /etc/webhook/hooks.json sudo chmod 640 /etc/webhook/hooks.json ``` ## Paso 5: Ejecutar webhook como servicio Crea una unidad systemd propia para que `webhook` se ejecute como `deploy` y escuche solo en `127.0.0.1`, ya que las peticiones de Internet le llegarán a través de Nginx: ```bash sudo nano /etc/systemd/system/deploy-webhook.service ``` Añade este contenido: ```ini [Unit] Description=Webhook listener for myapp deployments After=network-online.target Wants=network-online.target [Service] User=deploy Group=deploy ExecStart=/usr/bin/webhook -hooks /etc/webhook/hooks.json -ip 127.0.0.1 -port 9000 -verbose Restart=on-failure [Install] WantedBy=multi-user.target ``` Habilita e inicia el servicio: ```bash sudo systemctl daemon-reload sudo systemctl enable --now deploy-webhook.service ``` Comprueba en el registro que ha cargado el hook `deploy-myapp` y que escucha en `127.0.0.1:9000`: ```bash sudo journalctl -u deploy-webhook.service -n 20 --no-pager ``` Prueba localmente que una petición sin firma se rechaza: ```bash curl -i -X POST http://127.0.0.1:9000/hooks/deploy-myapp \ -H "Content-Type: application/json" -d '{"ref":"refs/heads/main"}' ``` ```output HTTP/1.1 403 Forbidden ... Hook rules were not satisfied. ``` ## Paso 6: Publicar el webhook con Nginx y HTTPS GitHub necesita alcanzar el webhook por HTTPS. Crea un bloque de servidor en Nginx que reenvíe solo la ruta `/hooks/`: ```bash sudo nano /etc/nginx/sites-available/deploy-webhook ``` Añade esta configuración: ```nginx server { listen 80; listen [::]:80; server_name deploy.your_domain; location /hooks/ { proxy_pass http://127.0.0.1:9000; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } location / { return 404; } } ``` Activa el sitio, valida la configuración y recarga Nginx: ```bash sudo ln -s /etc/nginx/sites-available/deploy-webhook /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx ``` Instala Certbot y obtén el certificado. Certbot añadirá HTTPS y la redirección desde HTTP al bloque de servidor: ```bash sudo apt install certbot python3-certbot-nginx sudo certbot --nginx -d deploy.your_domain ``` Prueba una petición firmada correctamente desde cualquier equipo. `openssl` calcula la misma firma que enviaría GitHub: ```bash payload='{"ref":"refs/heads/main"}' signature=$(printf '%s' "$payload" | openssl dgst -sha256 -hmac 'your_webhook_secret' | awk '{print $2}') curl -i -X POST https://deploy.your_domain/hooks/deploy-myapp \ -H "Content-Type: application/json" \ -H "X-Hub-Signature-256: sha256=$signature" \ -d "$payload" ``` ```output HTTP/2 200 ... Deployment started ``` En el servidor, el registro de `deploy-webhook` mostrará la ejecución del script y su salida. ## Paso 7: Configurar el webhook en GitHub En el repositorio de GitHub, ve a **Settings > Webhooks > Add webhook** y rellena el formulario: | Campo | Valor | |---|---| | Payload URL | `https://deploy.your_domain/hooks/deploy-myapp` | | Content type | `application/json` | | Secret | El valor de `your_webhook_secret` | | Which events | **Just the push event** | Al guardar, GitHub envía un evento `ping`. Ese evento no tiene el campo `ref`, así que `webhook` responde `403` y GitHub lo marca como fallido: es el comportamiento esperado. Lo importante es la siguiente entrega. Haz un cambio en la aplicación y súbelo a `main`: ```bash git commit -am "Test automatic deployment" git push origin main ``` En **Settings > Webhooks**, la entrega del evento `push` debe aparecer con respuesta `200`. En el servidor, sigue el despliegue en tiempo real: ```bash sudo journalctl -u deploy-webhook.service -f ``` Verás que el hook `deploy-myapp` se activa, que se ejecuta `/usr/local/bin/deploy-myapp.sh` y, al terminar, la salida del script con las líneas `Deploying ...` y `Deployed ...`. Confirma que el servidor está en el nuevo commit: ```bash sudo -u deploy git -C /srv/myapp log --oneline -1 ``` ### Usar GitLab en lugar de GitHub GitLab no firma el cuerpo de la petición: envía el secreto tal cual en la cabecera `X-Gitlab-Token`. Si tu repositorio está en GitLab, sustituye la primera regla de `hooks.json` por esta y configura el webhook en **Settings > Webhooks** con el mismo secreto en el campo **Secret token**: ```json { "match": { "type": "value", "value": "your_webhook_secret", "parameter": { "source": "header", "name": "X-Gitlab-Token" } } } ``` Reinicia el servicio después de cambiar el archivo con `sudo systemctl restart deploy-webhook.service`. ## Solución de problemas **GitHub muestra `403` en los eventos `push`**: la firma no coincide. Comprueba que el secreto de GitHub y el de `hooks.json` son idénticos y que el **Content type** es `application/json`. Si el `push` es a otra rama, el `403` es correcto. **El script falla con `Permission denied (publickey)` en `git fetch`**: la deploy key no está añadida al repositorio o el servicio no se ejecuta como `deploy`. Pruébalo con `sudo -u deploy git -C /srv/myapp fetch`. **`sudo: a terminal is required to read the password`**: el comando del script no coincide exactamente con la regla de `/etc/sudoers.d/deploy-myapp`. Debe ser `/usr/bin/systemctl restart myapp.service`. **El despliegue se revierte siempre**: la aplicación no arranca con el código nuevo. Revisa el motivo con `sudo journalctl -u myapp.service -n 50 --no-pager`. ## Conclusión Tienes un despliegue continuo ligero en Ubuntu 24.04: GitHub avisa al servidor en cada `push` a `main`, `webhook` comprueba la firma y lanza un script que despliega un único commit a la vez y vuelve atrás si la aplicación no arranca. Como siguientes pasos puedes añadir al script la ejecución de migraciones de base de datos, enviar una notificación al terminar, o pasar a GitHub Actions cuando necesites ejecutar pruebas antes de desplegar.