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.
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 quali job eseguire, quindi li esegue sul tuo hardware. Lo registri per un repository, lo installi come servizio systemd e viene riavviato automaticamente dopo ogni reboot. GitHub pianifica il job. Il lavoro viene eseguito dal tuo server.
La CI (continuous integration) su un server che controlli direttamente è utile per due motivi. I minuti di build non vengono più conteggiati e un job può accedere a risorse disponibili solo sul tuo sistema, ad esempio una build cache già pronta o una rete privata. Il prezzo da pagare è la sicurezza. Il runner esegue qualunque istruzione definita nel file del workflow, usando l'utente che gli hai assegnato. Per progettazione, quindi, un file di workflow consente l'esecuzione di codice 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 presuppone Ubuntu 24.04 e la versione del runner 2.338.0, la release corrente al 12 ottobre 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-runnerpasswd -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 gharunnerpasswd -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 come utente 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"Esegui prima uname -m se non conosci con certezza l'architettura. x86_64 usa il file linux-x64 riportato sopra. aarch64 usa actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz.
Ora verifica il file scaricato. Il valore SHA256 (secure hash algorithm, 256 bit) riportato di seguito si riferisce all'archivio tar x64 della versione 2.338.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 copialo da una di queste schermate quando installi una versione diversa.
echo "af4b794c1bc41d73d40535e3fe092a39f9679cd8d965954c2aca25a05ca41d32 actions-runner-linux-x64-2.338.0.tar.gz" | sha256sum -cUn download corretto restituisce una riga:
actions-runner-linux-x64-2.338.0.tar.gz: OKUn file troncato o modificato restituisce un errore e un avviso:
actions-runner-linux-x64-2.338.0.tar.gz: FAILED
sha256sum: WARNING: 1 computed checksum did NOT matchNon saltare questa verifica lasciando che sia tar a rilevare il problema. Un archivio scritto solo parzialmente genera un errore con 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.338.0.tar.gz
lsContenuto 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. Mantenere la shell dell'utente del runner e installarle con sudo, perché lo script scrive nel database dei pacchetti di sistema.
exit
cd /home/gharunner/actions-runner
sudo ./bin/installdependencies.shSu Ubuntu 24.04 vengono installate libkrb5-3, zlib1g, liblttng-ust1t64, libssl3t64 e libicu74. Per ogni libreria, lo script prova diversi nomi di versione e mantiene quello disponibile nella release in uso. Per questo lo stesso script funziona sulle versioni precedenti di Ubuntu e su Debian.
Se si salta questo passaggio e manca una di queste librerie, ./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.La mancanza 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 provengono dallo stesso punto: prima di avviarsi, config.sh verifica le librerie incluse con ldd e libicu con ldconfig. Un collegamento non risolto arresta quindi lo script, invece di causare un arresto anomalo difficile da interpretare in un secondo momento.
Registra il runner nel repository
Ottieni un token dal repository. Apri Settings, quindi Actions, Runners e New self-hosted runner. La pagina mostra un token di registrazione. Scade un'ora dopo la creazione, quindi generalo quando sei pronto a incollarlo.
Esegui la registrazione come utente del runner. config.sh non viene 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 \
--replaceEcco la funzione di questi flag. --name determina come il runner viene visualizzato nel repository, quindi scegli un nome che riconoscerai ancora tra sei mesi. --labels aggiunge le tue etichette; il runner include già self-hosted, Linux e X64 senza ulteriori opzioni. --work specifica la directory in cui vengono creati i checkout, all'interno della directory del runner. --unattended risponde alle richieste interattive usando i valori predefiniti, che è il comportamento corretto quando il comando viene eseguito in uno script. --replace riutilizza una registrazione esistente con lo stesso nome invece di interrompersi 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 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 statussvc.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-pagerUn 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 -aSe 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 molti saltano. Le indicazioni di GitHub sono chiare: i runner self-hosted «dovrebbero essere usati quasi mai per i repository pubblici» e «non offrono garanzie sull'esecuzione in macchine virtuali temporanee e pulite; inoltre, possono essere compromessi in modo persistente da codice non attendibile presente in un workflow».
Il meccanismo è semplice. Una pull request proveniente da un fork contiene 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 suoi comandi sul tuo VPS. Non serve alcun 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 chiede a un maintainer di approvare il workflow del fork di un nuovo collaboratore. GitHub considera una persona un nuovo collaboratore solo finché nessun suo commit o pull request è stato integrato nel repository. Dopo quel momento, le pull request successive vengono eseguite senza richiesta di approvazione. GitHub stesso avverte che è sufficiente una modifica banale integrata nel repository per superare questo controllo. Di fatto, il filtro consiste nella lettura manuale di un diff, mentre un payload nascosto a tre livelli di profondità in uno script di build è facile da non notare.
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. Non protegge il server. L'attaccante dispone di una shell con l'utente gharunner, quindi può leggere ogni file accessibile a quell'utente, raggiungere qualsiasi risorsa accessibile al VPS sulla sua rete privata e lasciare un meccanismo persistente in ~/.bashrc oppure in una unità systemd dell'utente, eseguita durante il job successivo.
La registrazione tramite --ephemeral fa sì che il runner accetti un job e poi annulli la propria registrazione, impedendo a un job di leggere il workspace del job successivo. È utile solo se un sistema ricrea la macchina o il container per ogni job, perché una backdoor scritta nella home directory dell'utente del runner sopravvive a una nuova registrazione.
Le regole successive 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 da 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
Un runner self-hosted si aggiorna automaticamente per impostazione predefinita. Rileva una nuova release, sostituisce i propri file e riavvia il servizio. Normalmente non è quindi necessario alcun intervento. Per disabilitare l'aggiornamento automatico, passa --disableupdate a ./config.sh durante la registrazione del runner. Per un runner già registrato, devi registrarlo nuovamente con questo flag. Questa impostazione non consente di restare definitivamente su una sola versione. La documentazione di GitHub specifica che un runner configurato con --disableupdate deve comunque essere aggiornato manualmente entro 30 giorni dalla nuova release. In caso contrario, GitHub smette di accodare i job a quel runner.
Un aggiornamento manuale mantiene la registrazione, perché .runner e .credentials non sono inclusi nell'archivio tar. Arresta il servizio, scarica il nuovo archivio tar e verificane il checksum come indicato in gharunner, estrailo nella stessa directory con tar xzf, quindi riavvia il servizio:
cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh startPer 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.
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_HEREEliminare la directory senza annullare la registrazione lascia il runner elencato come Offline nel repository. Rimane presente finché un amministratore non fa clic su Force remove this runner oppure finché GitHub non lo rimuove automaticamente dopo 14 giorni senza connessione.
Modalità di errore e stringhe visualizzate
Must not run with sudo. config.sh visualizza 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 come utente del servizio. Esegui ./config.sh come gharunner. La variabile RUNNER_ALLOW_RUNASROOT disabilita il controllo, ma in questo modo il problema si manifesta più avanti.
sudo: ./svc.sh: command not found. Ti trovi nella directory corretta. svc.sh non esiste ancora, perché config.sh non ha completato una registrazione. Registra il runner, quindi installa il servizio.
Http response code: NotFound from 'POST https://api.github.com/actions/runner-registration'. Di solito il token non è un token di registrazione valido. Potrebbe essere scaduto, perché la sua validità è di un'ora, oppure potresti avere incollato un personal access token al posto del token di registrazione disponibile nella pagina Runners. Genera un nuovo token e incollalo nuovamente. Verifica inoltre che --url sia l'URL completo del repository.
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 ma il runner risulta ancora offline, leggi journalctl -u 'actions.runner.*' e verifica la connettività 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 aggiungi una pulizia pianificata prima che il disco si riempia.
FAQ
Perché sudo ./svc.sh install restituisce «command not found»?
Perché svc.sh non si trova nell'archivio tar 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 con l'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, 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 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?
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 predefinita riguarda soltanto i contributori che non hanno mai avuto un commit o una pull request integrati nel repository. Se colleghi un runner a un repository pubblico, disabilita su quel runner i workflow delle pull request provenienti dai fork, non utilizzare altro su quel server e ricrea la macchina secondo una pianificazione stabilita.
Perché la registrazione non riesce con Http response code: NotFound?
La chiamata di registrazione restituisce NotFound quando la credenziale è errata, non soltanto quando l'URL è errato, rendendo il messaggio fuorviante. I token di registrazione scadono un'ora dopo essere stati visualizzati e un personal access token non è accettato per questa chiamata. Apri Settings, Actions, Runners, New self-hosted runner, copia il nuovo token e verifica che il valore --url punti a un repository sul quale disponi dei diritti di amministratore.