Self-hosted GitHub Actions runner installeren op Ubuntu
Installeer een self-hosted GitHub Actions runner op Ubuntu 24.04. Leer hoe u de runner als systemd-service configureert en voorkom beveiligingsrisico's bij fork pull requests.
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 taken en voert deze uit op uw hardware. U registreert de runner voor een repository, installeert deze als een systemd-service en de runner start automatisch na elke reboot. GitHub plant de taak in. Uw server voert het werk uit.
CI (continuous integration) op een eigen machine is om twee redenen waardevol. Build-minuten worden niet langer gemeten en een taak kan bronnen bereiken die alleen op uw machine beschikbaar zijn, zoals een warme build-cache of een privénetwerk. De prijs hiervoor is veiligheid. De runner voert alles uit wat in het workflow-bestand staat, onder de gebruiker die u heeft toegewezen. Een workflow-bestand is daarom inherent remote code execution. Bij een private repository is dit acceptabel, omdat alleen vertrouwde personen wijzigingen kunnen aanbrengen. Bij een public repository vormt dit een reëel risico; de sectie over fork pull requests legt dit mechanisme uit.
Alles hieronder is gebaseerd op Ubuntu 24.04 met runner-versie 2.338.0, de huidige release per 12 oktober 2026.
Wat u nodig heeft voordat u begint
Begin met een VPS met een standaard beheerdersaccount en sudo, de status 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) naar GitHub en houdt deze open terwijl deze wacht op werk, waardoor GitHub nooit verbinding maakt met uw server. Uw firewall kan gesloten blijven voor de buitenwereld en taken komen alsnog aan.
U heeft ook beheerdersrechten op de repository nodig, omdat het registratietoken wordt getoond in de instellingen van de repository.
Maak een toegewezen gebruiker aan voor de runner
Voer de runner nooit uit als root of als uw eigen beheerdergebruiker. Elke taak erft de rechten van de runner-gebruiker, dus een workflow die sudo aanroept slaagt als de runner-gebruiker sudo mag gebruiken. Maak één gebruiker zonder privileges aan die niets bezit behalve de eigen thuismap. Gebruikersaccounts met minimale rechten op een VPS behandelt het algemene patroon. Hieronder volgt de specifieke uitvoering.
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 blokkeert het wachtwoord, zodat niemand kan inloggen als gharunner met een wachtwoord. De modus 700 op de runner-map is van belang omdat de runner daar zijn inloggegevens in leesbare tekst opslaat en een checkout privégegevens van de broncode kan bevatten.
Controleer beide eigenschappen voordat u verdergaat:
sudo passwd -S gharunner
sudo -l -U gharunnerpasswd -S print een regel die begint met gharunner L, waarbij L betekent dat het wachtwoord is geblokkeerd. sudo -l -U gharunner hoort te antwoorden met is not allowed to run sudo. Als het een lijst met toegestane commando's print, zit het account in een sudo-groep en is de isolatie die u zojuist heeft opgebouwd verdwenen.
De runner downloaden en het tarball-bestand controleren
Werk vanaf dit punt als de gebruiker 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"Voer uname -m eerst uit als u niet zeker bent van de architectuur. x86_64 gebruikt het bestand linux-x64 van hierboven. aarch64 gebruikt actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz.
Controleer nu wat u heeft gedownload. De SHA256 (secure hash algorithm, 256 bit) hieronder is voor de 2.338.0 x64 tarball. GitHub toont de waarde voor de huidige release op de releasepagina en op het scherm New self-hosted runner. Deze waarde verandert bij elke versie; kopieer deze dus vanaf daar wanneer u een andere versie installeert.
echo "af4b794c1bc41d73d40535e3fe092a39f9679cd8d965954c2aca25a05ca41d32 actions-runner-linux-x64-2.338.0.tar.gz" | sha256sum -cEen geslaagde download geeft één regel weer:
actions-runner-linux-x64-2.338.0.tar.gz: OKEen afgebroken of gewijzigd bestand geeft de foutmelding en een waarschuwing weer:
actions-runner-linux-x64-2.338.0.tar.gz: FAILED
sha256sum: WARNING: 1 computed checksum did NOT matchSla de controle niet over om tar het probleem te laten vinden. Een onvolledig geschreven archief faalt met gzip: stdin: unexpected end of file en tar: Unexpected EOF in archive; dit geeft aan dat het bestand beschadigd is, maar niet of het voortijdig is afgebroken of is vervangen.
tar xzf ./actions-runner-linux-x64-2.338.0.tar.gz
lsWat het tarball-bestand bevat en wat niet
Na het uitpakken bevat de map config.sh, run.sh, env.sh, safe_sleep.sh, bin/ en externals/. bin/ bevat de runner-binaries en bin/installdependencies.sh. externals/ bevat de gebundelde Node-runtime waarop JavaScript-acties worden uitgevoerd.
Er is nog geen svc.sh aanwezig. De documentatie van GitHub beschrijft dit als het script "dat wordt aangemaakt na het succesvol toevoegen van de runner", omdat het wordt geschreven op basis van een sjabloon waarin uw repository- en runnernaam in de servicenaam zijn verwerkt. Daarom faalt sudo ./svc.sh install vóór ./config.sh met sudo: ./svc.sh: command not found. Registreer eerst en installeer daarna de service.
De afhankelijkheden voor de runner installeren
De runner is een .NET-applicatie en vereist daarom enkele gedeelde bibliotheken. Verlaat de shell van de runner-gebruiker en installeer deze met sudo, aangezien het script schrijft naar de pakketdatabase van het systeem.
exit
cd /home/gharunner/actions-runner
sudo ./bin/installdependencies.shOp Ubuntu 24.04 haalt dit libkrb5-3, zlib1g, liblttng-ust1t64, libssl3t64 en libicu74 op. Het script probeert verschillende versienamen voor elke bibliotheek en behoudt de versie die uw release aanbiedt; daarom werkt hetzelfde script op oudere Ubuntu-versies en op Debian.
Als u deze stap overslaat en een van deze bibliotheken ontbreekt, stopt ./config.sh voordat er actie wordt ondernomen:
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 zijn afkomstig uit dezelfde bron: config.sh controleert de gebundelde bibliotheken met ldd, en libicu met ldconfig, voordat het proces start. Een niet-opgeloste koppeling stopt het script dus in plaats van dat er later een onduidelijke crash optreedt.
De runner registreren bij uw repository
Haal een token op uit de repository. Open Settings, vervolgens Actions, dan Runners en kies New self-hosted runner. De pagina toont een registratietoken. Deze verloopt één uur na aanmaak, dus genereer deze pas wanneer u klaar bent om deze te plakken.
Registreer als de runner-gebruiker. config.sh weigert te draaien onder 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 \
--replaceWat deze vlaggen doen. --name bepaalt hoe de runner verschijnt in de repository, dus kies een naam die u over zes maanden nog steeds herkent. --labels voegt uw eigen labels toe; de runner beschikt al over self-hosted, Linux en X64 zonder dat u hierom hoeft te vragen. --work benoemt de map waar checkouts landen, binnen de runner-directory. --unattended beantwoordt de interactieve prompts met hun standaardwaarden, wat gewenst is wanneer het commando in een script staat. --replace neemt een bestaande registratie met dezelfde naam over in plaats van te falen; dit is wat u wilt wanneer u de server opnieuw opbouwt.
Een succesvolle uitvoering eindigt met deze regels:
√ Runner successfully added
√ Runner connection is good
√ Settings Saved.De registratie bevindt zich nu in de runner-directory als .runner, .credentials en .credentials_rsaparams. De laatste twee identificeren deze runner bij GitHub, dus iedereen die deze kan lezen, kan zich als de runner voordoen. Dat is de reden waarom de directory de modus 700 heeft en de gebruiker geen sudo-rechten bezit.
De runner installeren als systemd-service
./run.sh in een terminal is prima voor een eenmalige test, maar het proces stopt zodra uw SSH-sessie wordt beëindigd. Installeer de service zodat de runner automatisch start bij het opstarten van het systeem. systemd-services en timers op een VPS legt de unit-bestanden zelf uit. Hier schrijft svc.sh er een voor u.
exit
cd /home/gharunner/actions-runner
sudo ./svc.sh install gharunner
sudo ./svc.sh start
sudo ./svc.sh statussvc.sh vereist root-rechten omdat het een unit naar /etc/systemd/system schrijft en deze 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, wat uw beheerdersaccount is, waardoor elke taak wordt uitgevoerd als een gebruiker die sudo kan gebruiken.
De unit is vernoemd naar de repository en de runner, in de vorm actions.runner.YOUR-USER-YOUR-REPO.vps-runner-1.service. U hoeft dit nooit volledig uit te typen:
systemctl list-units 'actions.runner.*'
sudo journalctl -u 'actions.runner.*' -n 20 --no-pagerEen gezonde runner logt √ Connected to GitHub en vervolgens een regel die eindigt op Listening for Jobs, en de Runners-pagina van de repository toont deze als Idle. Een runner die als Offline wordt weergegeven, draait niet of kan GitHub niet bereiken op poort 443.
Een taak naar de runner sturen
runs-on selecteert een runner op basis van een label. Vraag om self-hosted plus uw eigen label, zodat een taak niet terechtkomt op een runner die u niet bedoelde.
name: build
on:
push:
branches: [main]
jobs:
build:
runs-on: [self-hosted, linux, vps]
steps:
- uses: actions/checkout@v5
- run: uname -aAls de taak blijft wachten bij Waiting for a runner to pick up this job, komen de labels niet overeen. Elk label in runs-on moet bestaan op de runner; één extra woord zorgt er al voor dat de taak in de wachtrij blijft staan zonder dat er ergens een foutmelding verschijnt. Vergelijk de lijst met de labels die naast de runner in de repository-instellingen worden getoond.
Waarom self-hosted runners en publieke repositories niet samengaan
Dit is het onderdeel dat mensen overslaan. De richtlijnen van GitHub zijn duidelijk: self-hosted runners "zouden bijna nooit gebruikt moeten worden voor publieke repositories", en ze "bieden geen garanties voor uitvoering in vluchtige, schone virtuele machines en kunnen permanent gecompromitteerd worden door niet-vertrouwde code in een workflow".
Het mechanisme is eenvoudig. Een pull request vanuit een fork bevat een eigen kopie van het workflowbestand. Als uw publieke repository pull request-workflows uitvoert op uw runner, kan iedereen die de repository fork-t een workflow voorstellen die hun commando's op uw VPS uitvoert. Zij hebben hiervoor geen schrijfrechten nodig, omdat hetgeen zij voorstellen juist datgene is wat wordt uitgevoerd.
Instellingen voor goedkeuring verzachten dit zonder het probleem op te lossen. Het standaardbeleid voor een publieke repository vereist dat een beheerder de fork-workflow van een nieuwe bijdrager goedkeurt. GitHub beschouwt iemand pas als nieuwe bijdrager totdat een van hun commits of pull requests is gemerged in de repository. Daarna worden hun latere pull requests uitgevoerd zonder tussenkomst, en GitHub waarschuwt zelf dat een triviale gemergde fix voldoende is om dit punt te bereiken. De controle is dus afhankelijk van een mens die een diff leest, en een payload die drie niveaus diep in een build-script verborgen zit, is gemakkelijk over het hoofd te zien.
Een fork pull request ontvangt uw secrets niet en de GITHUB_TOKEN is alleen-lezen. Dat beperkt de schade binnen GitHub. Het doet echter niets voor uw server. De aanvaller beschikt over een shell als gharunner, waardoor deze elk bestand kan lezen waar die gebruiker toegang toe heeft, alles kan bereiken wat de VPS op het privénetwerk kan benaderen, en iets kan achterlaten in ~/.bashrc of een systemd-unit van de gebruiker die tijdens de volgende taak wordt uitgevoerd.
Registreren met --ephemeral zorgt ervoor dat de runner één taak accepteert en zich daarna afmeldt, waardoor één taak de werkruimte van de volgende taak niet kan lezen. Dit helpt alleen als iets de machine of de container voor elke taak opnieuw opbouwt, omdat een achterdeur die in de home-directory van de runner-gebruiker is geschreven, een nieuwe registratie overleeft.
De regels die volgen zijn kort. Gebruik self-hosted runners voor private repositories. Als u er toch een moet koppelen aan een publieke repository, voer daar dan geen fork pull requests op uit, bewaar niets anders op die server en beschouw de machine als wegwerpbaar.
Docker-taken en de groep die in feite root is
Container-taken, service-containers en elke workflow-stap die docker build aanroept, vereisen een Docker-daemon op de runner-host. Installeer Docker op de gebruikelijke wijze, zoals beschreven in Docker en Docker Compose op een VPS, en voeg vervolgens de runner-gebruiker toe aan de docker-groep.
Begrijp de afweging voordat u dit uitvoert. Het lidmaatschap van de docker-groep staat gelijk aan root-toegang, omdat een container / kan mounten en daarin als root kan draaien. Een workflow die met de Docker-socket kan communiceren, kan dus elk bestand op de VPS lezen en schrijven, inclusief /etc/shadow. Bij een private repository met vertrouwde bijdragers kan dit een acceptabele prijs zijn. In alle andere gevallen heft het de beveiliging van een gebruiker zonder privileges op. Rootless Docker houdt container-builds binnen de rechten van de runner-gebruiker zelf, ten koste van een tragere storage-driver en het onvermogen om geprivilegieerde containers te draaien.
Updates en het correct verwijderen van de runner
Een self-hosted runner werkt zichzelf standaard bij. De runner detecteert een nieuwe release, vervangt de eigen bestanden en herstart de service, waardoor u normaal gesproken geen actie hoeft te ondernemen. Om de automatische update uit te schakelen, geeft u --disableupdate mee aan ./config.sh tijdens het registreren van de runner; voor een reeds geregistreerde runner betekent dit dat u deze opnieuw moet registreren met de vlag. Dit staat u niet toe om voor onbepaalde tijd op één versie te blijven. De documentatie van GitHub is expliciet: een runner die is geconfigureerd met --disableupdate moet nog steeds handmatig worden bijgewerkt binnen 30 dagen na een nieuwe release, anders stopt GitHub met het in de wachtrij plaatsen van taken voor deze runner.
Een handmatige update behoudt de registratie, omdat .runner en .credentials niet in het tarball-bestand zitten. Stop de service, download en controleer de checksum van de nieuwe tarball als gharunner, pak deze uit over dezelfde map met tar xzf en start daarna de service opnieuw:
cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh startOm de runner te verwijderen, deinstalleert u eerst de service en voert u daarna de deregistratie uit. Het verwijderingstoken is afkomstig van dezelfde Runners-pagina, onder de knop Remove van de betreffende 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_HEREHet verwijderen van de map zonder de runner te deregistreren, zorgt ervoor dat de runner als Offline in de repository blijft staan. De runner blijft daar staan totdat een beheerder op Force remove this runner klikt, of totdat GitHub de runner automatisch verwijdert na 14 dagen zonder verbinding.
Foutmodi en de bijbehorende meldingen
Must not run with sudo. config.sh toont deze melding en sluit af wanneer het als root wordt uitgevoerd. Deze controle is bewust ingebouwd, omdat bestanden die eigendom zijn van root in _work elk volgend proces verstoren dat als servicegebruiker wordt uitgevoerd. Voer ./config.sh uit als gharunner. De variabele RUNNER_ALLOW_RUNASROOT omzeilt deze controle, maar het gebruik ervan verplaatst het probleem enkel naar een later tijdstip.
sudo: ./svc.sh: command not found. U bevindt zich in de juiste map. svc.sh bestaat nog niet, omdat config.sh de registratie nog niet heeft voltooid. Registreer de runner en installeer daarna de service.
Http response code: NotFound from 'POST https://api.github.com/actions/runner-registration'. Meestal is het token geen geldig registratietoken. Het is mogelijk verlopen, aangezien deze slechts één uur geldig zijn, of er is een personal access token geplakt in plaats van het registratietoken van de Runners-pagina. Genereer een nieuw token, plak dit opnieuw en controleer of --url de volledige repository-URL bevat.
Dependencies is missing for Dotnet Core 6.0. Voer sudo ./bin/installdependencies.sh uit vanuit de runner-map als root en registreer daarna opnieuw.
Runner Offline na een herstart. Voer systemctl is-enabled 'actions.runner.*' uit. Als er niets wordt vermeld, is ./svc.sh install nooit uitgevoerd; de runner bestond in dat geval alleen binnen uw terminalsessie. Als de unit is ingeschakeld en de runner nog steeds Offline is, lees dan journalctl -u 'actions.runner.*' en controleer uitgaand HTTPS-verkeer.
De schijf raakt vol. Checkouts, build-caches en Docker-images hopen zich op onder _work en in de home-map van de runner-gebruiker; er is geen automatisch proces dat deze opschoont. Monitor du -sh /home/gharunner/actions-runner/_work en voeg een geplande opschoonactie toe voordat de schijfruimte kritiek wordt.
FAQ
Waarom geeft sudo ./svc.sh install de melding command not found?
Omdat svc.sh niet in de runner-tarball zit. Het wordt gegenereerd in de runner-directory wanneer ./config.sh de registratie voltooit, waarbij uw repository- en runner-naam worden gebruikt om de servicenaam op te bouwen. Voer ./config.sh eerst uit als de runner-gebruiker. Daarna vindt sudo ./svc.sh install gharunner het script en schrijft het een unit genaamd 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 naar GitHub en houdt deze open terwijl hij wacht op taken, dus GitHub initieert nooit een verbinding met uw VPS. Sta uitgaand verkeer op poort 443 toe en laat uw inkomende regels gesloten. Als de runner de status Offline toont terwijl de service draait, controleer dan de uitgaande filtering en DNS in plaats van de inkomende regels.
Kan ik een self-hosted runner gebruiken voor een publieke repository?
Dat kan, maar GitHub raadt het af. Een pull request van een fork bevat een eigen workflow-bestand, dus iedereen die uw repository kan forken, kan commando's voorstellen die op uw machine worden uitgevoerd. De standaard goedkeuringsprompt geldt alleen voor bijdragers van wie nog nooit een commit of pull request is samengevoegd in de repository. Als u een runner koppelt aan een publieke repository, schakel dan workflows voor fork-pull-requests uit, bewaar niets anders op die server en installeer de machine periodiek opnieuw.
Waarom mislukt de registratie met Http response code: NotFound?
De registratie-aanroep geeft NotFound terug wanneer de inloggegevens onjuist zijn, niet alleen wanneer de URL onjuist is, wat de melding misleidend maakt. Registratietokens verlopen één uur nadat ze zijn getoond en een personal access token wordt niet geaccepteerd voor deze aanroep. Open Instellingen, Actions, Runners, New self-hosted runner opnieuw, kopieer het nieuwe token en bevestig dat de --url-waarde verwijst naar een repository waar u beheerdersrechten heeft.