Self-hosted GitHub Actions-runner op een VPS
Installeer runner 2.336.0 op Ubuntu 24.04 met dedicated user, checksum, config.sh en systemd. Lees ook het risico van pull requests vanuit forks.
Wat een self-hosted GitHub Actions-runner doet
Een self-hosted GitHub Actions-runner is een programma dat u op uw eigen VPS installeert. Het vraagt GitHub om jobs en voert deze uit op uw hardware. U registreert de runner voor één repository, installeert deze als een systemd-service en de runner start na elke reboot opnieuw. GitHub plant de job. Uw server voert het werk uit.
CI (continuous integration) op een server die u beheert, is om twee redenen nuttig. Buildminuten worden niet meer per gebruik berekend. Daarnaast kan een job bij resources komen die alleen uw machine heeft, zoals een warme buildcache of een privénetwerk. Daar staat een beveiligingsrisico tegenover. De runner voert uit wat het workflowbestand voorschrijft, onder de gebruiker die u eraan hebt toegewezen. Een workflowbestand maakt dus bewust uitvoering van code op afstand mogelijk. Op een private repository is dat geen probleem, omdat alleen personen die u vertrouwt een workflow kunnen toevoegen. Op een public repository is dit wel een reëel risico. In het gedeelte over pull requests vanuit forks wordt uitgelegd hoe dit werkt.
Alles hieronder gebruikt Ubuntu 24.04 met runner-versie 2.336.0, de huidige release in juli 2026.
Wat u nodig hebt voordat u begint
Begin met een VPS met een gewoon beheerdersaccount en sudo, in de toestand die u bereikt in de eerste tien minuten op een nieuwe VPS. U hoeft geen inkomende poort te openen. De runner opent een uitgaande HTTPS-verbinding (hypertext transfer protocol secure) met GitHub en houdt deze open terwijl hij op werk wacht. GitHub maakt daarom nooit verbinding met uw server. Uw firewall kan voor de buitenwereld gesloten blijven en taken komen nog steeds binnen.
U hebt ook beheerdersrechten voor de repository nodig, omdat het registratietoken wordt weergegeven in de repository-instellingen.
Maak een speciale gebruiker voor de runner
Voer de runner nooit uit als root of als uw eigen beheerder. Elke taak erft de rechten van de runner-gebruiker. Een workflow die sudo aanroept, slaagt dus als de runner-gebruiker sudo kan gebruiken. Maak één gebruiker zonder verhoogde rechten die niets anders beheert dan de eigen homedirectory. Gebruikersaccounts met minimale rechten op een VPS beschrijft het algemene patroon. Hieronder staat de specifieke configuratie.
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 vergrendelt het wachtwoord. Niemand kan dan met een wachtwoord inloggen als gharunner. Modus 700 voor de runnerdirectory is belangrijk, omdat de runner daar de referenties in leesbare tekst opslaat en een checkout privébroncode kan bevatten.
Controleer beide eigenschappen voordat u verdergaat:
sudo passwd -S gharunner
sudo -l -U gharunnerpasswd -S geeft een regel weer die begint met gharunner L. L betekent dat het wachtwoord is vergrendeld. sudo -l -U gharunner moet antwoorden met is not allowed to run sudo. Als de opdracht in plaats daarvan een lijst met toegestane opdrachten weergeeft, bevindt de gebruiker zich in een sudo-groep en is de isolatie die u zojuist hebt ingesteld opgeheven.
De runner downloaden en het tarball controleren
Werk vanaf hier als de gebruiker 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"Voer eerst uname -m uit als u niet zeker bent van de architectuur. x86_64 gebruikt het bovenstaande bestand linux-x64. aarch64 gebruikt actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz.
Controleer nu wat u hebt gedownload. De onderstaande SHA256 (secure hash algorithm, 256 bit) is voor het tarball 2.336.0 x64. GitHub toont de waarde voor de huidige release op de releasepagina en op het scherm New self-hosted runner. De waarde verandert bij elke versie. Kopieer de waarde daarom daar wanneer u een andere versie installeert.
echo "04cf0be1aff4c3ec3554466c39124ca250e3effd8873bb7e8d68535aa9505d5d actions-runner-linux-x64-2.336.0.tar.gz" | sha256sum -cBij een correcte download wordt één regel weergegeven:
actions-runner-linux-x64-2.336.0.tar.gz: OKBij een afgekapt of gewijzigd bestand worden de foutmelding en een waarschuwing weergegeven:
actions-runner-linux-x64-2.336.0.tar.gz: FAILED
sha256sum: WARNING: 1 computed checksum did NOT matchSla deze controle niet over om tar het probleem te laten vinden. Een onvolledig geschreven archief mislukt met gzip: stdin: unexpected end of file en tar: Unexpected EOF in archive. Hieruit blijkt dat het bestand beschadigd is, maar niet of het bestand te vroeg is afgebroken of vervangen.
tar xzf ./actions-runner-linux-x64-2.336.0.tar.gz
lsWat de tarball bevat en wat niet
Na het uitpakken bevat de directory config.sh, run.sh, env.sh, safe_sleep.sh, bin/ en externals/. bin/ bevat de runner-binaries en bin/installdependencies.sh. externals/ bevat de meegeleverde Node-runtime waarop JavaScript-acties worden uitgevoerd.
Er is nog geen svc.sh. In de documentatie van GitHub wordt dit beschreven als het script "dat wordt gemaakt nadat de runner succesvol is toegevoegd", omdat het wordt gegenereerd op basis van een sjabloon waarin uw repositorynaam en runnernaam in de servicenaam zijn opgenomen. Daarom mislukt sudo ./svc.sh install vóór ./config.sh met sudo: ./svc.sh: command not found. Registreer de runner eerst en installeer daarna de service.
Installeer de afhankelijkheden van de runner
De runner is een .NET-toepassing en heeft daarom enkele gedeelde bibliotheken nodig. Blijf in de shell van de runner-gebruiker en installeer deze met sudo, omdat het script de database van systeempakketten wijzigt.
exit
cd /home/gharunner/actions-runner
sudo ./bin/installdependencies.shOp Ubuntu 24.04 installeert dit libkrb5-3, zlib1g, liblttng-ust1t64, libssl3t64 en libicu74. Het script probeert voor elke bibliotheek meerdere versienamen en gebruikt de naam die in uw release beschikbaar is. Daarom werkt hetzelfde script op oudere Ubuntu-versies en op Debian.
Sla deze stap over en ./config.sh stopt voordat het iets uitvoert:
Dependencies is missing for Dotnet Core 6.0
Execute sudo ./bin/installdependencies.sh to install any missing Dotnet Core 6.0 dependencies.Een ontbrekende libicu geeft hetzelfde advies onder een andere eerste regel: Libicu's dependencies is missing for Dotnet Core 6.0. Beide meldingen hebben dezelfde oorzaak: config.sh voert vóór de start ldd uit op de meegeleverde bibliotheken. Daardoor stopt het script bij een onopgeloste koppeling, in plaats van later een onduidelijke crash te veroorzaken.
De runner registreren bij uw repository
Haal een token op uit de repository. Open Settings, vervolgens Actions, daarna Runners en ten slotte New self-hosted runner. Op de pagina staat een registratietoken dat begint met A. Het verloopt een uur nadat het is aangemaakt. Genereer het daarom wanneer u klaar bent om het te plakken.
Registreer de runner als de runner-gebruiker. config.sh wordt niet uitgevoerd met 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 \
--replaceDit is wat de flags doen. --name bepaalt hoe de runner in de repository wordt weergegeven. Kies daarom een naam die u over zes maanden nog herkent. --labels voegt uw eigen labels toe. De runner heeft zonder verdere configuratie al self-hosted, Linux en X64. --work bepaalt de naam van de directory waarin checkouts worden geplaatst, binnen de runner-directory. --unattended beantwoordt de interactieve prompts met de standaardwaarden. Dat is gewenst wanneer de opdracht in een script staat. --replace neemt een bestaande registratie met dezelfde naam over in plaats van te mislukken. Dat is gewenst wanneer u de server opnieuw opbouwt.
Een geslaagde uitvoering eindigt met deze regels:
√ Runner successfully added
√ Runner connection is good
√ Settings Saved.De registratie staat nu in de runner-directory als .runner, .credentials en .credentials_rsaparams. De laatste twee identificeren deze runner bij GitHub. Iedereen die ze kan lezen, kan zich daarom als deze runner voordoen. Daarom heeft de directory modus 700 en heeft de gebruiker geen sudo-rechten.
De runner installeren als systemd-service
./run.sh in een terminal is geschikt voor één test, maar het proces stopt zodra uw SSH-sessie eindigt. Installeer de service zodat de runner bij het opstarten wordt gestart. systemd-services en timers op een VPS beschrijft de unit-bestanden zelf. svc.sh maakt er hier een voor u aan.
exit
cd /home/gharunner/actions-runner
sudo ./svc.sh install gharunner
sudo ./svc.sh start
sudo ./svc.sh statussvc.sh vereist root omdat het een unit-bestand naar /etc/systemd/system schrijft en de service inschakelt. Het argument na install is de gebruiker waaronder de service wordt uitgevoerd. Geef gharunner expliciet op. Zonder argument valt het script terug op $SUDO_USER. Dat is uw beheerdersaccount. Vervolgens wordt elke taak uitgevoerd als een gebruiker die sudo kan gebruiken.
De unit krijgt een naam op basis van de repository en de runner, in de vorm actions.runner.YOUR-USER-YOUR-REPO.vps-runner-1.service. U hoeft die naam nooit zelf in te voeren:
systemctl list-units 'actions.runner.*'
sudo journalctl -u 'actions.runner.*' -n 20 --no-pagerEen goed werkende runner logt √ Connected to GitHub en daarna een regel die eindigt op Listening for Jobs. Op de pagina Runners van de repository wordt de runner weergegeven als Idle. Een runner met de status Offline wordt niet uitgevoerd of kan GitHub niet bereiken via poort 443.
Een taak naar de runner sturen
runs-on selecteert een runner op label. Vraag om self-hosted plus uw eigen label, zodat een taak niet op een runner terechtkomt die u niet hebt bedoeld.
name: build
on:
push:
branches: [main]
jobs:
build:
runs-on: [self-hosted, linux, vps]
steps:
- uses: actions/checkout@v5
- run: uname -aAls de taak bij Waiting for a runner to pick up this job blijft wachten, komen de labels niet overeen. Elk label in runs-on moet op de runner bestaan. Eén extra woord zorgt er dus voor dat de taak in de wachtrij blijft staan zonder dat ergens een fout wordt gemeld. Vergelijk de lijst met de labels die naast de runner in de repository-instellingen worden weergegeven.
Waarom self-hosted runners en openbare repositories niet samengaan
Dit is het deel dat mensen overslaan. De richtlijnen van GitHub zijn duidelijk: self-hosted runners "should almost never be used for public repositories" en ze "do not have guarantees around running in ephemeral clean virtual machines, and can be persistently compromised by untrusted code in a workflow".
Het mechanisme is eenvoudig. Een pull request vanuit een fork bevat een eigen kopie van het workflowbestand. Als uw openbare repository pull request-workflows op uw runner uitvoert, kan iedereen die de repository kan forken een workflow voorstellen die zijn opdrachten op uw VPS uitvoert. Schrijftoegang is niet nodig, omdat het voorgestelde bestand ook daadwerkelijk wordt uitgevoerd.
Goedkeuringsinstellingen beperken dit risico, maar lossen het probleem niet op. Het standaardbeleid voor een openbare repository vraagt een maintainer om de workflow van een fork van een bijdrager die voor het eerst bijdraagt, goed te keuren. Nadat u die persoon eenmaal hebt goedgekeurd, worden latere pull requests van die persoon uitgevoerd zonder nieuwe goedkeuring. De controle bestaat dus uit een persoon die telkens een diff leest. Een payload die drie niveaus diep in een buildscript is verborgen, wordt gemakkelijk over het hoofd gezien.
Een pull request vanuit een fork ontvangt uw secrets niet en de GITHUB_TOKEN is alleen-lezen. Dat beperkt de schade binnen GitHub. Voor uw server maakt het geen verschil. De aanvaller heeft een shell als gharunner. Daardoor kan die elk bestand lezen waartoe die gebruiker toegang heeft, alles bereiken waartoe de VPS via het privénetwerk toegang heeft, en iets achterlaten in ~/.bashrc of in een user systemd-unit die tijdens de volgende job wordt uitgevoerd.
Registratie met --ephemeral zorgt ervoor dat de runner één job accepteert en zich daarna afmeldt. Daardoor kan één job de workspace van de volgende job niet lezen. Dit helpt alleen als de machine of container voor elke job opnieuw wordt opgebouwd, omdat een backdoor in de home-directory van de runnergebruiker een nieuwe registratie overleeft.
De regels die volgen zijn kort. Gebruik self-hosted runners voor private repositories. Als u er toch een aan een openbare repository moet koppelen, voer er dan geen pull requests vanuit forks op uit, bewaar niets anders op die server en behandel de machine als wegwerpmachine.
Docker-taken en de groep die feitelijk root is
Containertaken, servicecontainers en elke workflowstap die docker build aanroept, hebben een Docker-daemon nodig op de runnerhost. Installeer Docker op de gebruikelijke manier. Dit wordt beschreven in Docker en Docker Compose op een VPS. Voeg de runnergebruiker daarna toe aan de groep docker.
Begrijp de gevolgen voordat u dit doet. Lidmaatschap van de groep docker staat gelijk aan root, omdat een container / als bind mount kan koppelen en daarin als root kan worden uitgevoerd. Een workflow die toegang heeft tot de Docker-socket kan daarom elk bestand op de VPS lezen en schrijven, inclusief /etc/shadow. In een private repository met vertrouwde bijdragers kan dat een aanvaardbaar risico zijn. In andere situaties maakt dit de onbevoegde gebruiker zinloos. Met Rootless Docker blijven containerbuilds beperkt tot de rechten van de runnergebruiker. Daar staat tegenover dat de opslagdriver trager is en dat geprivilegieerde containers niet beschikbaar zijn.
Updates en de runner correct verwijderen
Een self-hosted runner wordt standaard automatisch bijgewerkt. De runner detecteert een nieuwe release, vervangt zijn eigen bestanden en start de service opnieuw. Normaal hoeft u dus niets te doen. Met ./config.sh --disableupdate schakelt u de automatische update uit wanneer u een vaste versie nodig hebt. Daarna moet u de runner zelf bijwerken. In de documentatie van GitHub staat expliciet dat een runner die met --disableupdate is geconfigureerd, handmatig moet worden bijgewerkt.
Bij een handmatige update blijft de registratie behouden, omdat .runner en .credentials niet in het tarball staan. Stop de service, download het nieuwe tarball en controleer de checksum ervan met gharunner. Pak het uit over dezelfde directory met tar xzf en start de service daarna opnieuw:
cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh startAls u de runner wilt verwijderen, verwijdert u eerst de service en maakt u daarna de registratie ongedaan. Het verwijderingstoken vindt u op dezelfde pagina Runners, onder de knop Remove van de runner zelf.
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_HEREAls u de directory verwijdert zonder de registratie ongedaan te maken, blijft de runner in de repository vermeld als Offline. GitHub komt namelijk alleen te weten dat de runner is verwijderd wanneer de runner dit meldt of een beheerder de vermelding handmatig verwijdert.
Foutscenario's, met de meldingen die u ziet
Must not run with sudo. config.sh geeft deze melding weer en wordt afgesloten wanneer u de opdracht als root uitvoert. Deze controle is bewust ingebouwd, omdat bestanden met root als eigenaar in _work elke volgende taak breken die als servicegebruiker wordt uitgevoerd. Voer ./config.sh uit als gharunner. De variabele RUNNER_ALLOW_RUNASROOT schakelt de controle uit. Daarmee wordt de fout alleen naar een later moment verschoven.
sudo: ./svc.sh: command not found. U bevindt zich in de juiste map. svc.sh bestaat nog niet, omdat config.sh nog geen registratie heeft voltooid. Registreer de runner en installeer daarna de service.
Http response code: NotFound from 'POST https://api.github.com/actions/runner-registration'. Het token is geen geldig registratietoken. Het is verlopen, omdat registratietokens slechts een uur geldig zijn, of er is een personal access token geplakt in plaats van het registratietoken van de pagina Runners. Genereer een nieuw token en plak het opnieuw.
Dependencies is missing for Dotnet Core 6.0. Voer sudo ./bin/installdependencies.sh vanuit de runnerdirectory uit als root en registreer de runner daarna opnieuw.
Runner offline na een herstart. Voer systemctl is-enabled 'actions.runner.*' uit. Als er niets wordt weergegeven, is ./svc.sh install nooit uitgevoerd. De runner bestond dan alleen binnen uw terminalsessie. Als de unit is ingeschakeld en de runner nog steeds offline is, leest u journalctl -u 'actions.runner.*' en controleert u uitgaande HTTPS-verbindingen.
De schijf raakt vol. Check-outs, buildcaches en Docker-images stapelen zich op onder _work en in de home-directory van de runnergebruiker. Er worden niet automatisch oude bestanden verwijderd. Monitor du -sh /home/gharunner/actions-runner/_work en voeg een geplande opschoontaak toe voordat de schijf vol raakt.
FAQ
Waarom meldt sudo ./svc.sh install dat de opdracht niet is gevonden?
Omdat svc.sh niet in het runner-tarball staat. Het bestand wordt in de runner-directory gegenereerd wanneer ./config.sh de registratie voltooit. Daarbij worden uw repositorynaam en runnernaam gebruikt om de servicenaam samen te stellen. Voer eerst ./config.sh uit als runner-gebruiker. Daarna vindt sudo ./svc.sh install gharunner het script en schrijft het een unit met de naam actions.runner.OWNER-REPO.RUNNER-NAME.service naar /etc/systemd/system.
Moet ik een firewallpoort openen voor een self-hosted runner?
Nee. De runner opent een uitgaande HTTPS-verbinding met GitHub en houdt deze open terwijl hij op jobs wacht. GitHub initieert daardoor nooit een verbinding met uw VPS. Sta uitgaand verkeer op 443 toe en houd uw inkomende regels gesloten. Als de runner Offline toont terwijl de service actief is, controleer dan uitgaande filtering en DNS in plaats van de inkomende regels.
Kan ik een self-hosted runner gebruiken voor een openbare repository?
Dat kan, maar GitHub raadt dit af. Een pull request van een fork bevat het eigen workflowbestand. Iedereen die uw repository kan forken, kan daardoor opdrachten voorstellen die op uw machine worden uitgevoerd. De goedkeuringsprompt geldt alleen voor de eerste uitvoering van een bijdrager. Als u een runner aan een openbare repository koppelt, schakelt u workflows voor pull requests van forks uit, bewaart u niets anders op die server en bouwt u de machine volgens een vast schema opnieuw op.
Waarom mislukt de registratie met Http response code: NotFound?
De registratieaanroep geeft NotFound als de referentie onjuist is, en niet alleen wanneer de URL onjuist is. Daardoor is de melding misleidend. Registratietokens verlopen een uur nadat ze zijn weergegeven. Een personal access token wordt voor deze aanroep niet geaccepteerd. Open opnieuw Settings, Actions, Runners, New self-hosted runner, kopieer het nieuwe token en controleer of de waarde van --url verwijst naar een repository waarvoor u beheerdersrechten hebt.