SSD Nodes Learn 8GB di RAM — $66/anno
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-08-02

Runner GitHub Actions self-hosted su VPS: guida Ubuntu

Configura un runner self-hosted su Ubuntu 24.04 con utente dedicato, checksum, config.sh e servizio systemd. Valuta anche il rischio delle pull request da fork.

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

Cosa fa un runner self-hosted di GitHub Actions

Un runner self-hosted di GitHub Actions è un programma che installi sul tuo VPS e che richiede a GitHub i job, quindi li esegue sul tuo hardware. Lo registri per un repository, lo installi come servizio systemd e viene riavviato dopo ogni reboot. GitHub pianifica il job. Il tuo server esegue il lavoro.

La CI (integrazione continua) su un sistema che amministri direttamente è utile per due motivi. I minuti di compilazione non vengono più conteggiati e un job può accedere a risorse disponibili solo sulla tua macchina, come una build cache già popolata o una rete privata. Il costo è la sicurezza. Il runner esegue qualunque operazione definita nel file del 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 non è un problema, perché solo persone di cui ti fidi possono aggiungere un workflow. In un repository pubblico è un rischio concreto; la sezione sulle pull request provenienti da fork spiega il meccanismo.

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

Cosa ti serve prima di iniziare

Parti da un VPS con un normale account amministratore 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 aperta mentre attende le attività. GitHub non si connette mai al tuo server. Il firewall può rimanere chiuso verso l'esterno e le attività continueranno comunque ad arrivare.

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

Creare un utente dedicato per il runner

Non eseguire mai il runner come root o con il tuo utente amministratore. Ogni job eredita i diritti dell'utente del runner, quindi un workflow che richiama sudo ha esito positivo se l'utente del runner può usare sudo. Crea un utente senza privilegi che possieda soltanto la propria directory home. Account utente con privilegi minimi su un VPS descrive il modello generale. Di seguito trovi la configurazione specifica.

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 procedere:

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, operare come 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"

Eseguire prima uname -m se non si è certi dell'architettura. x86_64 usa il file linux-x64 indicato sopra. aarch64 usa actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz.

Verificare ora il file scaricato. Il valore SHA256 (algoritmo di hash sicuro a 256 bit) riportato di seguito è relativo all'archivio tar x64 2.336.0. GitHub mostra il valore della release corrente nella pagina della release e nella schermata New self-hosted runner. Il valore cambia a ogni versione, quindi copiarlo da una di queste schermate quando si installa una versione diversa.

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 non riesce con gzip: stdin: unexpected end of file e tar: Unexpected EOF in archive. Questi messaggi indicano che il file è danneggiato, ma non se è stato troncato o sostituito.

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

Cosa contiene il tarball e cosa 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 azioni JavaScript.

svc.sh non esiste ancora. La documentazione di GitHub lo descrive come lo script «creato dopo l'aggiunta corretta del runner», perché viene scritto 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. Prima registra il runner, quindi installa 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. Lo script prova diversi nomi di versione per ogni libreria e mantiene quello fornito dalla tua release. Per questo lo stesso script funziona sulle versioni precedenti di Ubuntu e su Debian.

Se salti questo passaggio, ./config.sh si interrompe 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 prima riga diversa: Libicu's dependencies is missing for Dotnet Core 6.0. Entrambi i messaggi provengono dallo stesso punto: config.sh esegue ldd sulle librerie incluse prima dell'avvio, quindi un collegamento non risolto interrompe lo script invece di causare in seguito un arresto anomalo difficile da diagnosticare.

Registrare il runner nel repository

Ottenere un token dal repository. Aprire 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 generarlo quando si è pronti a incollarlo.

Registrarsi 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 determina il nome con cui il runner viene visualizzato nel repository, quindi scegliere un nome ancora riconoscibile tra sei mesi. --labels aggiunge le proprie etichette; il runner include già self-hosted, Linux e X64 senza configurazione aggiuntiva. --work specifica la directory in cui vengono salvati i checkout, all'interno della directory del runner. --unattended risponde ai prompt interattivi usando i valori predefiniti, come richiesto quando il comando viene inserito in uno script. --replace rileva una registrazione esistente con lo stesso nome invece di generare un errore, come richiesto quando si ricrea il server.

Un'esecuzione completata correttamente termina con queste righe:

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

La registrazione si trova ora 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 la modalità 700 e l'utente non dispone di sudo.

Installare il runner come servizio systemd

./run.sh in un terminale va bene per un solo test, ma si arresta quando termina la sessione SSH. Installare il servizio in modo che il runner si avvii all'avvio del sistema. servizi e timer systemd su un VPS descrive i file delle unità. Qui 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 argomenti, lo script usa $SUDO_USER, che è l'account amministratore, e quindi ogni job viene eseguito da un utente che può usare sudo.

L'unità prende il nome dal repository e dal runner, nel formato actions.runner.YOUR-USER-YOUR-REPO.vps-runner-1.service. Non è necessario digitare questo nome:

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

Un runner in esecuzione correttamente registra √ Connected to GitHub e poi una riga che termina con Listening for Jobs; nella pagina Runners del repository viene visualizzato come Idle. Un runner visualizzato 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; una parola aggiuntiva lascia il job accodato 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 molti saltano. Le indicazioni di GitHub sono chiare: i runner self-hosted "dovrebbero quasi mai essere usati per repository pubblici" e "non offrono garanzie sull'esecuzione in macchine virtuali temporanee e pulite e possono essere compromessi in modo persistente da codice non attendibile in un workflow".

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

Le impostazioni di approvazione attenuano il problema, ma non lo risolvono. Per impostazione predefinita, un repository pubblico chiede a un maintainer di approvare il workflow del fork inviato da un contributore per la prima volta. Dopo aver approvato una volta quella persona, le sue pull request successive vengono eseguite senza una nuova richiesta. Il controllo consiste quindi nella lettura manuale di un diff ogni volta, e un payload nascosto tre livelli più in profondità in uno script di build può sfuggire facilmente.

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

La registrazione con --ephemeral fa sì che il runner accetti un job e poi si deregistri, impedendo a un job di leggere il workspace del job successivo. È utile solo se, per ogni job, un componente ricrea la macchina o il container, 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 tenere altro su quel server e considera la macchina usa e getta.

Job Docker 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.

Valuta il compromesso prima di procedere. L'appartenenza al gruppo docker equivale a root, perché un container può montare / con un bind mount ed eseguirvi processi come root. Di conseguenza, un workflow che può comunicare con il socket Docker può leggere e modificare ogni file sul VPS, incluso /etc/shadow. In un repository privato con collaboratori fidati, questo compromesso può essere accettabile. In qualsiasi altro contesto, elimina il vantaggio di usare un utente senza privilegi. Docker rootless mantiene le build dei container entro i permessi dell'utente del runner, ma usa un driver di storage più lento e non consente container privilegiati.

Aggiornamenti e rimozione corretta del runner

Un runner self-hosted si aggiorna per impostazione predefinita. Rileva una nuova release, sostituisce i propri file e riavvia il servizio. Di norma, quindi, non devi fare nulla. ./config.sh --disableupdate disabilita l'aggiornamento automatico quando devi mantenere una versione fissa. Da quel momento, gli aggiornamenti sono a tuo carico: la documentazione di GitHub specifica che un runner configurato con --disableupdate deve essere aggiornato manualmente.

Un aggiornamento manuale mantiene la registrazione, perché .runner e .credentials non sono presenti nell'archivio tar. Arresta il servizio, scarica il nuovo archivio tar e verifica il checksum come gharunner, estrailo nella stessa directory con tar xzf, quindi 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 annulla 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 annullare la registrazione, il runner rimane elencato come Offline nel repository, perché GitHub rileva che è stato rimosso 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 visualizza questo messaggio e termina quando viene eseguito come root. Il controllo è intenzionale, perché i file di proprietà di root in _work interrompono ogni job successivo eseguito con l'utente del servizio. Esegui ./config.sh come gharunner. La variabile RUNNER_ALLOW_RUNASROOT ignora il controllo e utilizzarla sposta soltanto il problema a una fase successiva.

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. È scaduto, perché la sua validità è di un'ora, oppure al posto del token di registrazione della pagina Runners è stato incollato un personal access token. Genera un nuovo token e incollalo di nuovo.

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 solo nella sessione del terminale. Se l'unità è abilitata e il runner è ancora offline, leggi journalctl -u 'actions.runner.*' e verifica 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, senza alcuna eliminazione automatica. Monitora du -sh /home/gharunner/actions-runner/_work e aggiungi una pulizia pianificata prima che sia il disco a determinare quando intervenire.

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 nome del repository e del runner per creare il nome del servizio. Esegui prima ./config.sh come utente del runner. In seguito, 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 aperta mentre attende i job. GitHub, quindi, 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 lo stato Offline mentre il relativo servizio è in esecuzione, controlla il filtraggio del traffico in uscita e il DNS, non le regole in ingresso.

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

Puoi farlo, ma GitHub lo sconsiglia. Una pull request proveniente da un fork contiene il proprio file di workflow. Chiunque possa creare un fork del tuo repository può quindi proporre comandi eseguiti sul tuo computer. La richiesta di approvazione riguarda solo la prima esecuzione di un collaboratore. Se colleghi un runner a un repository pubblico, disabilita i workflow delle pull request provenienti dai fork, non conservare altri dati o servizi su quel server e ricrea la macchina a intervalli regolari.

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. Per questo il messaggio può risultare fuorviante. I token di registrazione scadono un'ora dopo essere stati visualizzati e un personal access token non viene 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 diritti di amministratore.

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