--- title: "Desplegar en un servidor Ubuntu 24.04 con GitHub Actions" description: "Crea un flujo de GitHub Actions que prueba tu aplicación y la despliega por SSH con rsync en un servidor Ubuntu 24.04, con usuario dedicado y sudo limitado." category: "CI/CD DevOps" language: es source: https://cubepath.com/es/docs/ci-cd-devops/github-actions-para-implementacion-en-servidor updated: 2026-09-25 --- # Desplegar en un servidor Ubuntu 24.04 con GitHub Actions GitHub Actions es la plataforma de integración y despliegue continuo integrada en GitHub: cada `push` puede lanzar un flujo de trabajo que compila, prueba y publica tu código. En este tutorial crearás un flujo que ejecuta las pruebas de una aplicación Node.js y, si pasan, la copia por SSH con `rsync` a un servidor Ubuntu 24.04 y reinicia su servicio systemd. El servidor tendrá un usuario de despliegue dedicado, sin contraseña y con permiso `sudo` solo para reiniciar ese servicio. ## Requisitos previos Para seguir esta guía necesitas: - Un servidor con Ubuntu 24.04 LTS accesible por SSH, por ejemplo un VPS de CubePath, y un usuario no root con privilegios `sudo`. - Node.js instalado en el servidor (la versión que use tu aplicación) y `git` en tu equipo local. - Un repositorio en GitHub con una aplicación Node.js que tenga un `package.json` con los scripts `start` y `test`, y su `package-lock.json`. - Permisos de administración en el repositorio para crear secretos y entornos. A lo largo de la guía, sustituye `your_server_ip` por la IP pública de tu servidor y `myapp` por el nombre de tu aplicación. ## Paso 1: Crear el usuario de despliegue en el servidor Desplegar con tu usuario personal o con `root` da al flujo de trabajo más permisos de los que necesita. Crea un usuario `deploy` sin contraseña, que solo podrá entrar con la clave SSH que añadirás después: ```bash sudo useradd --create-home --shell /bin/bash deploy ``` Crea el directorio de la aplicación y asígnalo a ese usuario: ```bash sudo mkdir -p /srv/myapp sudo chown deploy:deploy /srv/myapp ``` Asegúrate de que `rsync` está instalado, porque el flujo lo usará en ambos extremos de la conexión: ```bash sudo apt update sudo apt install rsync ``` Comprueba que el usuario existe: ```bash id deploy ``` ```output uid=1001(deploy) gid=1001(deploy) groups=1001(deploy) ``` ## Paso 2: Crear el servicio systemd de la aplicación El flujo reiniciará la aplicación después de copiar el código nuevo, así que la aplicación debe ejecutarse como servicio systemd. Localiza primero la ruta del binario de Node.js: ```bash which node ``` ```output /usr/bin/node ``` Crea la unidad del servicio: ```bash sudo nano /etc/systemd/system/myapp.service ``` Añade este contenido, ajustando `ExecStart` a la ruta que te devolvió `which node` y al comando de arranque 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 ``` El guion delante de `EnvironmentFile` indica que el archivo `.env` es opcional. Recarga systemd y habilita el servicio para que arranque con el sistema. Todavía no lo inicies: el directorio está vacío hasta el primer despliegue. ```bash sudo systemctl daemon-reload sudo systemctl enable myapp.service ``` ## Paso 3: Permitir al usuario deploy reiniciar solo ese servicio El usuario `deploy` necesita reiniciar `myapp.service`, pero no debe tener `sudo` completo. Crea una regla de sudoers limitada a ese único 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 la sintaxis del archivo: ```bash sudo visudo -cf /etc/sudoers.d/deploy-myapp ``` ```output /etc/sudoers.d/deploy-myapp: parsed OK ``` Comprueba qué puede ejecutar el usuario: ```bash sudo -l -U deploy ``` ```output User deploy may run the following commands on server: (root) NOPASSWD: /usr/bin/systemctl restart myapp.service ``` > Importante: el comando del flujo debe coincidir exactamente con la regla, incluido el sufijo `.service`. `sudo systemctl restart myapp` sin sufijo pediría contraseña y el despliegue fallaría. ## Paso 4: Generar la clave SSH de despliegue Genera en tu equipo local un par de claves exclusivo para GitHub Actions. No reutilices tu clave personal: si se filtra, solo tendrás que revocar esta. La opción `-N ""` crea la clave sin frase de paso, porque el flujo no puede escribirla de forma interactiva: ```bash ssh-keygen -t ed25519 -C "github-actions-myapp" -N "" -f ./myapp_deploy_key ``` Se crean dos archivos: `myapp_deploy_key` (privada) y `myapp_deploy_key.pub` (pública). Instala la clave pública en el usuario `deploy` del servidor. Conéctate al servidor con tu usuario habitual y ejecuta: ```bash sudo install -d -m 700 -o deploy -g deploy /home/deploy/.ssh sudo nano /home/deploy/.ssh/authorized_keys ``` Pega en el archivo el contenido de `myapp_deploy_key.pub` (una sola línea que empieza por `ssh-ed25519`) y ajusta los permisos: ```bash sudo chown deploy:deploy /home/deploy/.ssh/authorized_keys sudo chmod 600 /home/deploy/.ssh/authorized_keys ``` Desde tu equipo local, comprueba que la clave funciona: ```bash ssh -i ./myapp_deploy_key deploy@your_server_ip 'whoami' ``` ```output deploy ``` ## Paso 5: Obtener la huella del servidor El flujo debe verificar que se conecta a tu servidor y no a otro. En lugar de ejecutar `ssh-keyscan` dentro del flujo en cada despliegue (que aceptaría cualquier clave que responda), guarda la clave de host una vez como secreto. Desde tu equipo local: ```bash ssh-keyscan -t ed25519 your_server_ip ``` ```output your_server_ip ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... ``` Antes de confiar en esa línea, compárala con la huella real. En el servidor: ```bash ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub ``` Y en tu equipo local: ```bash ssh-keyscan -t ed25519 your_server_ip 2>/dev/null | ssh-keygen -lf - ``` Las dos huellas `SHA256:...` deben ser idénticas. Guarda la línea completa que devolvió `ssh-keyscan` para el siguiente paso. ## Paso 6: Guardar los secretos en GitHub GitHub cifra los secretos y los oculta en los registros de ejecución. Usarás un entorno llamado `production`, que agrupa los secretos del servidor y permite añadir reglas de protección, como exigir una aprobación antes de desplegar. En el repositorio, ve a **Settings > Environments > New environment**, llámalo `production` y, dentro de él, en **Environment secrets**, crea estos secretos: | Nombre | Valor | |---|---| | `SSH_HOST` | La IP o el nombre de tu servidor (`your_server_ip`) | | `SSH_USER` | `deploy` | | `SSH_PRIVATE_KEY` | El contenido completo de `myapp_deploy_key`, incluidas las líneas `BEGIN` y `END` | | `SSH_KNOWN_HOSTS` | La línea que devolvió `ssh-keyscan` en el paso anterior | Si tienes instalada la CLI de GitHub (`gh`), puedes crearlos desde el directorio del repositorio: ```bash gh secret set SSH_PRIVATE_KEY --env production < ./myapp_deploy_key gh secret set SSH_KNOWN_HOSTS --env production --body "$(ssh-keyscan -t ed25519 your_server_ip 2>/dev/null)" gh secret set SSH_HOST --env production --body "your_server_ip" gh secret set SSH_USER --env production --body "deploy" ``` Cuando los secretos estén guardados, borra la clave privada de tu equipo local: ```bash rm ./myapp_deploy_key ``` ## Paso 7: Crear el flujo de trabajo Los flujos de GitHub Actions son archivos YAML dentro de `.github/workflows/`. En la raíz de tu repositorio local, crea el directorio y el archivo: ```bash mkdir -p .github/workflows nano .github/workflows/deploy.yml ``` Añade este contenido: ```yaml name: Test and deploy on: push: branches: [main] workflow_dispatch: permissions: contents: read concurrency: group: deploy-production cancel-in-progress: false jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: node-version: 22 cache: npm - run: npm ci - run: npm test deploy: needs: test runs-on: ubuntu-latest environment: production steps: - uses: actions/checkout@v5 - name: Configure SSH env: SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} SSH_KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS }} run: | install -m 700 -d ~/.ssh printf '%s\n' "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519 printf '%s\n' "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts - name: Copy files env: SSH_TARGET: ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} run: | rsync -az --delete \ --exclude='.git/' \ --exclude='.github/' \ --exclude='node_modules/' \ --exclude='.env' \ ./ "$SSH_TARGET:/srv/myapp/" - name: Install dependencies and restart env: SSH_TARGET: ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} run: | ssh "$SSH_TARGET" \ 'cd /srv/myapp && npm ci --omit=dev && sudo systemctl restart myapp.service' ``` Qué hace cada parte: - `on`: el flujo se ejecuta con cada `push` a `main` y también de forma manual desde la pestaña **Actions** (`workflow_dispatch`). - `permissions`: el token automático del flujo solo puede leer el repositorio. - `concurrency`: si haces dos `push` seguidos, el segundo despliegue espera a que termine el primero en lugar de ejecutarse a la vez. - `test` y `deploy` son dos trabajos. `needs: test` hace que el despliegue solo se ejecute si las pruebas pasan. - `environment: production` da acceso a los secretos de ese entorno y aplica sus reglas de protección. - `rsync --delete` deja en el servidor una copia exacta del repositorio. Las exclusiones evitan subir `node_modules` y, sobre todo, que se borre el `.env` que solo existe en el servidor. - En el servidor, `npm ci --omit=dev` instala las dependencias de producción a partir de `package-lock.json` y después se reinicia el servicio con el comando exacto permitido en sudoers. > Nota: los secretos se pasan a los comandos mediante `env` en lugar de escribir `${{ secrets.X }}` dentro del script. Así su valor no se interpreta como código de shell si contiene caracteres especiales. ## Paso 8: Lanzar el primer despliegue Confirma el flujo y súbelo a `main`: ```bash git add .github/workflows/deploy.yml git commit -m "Add test and deploy workflow" git push origin main ``` En GitHub, abre la pestaña **Actions** del repositorio. Verás la ejecución **Test and deploy** con los trabajos `test` y `deploy`. Si tienes la CLI `gh`, puedes seguirla desde la terminal: ```bash gh run watch ``` Cuando ambos trabajos terminen en verde, comprueba el servicio en el servidor: ```bash sudo systemctl status myapp.service ``` ```output ● myapp.service - myapp Node.js application Loaded: loaded (/etc/systemd/system/myapp.service; enabled; preset: enabled) Active: active (running) since Thu 2026-09-24 10:15:02 UTC; 12s ago Main PID: 4182 (node) ``` Revisa también los registros de la aplicación: ```bash sudo journalctl -u myapp.service -n 20 --no-pager ``` A partir de ahora, cada `push` a `main` que pase las pruebas se desplegará automáticamente. ## Paso 9: Exigir aprobación antes de desplegar (opcional) Si quieres que alguien revise cada despliegue a producción, ve a **Settings > Environments > production** y activa **Required reviewers**, añadiendo las personas o equipos que pueden aprobar. El trabajo `deploy` quedará en espera con el estado **Waiting** hasta que uno de ellos lo apruebe desde la ejecución. En el mismo apartado, **Deployment branches and tags** permite limitar el entorno a la rama `main`, de modo que un flujo lanzado desde otra rama no pueda leer los secretos de producción. ## Solución de problemas **`Host key verification failed`**: el secreto `SSH_KNOWN_HOSTS` no coincide con la clave del servidor o `SSH_HOST` no es el mismo nombre o IP que usaste con `ssh-keyscan`. Genera de nuevo la línea con el valor exacto de `SSH_HOST`. **`Permission denied (publickey)`**: la clave pública no está en `/home/deploy/.ssh/authorized_keys`, los permisos son demasiado abiertos o el secreto `SSH_PRIVATE_KEY` está incompleto. Revisa en el servidor `sudo journalctl -u ssh -n 20` para ver el motivo del rechazo. **`sudo: a terminal is required to read the password`**: el comando de reinicio del flujo no coincide exactamente con la regla de `/etc/sudoers.d/deploy-myapp`. Comprueba la ruta `/usr/bin/systemctl` y el sufijo `.service`. **`npm: command not found` en el paso de reinicio**: la sesión SSH no interactiva no encuentra Node.js. Pasa si lo instalaste con nvm en el directorio de otro usuario; instálalo desde el repositorio de NodeSource o desde los paquetes de Ubuntu para que quede en `/usr/bin`. ## Conclusión Tienes un flujo de GitHub Actions que prueba cada cambio en `main` y lo despliega por SSH en tu servidor Ubuntu 24.04, con un usuario dedicado, la clave de host verificada y `sudo` limitado a reiniciar un único servicio. Como siguientes pasos puedes añadir un entorno `staging` con su propio servidor y secretos, publicar la aplicación detrás de Nginx con HTTPS, o sustituir la copia de archivos por imágenes de Docker publicadas en GitHub Container Registry.