SSD Nodes Learn Hosting plans →
Guías Matt ConnorPor Matt Connor · Actualizado 2026-10-22

Instalar un runner autohospedado de GitHub Actions

Configura un runner de GitHub Actions en Ubuntu 24.04 con usuario dedicado, checksum, config.sh y systemd, y revisa el riesgo de pull requests desde forks.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, July 30, 2026.

Qué hace un runner autoalojado de GitHub Actions

Un runner autoalojado de GitHub Actions es un programa que instala en su propio VPS. Solicita trabajos a GitHub y los ejecuta en su hardware. Se registra para un repositorio, se instala como un servicio de systemd y vuelve a iniciarse después de cada reinicio. GitHub programa el trabajo. El servidor lo ejecuta.

La integración continua (CI) en un equipo propio resulta útil por dos motivos. Los minutos de compilación dejan de contabilizarse y un trabajo puede acceder a recursos que sólo están disponibles en su máquina, como una caché de compilación preparada o una red privada. El coste es la seguridad. El runner ejecuta todo lo que indique el archivo de workflow, con el usuario que se le haya asignado. Por diseño, un archivo de workflow permite la ejecución remota de código. En un repositorio privado, esto no supone un problema si sólo las personas de confianza pueden añadir workflows. En un repositorio público, representa un riesgo real. La sección sobre las pull requests desde forks explica el mecanismo.

Todo lo que sigue usa Ubuntu 24.04 y la versión 2.338.0 del runner, la versión actual a fecha de 12 October 2026.

Qué necesitas antes de empezar

Empieza con un VPS que tenga una cuenta de administrador normal y sudo, en el estado que alcanzas en los primeros diez minutos en un VPS nuevo. No necesitas abrir ningún puerto de entrada. El runner abre una conexión HTTPS (protocolo seguro de transferencia de hipertexto) saliente a GitHub y la mantiene abierta mientras espera trabajo. Por tanto, GitHub nunca se conecta a tu servidor. El firewall puede permanecer cerrado al exterior y los trabajos seguirán llegando.

También necesitas permisos de administrador en el repositorio, porque el token de registro se muestra en la configuración del repositorio.

Crear un usuario dedicado para el runner

Nunca ejecutes el runner como root ni como tu propio usuario administrador. Cada trabajo hereda los permisos del usuario del runner, por lo que un flujo de trabajo que llama a sudo funciona si el usuario del runner puede usar sudo. Crea un usuario sin privilegios que no sea propietario de nada excepto de su propio directorio personal. Cuentas de usuario con privilegios mínimos en un VPS explica el patrón general. Este es el procedimiento específico.

sudo useradd -m -s /bin/bash gharunner
sudo passwd -l gharunner
sudo chmod 750 /home/gharunner
sudo install -d -m 700 -o gharunner -g gharunner /home/gharunner/actions-runner

passwd -l bloquea la contraseña, por lo que nadie puede iniciar sesión como gharunner con ella. El modo 700 en el directorio del runner es importante porque el runner almacena allí sus credenciales en texto sin cifrar, y un checkout puede contener código fuente privado.

Comprueba ambas propiedades antes de continuar:

sudo passwd -S gharunner
sudo -l -U gharunner

passwd -S muestra una línea que empieza por gharunner L, donde L significa que la contraseña está bloqueada. sudo -l -U gharunner debe responder con is not allowed to run sudo. Si muestra una lista de comandos permitidos, la cuenta pertenece a un grupo de sudo y el aislamiento que acabas de crear se ha perdido.

Descargue el runner y compruebe el archivo tar

A partir de aquí, trabaje como el usuario runner.

sudo -iu gharunner
cd ~/actions-runner
RUNNER_VERSION=2.338.0
curl -fL -o actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz \
  "https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz"

Ejecute uname -m primero si no está seguro de la arquitectura. x86_64 usa el archivo linux-x64 anterior. aarch64 usa actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz.

Ahora compruebe lo que descargó. El valor SHA256 (algoritmo de hash seguro de 256 bits) siguiente corresponde al archivo tar x64 de la versión 2.338.0. GitHub muestra el valor de la versión actual en la página de la versión y en la pantalla New self-hosted runner. El valor cambia con cada versión, así que cópielo de allí cuando instale otra.

echo "af4b794c1bc41d73d40535e3fe092a39f9679cd8d965954c2aca25a05ca41d32  actions-runner-linux-x64-2.338.0.tar.gz" | sha256sum -c

Una descarga correcta muestra una línea:

actions-runner-linux-x64-2.338.0.tar.gz: OK

Un archivo truncado o modificado muestra el fallo y una advertencia:

actions-runner-linux-x64-2.338.0.tar.gz: FAILED
sha256sum: WARNING: 1 computed checksum did NOT match

No omita esta comprobación y deje que tar detecte el problema. Un archivo incompleto falla con gzip: stdin: unexpected end of file y tar: Unexpected EOF in archive. Esto indica que el archivo está dañado, pero no si quedó incompleto o fue reemplazado.

tar xzf ./actions-runner-linux-x64-2.338.0.tar.gz
ls

Qué contiene el tarball y qué no contiene

Después de la extracción, el directorio contiene config.sh, run.sh, env.sh, safe_sleep.sh, bin/ y externals/. bin/ contiene los binarios del ejecutor y bin/installdependencies.sh. externals/ contiene el entorno de ejecución de Node incluido que ejecutan las acciones de JavaScript.

Todavía no existe svc.sh. La documentación de GitHub lo describe como el script «que se crea después de agregar correctamente el ejecutor», porque se escribe a partir de una plantilla con el repositorio y el nombre del ejecutor incorporados en el nombre del servicio. Por eso, sudo ./svc.sh install antes de ./config.sh falla con sudo: ./svc.sh: command not found. Registre primero el ejecutor y, después, instale el servicio.

Instale las dependencias del runner

El runner es una aplicación .NET, por lo que necesita algunas bibliotecas compartidas. Salga del shell del usuario del runner e instálelas con sudo, porque el script escribe en la base de datos de paquetes del sistema.

exit
cd /home/gharunner/actions-runner
sudo ./bin/installdependencies.sh

En Ubuntu 24.04 instala libkrb5-3, zlib1g, liblttng-ust1t64, libssl3t64 y libicu74. El script prueba varios nombres de versión para cada biblioteca y conserva el que incluye su release, por lo que el mismo script funciona en versiones anteriores de Ubuntu y en Debian.

Si omite este paso y falta una de esas bibliotecas, ./config.sh se detiene antes de hacer nada:

Dependencies is missing for Dotnet Core 6.0
Execute sudo ./bin/installdependencies.sh to install any missing Dotnet Core 6.0 dependencies.

La ausencia de libicu muestra el mismo aviso con una primera línea diferente, Libicu's dependencies is missing for Dotnet Core 6.0. Ambos mensajes proceden del mismo lugar: config.sh comprueba las bibliotecas incluidas con ldd y libicu con ldconfig antes de iniciarse, por lo que un enlace no resuelto detiene el script en lugar de provocar un error confuso más adelante.

Registre el runner en su repositorio

Obtenga un token del repositorio. Abra Settings, después Actions, después Runners y, por último, New self-hosted runner. La página muestra un token de registro. Caduca una hora después de su creación, así que genérelo cuando esté listo para pegarlo.

Regístrelo como el usuario del runner. config.sh no se ejecuta con sudo.

sudo -iu gharunner
cd ~/actions-runner
./config.sh --url https://github.com/YOUR-USER/YOUR-REPO \
  --token PASTE_REGISTRATION_TOKEN_HERE \
  --name vps-runner-1 \
  --labels vps \
  --work _work \
  --unattended \
  --replace

Esto es lo que hacen esas opciones. --name define cómo aparece el runner en el repositorio, así que elija un nombre que pueda reconocer dentro de seis meses. --labels añade sus propias etiquetas; el runner ya incluye self-hosted, Linux y X64 sin que tenga que indicarlas. --work define el directorio donde se guardan los checkouts, dentro del directorio del runner. --unattended responde a las preguntas interactivas con sus valores predeterminados, que es lo adecuado cuando el comando se ejecuta desde un script. --replace reutiliza un registro existente con el mismo nombre en lugar de fallar, que es lo adecuado al reconstruir el servidor.

Una ejecución correcta termina con estas líneas:

√ Runner successfully added
√ Runner connection is good
√ Settings Saved.

El registro queda almacenado en el directorio del runner como .runner, .credentials y .credentials_rsaparams. Los dos últimos identifican este runner ante GitHub, por lo que cualquiera que pueda leerlos puede suplantarlo. Por eso el directorio tiene el modo 700 y el usuario no tiene acceso a sudo.

Instalar el runner como servicio de systemd

./run.sh en una terminal sirve para una prueba, pero se detiene al cerrar la sesión SSH. Instala el servicio para que el runner se inicie durante el arranque. servicios y temporizadores de systemd en un VPS explica los propios archivos de unidad. Aquí, svc.sh crea uno automáticamente.

exit
cd /home/gharunner/actions-runner
sudo ./svc.sh install gharunner
sudo ./svc.sh start
sudo ./svc.sh status

svc.sh requiere root porque escribe una unidad en /etc/systemd/system y la habilita. El argumento que sigue a install es el usuario con el que se ejecuta el servicio. Pasa gharunner explícitamente. Sin argumentos, el script usa $SUDO_USER, que es tu cuenta de administrador. En ese caso, todos los trabajos se ejecutan con un usuario que puede usar sudo.

La unidad recibe un nombre basado en el repositorio y el runner, con el formato actions.runner.YOUR-USER-YOUR-REPO.vps-runner-1.service. No tienes que escribirlo manualmente:

systemctl list-units 'actions.runner.*'
sudo journalctl -u 'actions.runner.*' -n 20 --no-pager

Un runner operativo registra √ Connected to GitHub y después una línea que termina en Listening for Jobs. La página Runners del repositorio lo muestra como Idle. Un runner que aparece como Offline no se está ejecutando o no puede conectarse a GitHub por el puerto 443.

Enviar un trabajo al ejecutor

runs-on selecciona un ejecutor por etiqueta. Solicita self-hosted y añade tu propia etiqueta para impedir que el trabajo se ejecute en un ejecutor no previsto.

name: build
on:
  push:
    branches: [main]
jobs:
  build:
    runs-on: [self-hosted, linux, vps]
    steps:
      - uses: actions/checkout@v5
      - run: uname -a

Si el trabajo permanece en Waiting for a runner to pick up this job, las etiquetas no coinciden. Todas las etiquetas de runs-on deben existir en el ejecutor. Una palabra adicional deja el trabajo en cola sin mostrar ningún error. Compara la lista con las etiquetas que aparecen junto al ejecutor en la configuración del repositorio.

Por qué los runners autohospedados y los repositorios públicos no deben combinarse

Esta es la parte que muchas personas omiten. La documentación de GitHub es clara: los runners autohospedados «casi nunca deben usarse para repositorios públicos» y «no ofrecen garantías de ejecutarse en máquinas virtuales efímeras y limpias; además, el código no confiable de un workflow puede comprometerlos de forma persistente».

El mecanismo es sencillo. Una pull request de un fork incluye su propia copia del archivo de workflow. Si el repositorio público ejecuta workflows de pull request en tu runner, cualquiera que pueda crear un fork del repositorio puede proponer un workflow que ejecute sus comandos en tu VPS. No necesita acceso de escritura, porque lo que propone es precisamente lo que se ejecuta.

La configuración de aprobaciones reduce el riesgo, pero no lo elimina. La política predeterminada de un repositorio público pide a un mantenedor que apruebe el workflow del fork de un colaborador que participa por primera vez. GitHub considera que alguien participa por primera vez sólo hasta que uno de sus commits o pull requests se fusiona en el repositorio. Después, sus siguientes pull requests se ejecutan sin solicitar aprobación, y el propio GitHub advierte que una corrección trivial fusionada es suficiente para llegar a ese punto. Por tanto, el control consiste en que una persona lea un diff, y es fácil pasar por alto una carga maliciosa oculta tres niveles más abajo en un script de compilación.

Una pull request de un fork no recibe tus secretos y su GITHUB_TOKEN es de sólo lectura. Esto limita el daño dentro de GitHub. No protege tu servidor. El atacante obtiene un shell como gharunner, por lo que puede leer todos los archivos que ese usuario pueda leer, acceder a cualquier recurso al que el VPS pueda llegar en su red privada y dejar algo en ~/.bashrc o en una unidad de systemd del usuario que se ejecute durante el siguiente job.

Registrar el runner con --ephemeral hace que acepte un job y después se dé de baja, de modo que un job no pueda leer el workspace del siguiente. Esto sólo ayuda si algún mecanismo reconstruye la máquina o el contenedor para cada job, porque una puerta trasera escrita en el directorio personal del usuario del runner sobrevive a un registro nuevo.

Las reglas siguientes son breves. Usa runners autohospedados para repositorios privados. Si debes asociar uno a un repositorio público, no ejecutes en él pull requests de forks, no mantengas nada más en ese servidor y trata la máquina como desechable.

Trabajos de Docker y el grupo que equivale a root

Los trabajos de contenedores, los contenedores de servicio y cualquier paso del flujo de trabajo que llame a docker build necesitan un daemon de Docker en el host del runner. Instala Docker de la forma habitual, descrita en Docker y Docker Compose en un VPS, y añade el usuario del runner al grupo docker.

Comprende el compromiso antes de hacerlo. Pertenecer al grupo docker equivale a tener acceso de root, porque un contenedor puede montar / mediante un bind mount y ejecutarse como root dentro de él. Por tanto, un flujo de trabajo que pueda comunicarse con el socket de Docker puede leer y escribir cualquier archivo del VPS, incluido /etc/shadow. En un repositorio privado con colaboradores de confianza, este riesgo puede ser aceptable. En cualquier otro caso, elimina la razón de usar un usuario sin privilegios. Rootless Docker mantiene las compilaciones de contenedores dentro de los permisos del propio usuario del runner, pero utiliza un controlador de almacenamiento más lento y no permite contenedores con privilegios.

Actualizaciones y eliminación limpia del runner

Un self-hosted runner se actualiza de forma predeterminada. Detecta una nueva versión, reemplaza sus propios archivos y reinicia el servicio, por lo que normalmente no es necesario hacer nada. Para desactivar la actualización automática, pase --disableupdate a ./config.sh al registrar el runner. Si el runner ya está registrado, debe registrarlo de nuevo con el flag. Esto no permite mantenerlo en una versión de forma permanente. La documentación de GitHub indica explícitamente que un runner configurado con --disableupdate también debe actualizarse manualmente en un plazo de 30 días desde una nueva versión. De lo contrario, GitHub deja de poner trabajos en cola para ese runner.

Una actualización manual conserva el registro, porque .runner y .credentials no están en el tarball. Detenga el servicio, descargue el nuevo tarball y compruebe su suma de comprobación como gharunner, extráigalo sobre el mismo directorio con tar xzf y vuelva a iniciar el servicio:

cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh start

Para eliminar el runner, desinstale primero el servicio y, después, elimine su registro. El token de eliminación se obtiene en la misma página Runners, mediante el botón Remove del propio runner.

cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh uninstall
sudo -iu gharunner
cd ~/actions-runner
./config.sh remove --token PASTE_REMOVAL_TOKEN_HERE

Eliminar el directorio sin eliminar el registro deja el runner listado como Offline en el repositorio. Permanece allí hasta que un administrador haga clic en Force remove this runner o hasta que GitHub lo elimine automáticamente después de 14 días sin conexión.

Modos de fallo y cadenas que verá

Must not run with sudo. config.sh muestra este mensaje y termina cuando se ejecuta como root. La comprobación es intencionada, porque los archivos propiedad de root en _work rompen todos los trabajos posteriores que se ejecutan con la cuenta de servicio. Ejecute ./config.sh como gharunner. La variable RUNNER_ALLOW_RUNASROOT omite la comprobación, pero usarla sólo retrasa el fallo.

sudo: ./svc.sh: command not found. Está en el directorio correcto. svc.sh todavía no existe porque config.sh no ha completado ningún registro. Registre el runner y, después, instale el servicio.

Http response code: NotFound from 'POST https://api.github.com/actions/runner-registration'. Normalmente, el token no es un token de registro válido. Puede haber caducado, ya que sólo dura una hora, o puede haberse pegado un personal access token en lugar del token de registro de la página Runners. Genere un token nuevo y péguelo de nuevo. Compruebe también que --url sea la URL completa del repositorio.

Dependencies is missing for Dotnet Core 6.0. Ejecute sudo ./bin/installdependencies.sh desde el directorio del runner como root y vuelva a registrarlo.

Runner sin conexión después de reiniciar. Ejecute systemctl is-enabled 'actions.runner.*'. Si no aparece nada, nunca se ejecutó ./svc.sh install, por lo que el runner sólo existió dentro de la sesión de terminal. Si la unidad está habilitada y el runner sigue sin conexión, lea journalctl -u 'actions.runner.*' y compruebe las conexiones HTTPS salientes.

El disco se llena. Los checkouts, las cachés de compilación y las imágenes de Docker se acumulan en _work y en el directorio principal del usuario del runner. Nada los limpia automáticamente. Supervise du -sh /home/gharunner/actions-runner/_work y añada una limpieza programada antes de que el disco se llene.

FAQ

¿Por qué sudo ./svc.sh install muestra «command not found»?

Porque svc.sh no está en el archivo tar del runner. Se genera en el directorio del runner cuando ./config.sh termina el registro. Usa el nombre del repositorio y del runner para construir el nombre del servicio. Ejecute primero ./config.sh como usuario del runner. Después, sudo ./svc.sh install gharunner encuentra el script y escribe una unidad llamada actions.runner.OWNER-REPO.RUNNER-NAME.service en /etc/systemd/system.

¿Necesito abrir un puerto del firewall para un runner autohospedado?

No. El runner abre una conexión HTTPS saliente a GitHub y la mantiene abierta mientras espera trabajos. Por tanto, GitHub nunca inicia una conexión con su VPS. Permita las conexiones salientes por 443 y mantenga cerradas las reglas de entrada. Si el runner muestra Offline mientras su servicio está activo, revise el filtrado saliente y DNS, no las reglas de entrada.

¿Puedo usar un runner autohospedado en un repositorio público?

Puede hacerlo, pero GitHub no lo recomienda. Una pull request de un fork incluye su propio archivo de workflow. Por tanto, cualquier persona que pueda crear un fork del repositorio puede proponer comandos que se ejecuten en su máquina. El aviso de aprobación predeterminado sólo se aplica a colaboradores que nunca hayan tenido un commit o una pull request fusionados en el repositorio. Si asocia un runner a un repositorio público, desactive en él los workflows de pull requests de forks, no mantenga nada más en ese servidor y vuelva a crear la máquina periódicamente.

¿Por qué falla el registro con Http response code: NotFound?

La llamada de registro responde NotFound cuando la credencial es incorrecta, no sólo cuando la URL es incorrecta. Esto hace que el mensaje resulte engañoso. Los tokens de registro caducan una hora después de mostrarse, y esta llamada no acepta un personal access token. Abra de nuevo Settings, Actions, Runners, New self-hosted runner, copie el token nuevo y confirme que el valor de --url apunta a un repositorio en el que tiene permisos de administrador.

¿Te resultó útil este artículo?

#github-actions#ci#self-hosted#runner#ubuntu-24-04