SSD Nodes Learn Hosting plans →
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-08-07

Runner GitHub Actions self-hosted su VPS Ubuntu

Configura un runner GitHub Actions self-hosted su Ubuntu 24.04 con utente dedicato, checksum, config.sh e servizio systemd. Attenzione alle pull request dai fork.

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

Cosa fa un runner GitHub Actions self-hosted

Un runner GitHub Actions self-hosted è un programma che installi sul tuo VPS e che chiede a GitHub i job, eseguendoli sul tuo hardware. Lo registri su un repository, lo installi come servizio systemd e il runner torna operativo dopo ogni riavvio. GitHub pianifica il job. Il lavoro viene eseguito dal tuo server.

La CI (continuous integration) su un server di tua proprietà è utile per due motivi. I minuti di esecuzione delle build non vengono più conteggiati e un job può accedere a risorse disponibili soltanto sulla tua macchina, come una cache di build già popolata o una rete privata. Il costo è la sicurezza. Il runner esegue qualunque operazione definita dal file di workflow, con l'utente che gli hai assegnato. Per progettazione, quindi, un file di workflow consente l'esecuzione di codice da remoto. In un repository privato questo è accettabile, perché solo persone di cui ti fidi possono aggiungere un workflow. In un repository pubblico è un rischio concreto. La sezione sulle pull request provenienti dai fork spiega il meccanismo.

Tutto ciò che segue si riferisce a Ubuntu 24.04 con la versione 2.336.0 del runner, la release corrente a luglio 2026.

Cosa serve prima di iniziare

Parti da un VPS con un normale account amministrativo e sudo, nello stato raggiunto in i primi dieci minuti su un nuovo VPS. Non devi aprire alcuna porta in ingresso. Il runner apre una connessione HTTPS (hypertext transfer protocol secure) in uscita verso GitHub e la mantiene attiva mentre attende i lavori; GitHub non si connette mai al tuo server. Il firewall può restare chiuso verso Internet e i job continueranno comunque ad arrivare.

Ti servono anche diritti amministrativi sul repository, perché il token di registrazione viene mostrato nelle impostazioni del repository.

Crea un utente dedicato per il runner

Non eseguire mai il runner come root o come utente amministratore personale. Ogni job eredita i privilegi dell'utente del runner, quindi un workflow che richiama sudo riesce se l'utente del runner può usare sudo. Crea un solo utente senza privilegi, proprietario esclusivamente della propria home directory. Account utente con privilegi minimi su un VPS descrive il modello generale. Questo è il caso specifico.

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 blocca la password, quindi nessuno può accedere come gharunner usando una password. La modalità 700 sulla directory del runner è importante perché il runner vi memorizza le proprie credenziali in testo in chiaro e un checkout può contenere codice sorgente privato.

Verifica entrambe le proprietà prima di proseguire:

sudo passwd -S gharunner
sudo -l -U gharunner

passwd -S stampa una riga che inizia con gharunner L, dove L indica che la password è bloccata. sudo -l -U gharunner dovrebbe restituire is not allowed to run sudo. Se invece stampa un elenco di comandi consentiti, l'account appartiene a un gruppo sudo e l'isolamento appena configurato non è più attivo.

Scaricare il runner e verificare l'archivio tar

Da questo punto, opera con l'utente runner.

sudo -iu gharunner
cd ~/actions-runner
RUNNER_VERSION=2.336.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"

Esegui prima uname -m se non conosci con certezza l'architettura. x86_64 usa il file linux-x64 precedente. aarch64 usa actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz.

Ora verifica il file scaricato. Il valore SHA256 (Secure Hash Algorithm a 256 bit) riportato di seguito si riferisce all'archivio tar x64 della versione 2.336.0. GitHub mostra il valore della release corrente nella relativa pagina e nella schermata New self-hosted runner. Il valore cambia a ogni versione. Quando installi una versione diversa, copialo da una di queste pagine.

echo "04cf0be1aff4c3ec3554466c39124ca250e3effd8873bb7e8d68535aa9505d5d  actions-runner-linux-x64-2.336.0.tar.gz" | sha256sum -c

Un download corretto restituisce una riga:

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

Un file troncato o alterato restituisce l'errore e un avviso:

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

Non saltare questa verifica lasciando che sia tar a rilevare il problema. Un archivio scritto solo parzialmente genera gzip: stdin: unexpected end of file e tar: Unexpected EOF in archive. Questo indica che il file è danneggiato, ma non se il download è stato interrotto o se il file è stato sostituito.

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

Contenuto del tarball e ciò che non contiene

Dopo l'estrazione, la directory contiene config.sh, run.sh, env.sh, safe_sleep.sh, bin/ e externals/. bin/ contiene i binari del runner e bin/installdependencies.sh. externals/ contiene il runtime Node incluso, usato per eseguire le action JavaScript.

Non esiste ancora svc.sh. La documentazione di GitHub lo descrive come lo script «creato dopo l'aggiunta corretta del runner», perché viene generato da un modello che incorpora il repository e il nome del runner nel nome del servizio. Di conseguenza, sudo ./svc.sh install prima di ./config.sh non riesce e restituisce sudo: ./svc.sh: command not found. Registrare prima il runner, quindi installare il servizio.

Installare le dipendenze del runner

Il runner è un'applicazione .NET, quindi richiede alcune librerie condivise. Mantieni la shell dell'utente del runner e installa le librerie con sudo, perché lo script scrive nel database dei pacchetti di sistema.

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

Su Ubuntu 24.04 questo installa libkrb5-3, zlib1g, liblttng-ust1t64, libssl3t64 e libicu74. Per ogni libreria, lo script prova diversi nomi di versione e mantiene quello fornito dalla release in uso. Per questo lo stesso script funziona anche sulle versioni precedenti di Ubuntu e su Debian.

Se salti questo passaggio, ./config.sh si arresta prima di eseguire qualsiasi operazione:

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

L'assenza di libicu produce lo stesso suggerimento, ma con una riga iniziale diversa: Libicu's dependencies is missing for Dotnet Core 6.0. Entrambi i messaggi hanno la stessa origine: config.sh esegue ldd sulle librerie incluse prima di avviarsi. Se un collegamento non viene risolto, lo script si arresta invece di generare in seguito un crash difficile da diagnosticare.

Registrare il runner nel repository

Recupera un token dal repository. Apri Settings, quindi Actions, Runners e New self-hosted runner. La pagina mostra un token di registrazione che inizia con A. Scade un'ora dopo la creazione, quindi generalo quando sei pronto a incollarlo.

Esegui la registrazione come utente del runner. config.sh non può essere eseguito 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

Funzione dei flag. --name indica il nome con cui il runner viene visualizzato nel repository, quindi scegline uno che sia ancora riconoscibile tra sei mesi. --labels aggiunge le etichette personalizzate; il runner include già self-hosted, Linux e X64 senza ulteriori impostazioni. --work specifica la directory in cui vengono collocati i checkout, all'interno della directory del runner. --unattended risponde ai prompt interattivi usando i valori predefiniti, come richiesto quando il comando viene eseguito in uno script. --replace rileva una registrazione esistente con lo stesso nome invece di terminare con un errore, come richiesto quando ricrei il server.

Un'esecuzione completata correttamente termina con queste righe:

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

La registrazione viene ora salvata nella directory del runner come .runner, .credentials e .credentials_rsaparams. Gli ultimi due identificano questo runner presso GitHub, quindi chiunque possa leggerli può impersonarlo. Per questo la directory ha modalità 700 e l'utente non dispone di sudo.

Installare il runner come servizio systemd

./run.sh in un terminale è sufficiente per un test, ma il processo termina insieme alla sessione SSH. Installare il servizio per avviare il runner all'avvio del sistema. servizi e timer systemd su un VPS descrive in dettaglio i file delle unità. In questo caso svc.sh ne crea uno automaticamente.

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

svc.sh richiede root perché scrive un'unità in /etc/systemd/system e la abilita. L'argomento dopo install specifica l'utente con cui viene eseguito il servizio. Passare esplicitamente gharunner. Senza questo argomento, lo script usa $SUDO_USER, cioè il proprio account amministrativo; di conseguenza, ogni job viene eseguito da un utente che può usare sudo.

L'unità usa un nome derivato dal repository e dal runner, nel formato actions.runner.YOUR-USER-YOUR-REPO.vps-runner-1.service. Non è necessario digitarlo manualmente:

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

Un runner operativo registra √ Connected to GitHub, seguito da una riga che termina con Listening for Jobs; la pagina Runners del repository lo mostra quindi come Idle. Un runner indicato come Offline non è in esecuzione oppure non riesce a raggiungere GitHub sulla porta 443.

Inviare un job al runner

runs-on seleziona un runner in base all'etichetta. Richiedi self-hosted insieme alla tua etichetta, così il job non può essere eseguito su un runner diverso da quello previsto.

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

Se il job rimane in attesa in Waiting for a runner to pick up this job, le etichette non corrispondono. Ogni etichetta in runs-on deve esistere nel runner. Anche una sola parola aggiuntiva lascia il job in coda senza segnalare errori. Confronta l'elenco con le etichette visualizzate accanto al runner nelle impostazioni del repository.

Perché i runner self-hosted e i repository pubblici non sono compatibili

Questa è la parte che spesso viene ignorata. Le indicazioni di GitHub sono esplicite: i runner self-hosted «dovrebbero quasi mai essere usati per repository pubblici» e «non offrono garanzie sull'esecuzione in macchine virtuali effimere e pulite; inoltre, un codice non attendibile eseguito in un workflow può comprometterli in modo persistente».

Il meccanismo è semplice. Una pull request proveniente da un fork include una propria copia del file di workflow. Se il repository pubblico esegue i workflow delle pull request sul tuo runner, chiunque possa creare un fork del repository può proporre un workflow che esegue i propri comandi sul tuo VPS. Non serve l'accesso in scrittura, perché ciò che viene proposto è proprio ciò che viene eseguito.

Le impostazioni di approvazione riducono il rischio, ma non lo eliminano. Per impostazione predefinita, un repository pubblico richiede a un maintainer di approvare il workflow del fork appartenente a un nuovo contributor. Dopo la prima approvazione, le pull request successive di quella persona vengono eseguite senza una nuova richiesta. Il controllo consiste quindi nella lettura manuale di un diff, ogni volta, e un payload nascosto a tre livelli di profondità in uno script di build può essere facilmente ignorato.

Una pull request proveniente da un fork non riceve i tuoi secret e il suo GITHUB_TOKEN è di sola lettura. Questo limita i danni all'interno di GitHub, ma non protegge il server. L'attaccante dispone di una shell come gharunner, quindi può leggere ogni file accessibile a quell'utente, raggiungere qualsiasi risorsa accessibile al VPS sulla rete privata e lasciare una persistenza in ~/.bashrc o in una unità systemd dell'utente, eseguita durante il job successivo.

La registrazione con --ephemeral fa sì che il runner accetti un job e poi annulli la registrazione, impedendo a un job di leggere il workspace del job successivo. È utile soltanto se la macchina o il container vengono ricreati per ogni job, perché una backdoor scritta nella home directory dell'utente del runner sopravvive a una nuova registrazione.

Le regole seguenti sono brevi. Usa i runner self-hosted per i repository privati. Se devi collegarne uno a un repository pubblico, non eseguire su quel runner le pull request provenienti dai fork, non ospitare altro su quel server e considera la macchina usa e getta.

Docker, i job e il gruppo che equivale a root

I job dei container, i container dei servizi e qualsiasi passaggio del workflow che richiami docker build richiedono un daemon Docker sull'host del runner. Installa Docker nel modo consueto, descritto in Docker e Docker Compose su un VPS, quindi aggiungi l'utente del runner al gruppo docker.

Prima di procedere, valuta la conseguenza. L'appartenenza al gruppo docker equivale ai privilegi di root, perché un container può montare / come bind mount ed eseguire processi come root al proprio interno. Di conseguenza, un workflow che può accedere al socket Docker può leggere e modificare qualsiasi file sul VPS, incluso /etc/shadow. In un repository privato con collaboratori affidabili, questo compromesso può essere accettabile. In qualsiasi altro contesto, annulla il vantaggio di usare un utente senza privilegi. Docker rootless mantiene le build dei container all'interno dei privilegi dell'utente del runner, ma utilizza un driver di storage più lento e non consente container privilegiati.

Aggiornamenti e rimozione corretta del runner

Per impostazione predefinita, un self-hosted runner si aggiorna automaticamente. Rileva una nuova release, sostituisce i propri file e riavvia il servizio; normalmente non è quindi necessario intervenire. ./config.sh --disableupdate disabilita l'aggiornamento automatico quando è necessario mantenere una versione fissa. In questo caso, gli aggiornamenti devono essere eseguiti manualmente: la documentazione di GitHub specifica che un runner configurato con --disableupdate deve essere aggiornato a mano.

Un aggiornamento manuale conserva la registrazione, perché .runner e .credentials non sono presenti nell'archivio tar. Arresta il servizio, scarica il nuovo archivio tar e verificane il checksum come indicato in gharunner, quindi estrailo nella stessa directory con tar xzf e riavvia il servizio:

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

Per rimuovere il runner, disinstalla prima il servizio, quindi rimuovi la registrazione. Il token di rimozione si trova nella stessa pagina Runners, sotto il pulsante Remove del runner interessato.

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

Se elimini la directory senza rimuovere la registrazione, il runner resta elencato come Offline nel repository, perché GitHub rileva la rimozione solo quando il runner lo comunica oppure quando un amministratore elimina manualmente la voce.

Modalità di errore, con le stringhe visualizzate

Must not run with sudo. config.sh stampa questo messaggio e termina se viene eseguito come root. Il controllo è intenzionale, perché i file di proprietà di root in _work compromettono tutti i job successivi eseguiti dall'utente del servizio. Esegui ./config.sh come gharunner. La variabile RUNNER_ALLOW_RUNASROOT disattiva il controllo, ma il problema si presenterà semplicemente più avanti.

sudo: ./svc.sh: command not found. Ti trovi nella directory corretta. svc.sh non esiste ancora perché config.sh non ha completato alcuna registrazione. Registra il runner, quindi installa il servizio.

Http response code: NotFound from 'POST https://api.github.com/actions/runner-registration'. Il token non è un token di registrazione valido. Potrebbe essere scaduto, perché la sua validità è di un'ora, oppure potresti aver incollato un personal access token al posto del token di registrazione disponibile nella pagina Runners. Genera un nuovo token e incollalo nuovamente.

Dependencies is missing for Dotnet Core 6.0. Esegui sudo ./bin/installdependencies.sh dalla directory del runner come root, quindi registra nuovamente il runner.

Runner offline dopo un riavvio. Esegui systemctl is-enabled 'actions.runner.*'. Se non viene visualizzato nulla, ./svc.sh install non è mai stato eseguito; il runner è quindi esistito soltanto nella sessione del terminale. Se l'unità è abilitata e il runner è ancora offline, leggi journalctl -u 'actions.runner.*' e verifica le connessioni HTTPS in uscita.

Il disco si riempie. I checkout, le cache delle build e le immagini Docker si accumulano in _work e nella home dell'utente del runner; nessun processo li elimina automaticamente. Monitora du -sh /home/gharunner/actions-runner/_work e configura una pulizia pianificata prima che sia il disco a interrompere le attività.

FAQ

Perché sudo ./svc.sh install restituisce «command not found»?

Perché svc.sh non si trova nell'archivio tarball del runner. Viene generato nella directory del runner quando ./config.sh completa la registrazione, usando il repository e il nome del runner per creare il nome del servizio. Esegui prima ./config.sh con l'utente del runner. Successivamente sudo ./svc.sh install gharunner trova lo script e scrive un'unità denominata actions.runner.OWNER-REPO.RUNNER-NAME.service in /etc/systemd/system.

Devo aprire una porta del firewall per un runner self-hosted?

No. Il runner apre una connessione HTTPS in uscita verso GitHub e la mantiene attiva mentre attende i job, quindi GitHub non avvia mai una connessione verso il tuo VPS. Consenti il traffico in uscita sulla porta 443 e lascia chiuse le regole in ingresso. Se il runner mostra Offline mentre il relativo servizio è in esecuzione, controlla il filtraggio in uscita e il DNS, non le regole in ingresso.

Posso usare un runner self-hosted in un repository pubblico?

Sì, ma GitHub lo sconsiglia. Una pull request proveniente da un fork contiene il proprio file di workflow, quindi chiunque possa creare un fork del tuo repository può proporre comandi da eseguire sulla tua macchina. La richiesta di approvazione riguarda solo la prima esecuzione di un collaboratore. Se colleghi un runner a un repository pubblico, disabilita su quel runner i workflow delle pull request provenienti da fork, non usare il server per altri scopi e ricrea la macchina secondo una pianificazione prestabilita.

Perché la registrazione non riesce con Http response code: NotFound?

La chiamata di registrazione restituisce NotFound quando la credenziale è errata, non solo quando l'URL è errato, quindi il messaggio può risultare fuorviante. I token di registrazione scadono un'ora dopo essere stati visualizzati e un personal access token non è accettato per questa chiamata. Apri di nuovo Settings, Actions, Runners, New self-hosted runner, copia il nuovo token e verifica che il valore --url punti a un repository per il quale disponi dei privilegi di amministratore.

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