GitHub Actions runner na VPS: instalacja i systemd
Instrukcja dla Ubuntu 24.04: dedykowany użytkownik, checksum, config.sh i usługa systemd. Uwzględniono ryzyko pull requestów z forków oraz runner 2.336.0.
Jak działa samodzielnie hostowany runner GitHub Actions
Samodzielnie hostowany runner GitHub Actions to program instalowany na własnym VPS, który pobiera zadania z GitHub i uruchamia je na własnym sprzęcie. Runner jest rejestrowany dla jednego repozytorium, instalowany jako usługa systemd i uruchamiany ponownie po każdym restarcie. GitHub planuje wykonanie zadania. Serwer wykonuje pracę.
CI (ciągła integracja) na własnym serwerze ma sens z dwóch powodów. Minuty kompilacji nie są już rozliczane, a zadanie może uzyskać dostęp do zasobów dostępnych tylko na danej maszynie, takich jak rozgrzana pamięć podręczna kompilacji lub sieć prywatna. Ceną jest bezpieczeństwo. Runner wykonuje wszystko, co określa plik workflow, z uprawnieniami wskazanego użytkownika. Plik workflow z założenia umożliwia zdalne wykonanie kodu. W przypadku prywatnego repozytorium jest to akceptowalne, ponieważ tylko zaufane osoby mogą dodać taki plik. W przypadku publicznego repozytorium stanowi to rzeczywiste zagrożenie. Mechanizm opisano w sekcji dotyczącej żądań pull z forków.
Poniżej przyjęto Ubuntu 24.04 oraz runner w wersji 2.336.0, czyli bieżące wydanie z lipca 2026.
Co jest potrzebne przed rozpoczęciem
Należy rozpocząć od VPS z użyciem zwykłego konta administratora i sudo, w stanie osiągniętym po wykonaniu instrukcji pierwsze dziesięć minut na nowym VPS. Nie trzeba otwierać portu przychodzącego. Runner otwiera wychodzące połączenie HTTPS (hypertext transfer protocol secure) z GitHub i utrzymuje je otwarte podczas oczekiwania na zadania, dlatego GitHub nigdy nie łączy się z serwerem. Zapora sieciowa może pozostać zamknięta dla świata zewnętrznego, a zadania nadal będą dostarczane.
Potrzebne są również uprawnienia administratora do repozytorium, ponieważ token rejestracyjny jest wyświetlany w ustawieniach repozytorium.
Utworzenie dedykowanego użytkownika dla runnera
Nie należy uruchamiać runnera jako root ani jako własny użytkownik administracyjny. Każde zadanie dziedziczy uprawnienia użytkownika runnera, dlatego workflow wywołujący sudo zakończy się powodzeniem, jeśli użytkownik runnera może używać sudo. Należy utworzyć nieuprzywilejowanego użytkownika, który nie będzie właścicielem niczego poza własnym katalogiem domowym. Ogólny schemat opisano w sekcji Użytkownicy o zasadzie najmniejszych uprawnień na VPS. Poniżej przedstawiono konkretną konfigurację.
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 blokuje hasło, dlatego nie można zalogować się jako gharunner przy jego użyciu. Tryb 700 katalogu runnera ma znaczenie, ponieważ runner przechowuje w nim dane uwierzytelniające w postaci jawnego tekstu, a kopia robocza może zawierać prywatny kod źródłowy.
Przed kontynuowaniem należy sprawdzić obie właściwości:
sudo passwd -S gharunner
sudo -l -U gharunnerpasswd -S wyświetla wiersz zaczynający się od gharunner L, gdzie L oznacza, że hasło jest zablokowane. sudo -l -U gharunner powinno zwrócić is not allowed to run sudo. Jeśli zamiast tego wyświetli listę dozwolonych poleceń, konto należy do grupy sudo, a utworzona izolacja nie jest zachowana.
Pobieranie runnera i sprawdzanie archiwum tar
Od tego miejsca należy pracować jako użytkownik 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"Najpierw należy uruchomić uname -m, jeśli architektura nie jest znana. x86_64 używa pliku linux-x64 z powyższej sekcji. aarch64 używa actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz.
Następnie należy sprawdzić pobrany plik. Podana wartość SHA256 (bezpieczny algorytm skrótu, 256 bitów) dotyczy archiwum tar 2.336.0 x64. GitHub wyświetla wartość bieżącego wydania na stronie wydania oraz na ekranie New self-hosted runner. Wartość zmienia się przy każdej wersji, dlatego podczas instalowania innej wersji należy skopiować ją z jednego z tych miejsc.
echo "04cf0be1aff4c3ec3554466c39124ca250e3effd8873bb7e8d68535aa9505d5d actions-runner-linux-x64-2.336.0.tar.gz" | sha256sum -cPrawidłowo pobrany plik powoduje wyświetlenie jednego wiersza:
actions-runner-linux-x64-2.336.0.tar.gz: OKNiekompletny lub zmodyfikowany plik powoduje wyświetlenie błędu i ostrzeżenia:
actions-runner-linux-x64-2.336.0.tar.gz: FAILED
sha256sum: WARNING: 1 computed checksum did NOT matchNie należy pomijać tego sprawdzenia i pozwalać, aby tar wykryło problem dopiero później. Niekompletne archiwum powoduje błędy gzip: stdin: unexpected end of file i tar: Unexpected EOF in archive. Informują one, że plik jest uszkodzony, ale nie wskazują, czy pobieranie zostało przerwane, czy plik został zastąpiony.
tar xzf ./actions-runner-linux-x64-2.336.0.tar.gz
lsCo zawiera archiwum tar i czego nie zawiera
Po rozpakowaniu katalog zawiera config.sh, run.sh, env.sh, safe_sleep.sh, bin/ i externals/. bin/ zawiera pliki binarne runnera oraz bin/installdependencies.sh. externals/ zawiera dołączone środowisko uruchomieniowe Node, w którym wykonywane są akcje JavaScript.
svc.sh nie istnieje jeszcze. Dokumentacja GitHub opisuje go jako skrypt „tworzony po pomyślnym dodaniu runnera”, ponieważ jest generowany na podstawie szablonu zawierającego nazwę repozytorium i runnera w nazwie usługi. Dlatego sudo ./svc.sh install przed ./config.sh kończy się błędem sudo: ./svc.sh: command not found. Najpierw należy zarejestrować runnera, a następnie zainstalować usługę.
Instalowanie zależności runnera
Runner jest aplikacją .NET, dlatego wymaga kilku bibliotek współdzielonych. Należy pozostawić powłokę użytkownika runnera i zainstalować biblioteki za pomocą sudo, ponieważ skrypt zapisuje dane w systemowej bazie pakietów.
exit
cd /home/gharunner/actions-runner
sudo ./bin/installdependencies.shW Ubuntu 24.04 zostaną zainstalowane libkrb5-3, zlib1g, liblttng-ust1t64, libssl3t64 i libicu74. Skrypt sprawdza kilka nazw wersji każdej biblioteki i zachowuje nazwę dostępną w danym wydaniu. Dzięki temu ten sam skrypt działa w starszych wersjach Ubuntu i w Debianie.
Pominięcie tego kroku powoduje, że ./config.sh kończy działanie przed wykonaniem jakichkolwiek operacji:
Dependencies is missing for Dotnet Core 6.0
Execute sudo ./bin/installdependencies.sh to install any missing Dotnet Core 6.0 dependencies.Brak libicu powoduje wyświetlenie tej samej porady, ale z innym pierwszym wierszem: Libicu's dependencies is missing for Dotnet Core 6.0. Oba komunikaty pochodzą z tego samego miejsca: config.sh uruchamia ldd dla dołączonych bibliotek przed rozpoczęciem działania. Dlatego nierozwiązane dowiązanie powoduje zatrzymanie skryptu zamiast nieczytelnego zakończenia działania w późniejszym czasie.
Rejestrowanie runnera w repozytorium
Pobierz token z repozytorium. Otwórz kolejno Settings, Actions, Runners i New self-hosted runner. Na stronie jest wyświetlany token rejestracyjny rozpoczynający się od A. Token wygasa po upływie jednej godziny od utworzenia. Wygeneruj go dopiero wtedy, gdy będzie gotowy do wklejenia.
Zarejestruj runnera jako użytkownik, który będzie go uruchamiać. config.sh nie działa z 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 \
--replaceZnaczenie flag. --name określa nazwę, pod którą runner jest wyświetlany w repozytorium. Wybierz nazwę, którą będzie można rozpoznać także za sześć miesięcy. --labels dodaje własne etykiety. Runner automatycznie otrzymuje etykiety self-hosted, Linux i X64. --work określa nazwę katalogu, w którym będą umieszczane checkouty, wewnątrz katalogu runnera. --unattended odpowiada na interaktywne monity wartościami domyślnymi. Jest to wymagane, gdy polecenie jest wykonywane w skrypcie. --replace przejmuje istniejącą rejestrację o tej samej nazwie zamiast kończyć działanie błędem. Jest to wymagane podczas ponownego tworzenia serwera.
Pomyślne wykonanie kończy się następującymi wierszami:
√ Runner successfully added
√ Runner connection is good
√ Settings Saved.Rejestracja jest teraz przechowywana w katalogu runnera jako .runner, .credentials i .credentials_rsaparams. Dwa ostatnie pliki służą do identyfikowania tego runnera przez GitHub. Każdy, kto może je odczytać, może podszyć się pod tego runnera. Z tego powodu katalog ma tryb 700, a użytkownik nie ma uprawnień sudo.
Instalacja runnera jako usługi systemd
./run.sh w terminalu sprawdza się podczas jednego testu, ale proces kończy działanie wraz z sesją SSH. Należy zainstalować usługę, aby runner uruchamiał się podczas rozruchu systemu. W artykule Usługi i liczniki systemd na VPS opisano same pliki jednostek. W tym przypadku svc.sh utworzy taki plik automatycznie.
exit
cd /home/gharunner/actions-runner
sudo ./svc.sh install gharunner
sudo ./svc.sh start
sudo ./svc.sh statussvc.sh wymaga uprawnień root, ponieważ zapisuje jednostkę w /etc/systemd/system i włącza ją. Argument po install określa użytkownika, z którego uprawnieniami działa usługa. Należy jawnie przekazać gharunner. Bez argumentu skrypt używa $SUDO_USER, czyli konta administratora. W takiej konfiguracji każde zadanie działa z uprawnieniami użytkownika, który może używać sudo.
Nazwa jednostki jest tworzona na podstawie repozytorium i runnera, w formacie actions.runner.YOUR-USER-YOUR-REPO.vps-runner-1.service. Nie trzeba wpisywać jej ręcznie:
systemctl list-units 'actions.runner.*'
sudo journalctl -u 'actions.runner.*' -n 20 --no-pagerSprawny runner zapisuje w dzienniku √ Connected to GitHub, a następnie wiersz kończący się ciągiem Listening for Jobs. Na stronie Runners repozytorium jest wtedy wyświetlany jako Idle. Runner wyświetlany jako Offline nie działa albo nie może połączyć się z GitHub przez port 443.
Przekazywanie zadania do runnera
runs-on wybiera runner na podstawie etykiety. Należy określić self-hosted oraz własną etykietę, aby zadanie nie zostało uruchomione na niewłaściwym runnerze.
name: build
on:
push:
branches: [main]
jobs:
build:
runs-on: [self-hosted, linux, vps]
steps:
- uses: actions/checkout@v5
- run: uname -aJeśli zadanie oczekuje w stanie Waiting for a runner to pick up this job, etykiety się nie zgadzają. Każda etykieta w runs-on musi istnieć na runnerze. Nawet jedno dodatkowe słowo spowoduje pozostawienie zadania w kolejce bez żadnego komunikatu o błędzie. Należy porównać tę listę z etykietami wyświetlanymi obok runnera w ustawieniach repozytorium.
Dlaczego self-hosted runners i publiczne repozytoria nie współgrają
To część, którą się pomija. Zalecenia GitHub są jednoznaczne: self-hosted runners „niemal nigdy nie powinny być używane z publicznymi repozytoriami” i „nie zapewniają uruchamiania zadań w efemerycznych, czystych maszynach wirtualnych, a niezaufany kod w workflow może trwale naruszyć ich bezpieczeństwo”.
Mechanizm jest prosty. Pull request z fork zawiera własną kopię pliku workflow. Jeśli publiczne repozytorium uruchamia workflow pull requestów na runnerze, każdy, kto może utworzyć fork repozytorium, może zaproponować workflow uruchamiający jego polecenia na VPS. Nie jest potrzebny dostęp z uprawnieniami zapisu, ponieważ uruchamiany jest właśnie proponowany kod.
Ustawienia zatwierdzania ograniczają to ryzyko, ale go nie usuwają. Domyślna zasada dla publicznego repozytorium wymaga od maintainer zatwierdzenia workflow z fork autora przesyłającego zmiany po raz pierwszy. Po jednorazowym zatwierdzeniu tej osoby jej kolejne pull requesty są uruchamiane bez ponownego pytania. Zabezpieczeniem jest więc każdorazowe sprawdzenie diff przez człowieka, a ładunek ukryty trzy poziomy niżej w skrypcie kompilacji łatwo przeoczyć.
Pull request z fork nie otrzymuje sekretów, a jego GITHUB_TOKEN jest tylko do odczytu. Ogranicza to szkody wewnątrz GitHub. Nie zapewnia ochrony serwera. Atakujący otrzymuje shell jako gharunner, więc może odczytać każdy plik dostępny dla tego użytkownika, uzyskać dostęp do wszystkiego, do czego VPS ma dostęp w sieci prywatnej, oraz pozostawić trwały element w ~/.bashrc lub w jednostce systemd użytkownika uruchamianej podczas następnego zadania.
Rejestracja za pomocą --ephemeral powoduje, że runner przyjmuje jedno zadanie, a następnie wyrejestrowuje się. Dzięki temu jedno zadanie nie może odczytać workspace kolejnego zadania. Pomaga to tylko wtedy, gdy dla każdego zadania maszyna lub kontener jest przebudowywany, ponieważ backdoor zapisany w katalogu domowym użytkownika runnera przetrwa ponowną rejestrację.
Poniższe zasady są krótkie. Self-hosted runners należy stosować z prywatnymi repozytoriami. Jeśli runner musi zostać dołączony do publicznego repozytorium, nie należy uruchamiać na nim pull requestów z fork, na serwerze nie należy przechowywać niczego innego, a maszynę należy traktować jako przeznaczoną do wymiany.
Zadania Docker i grupa, która faktycznie ma uprawnienia root
Zadania kontenerowe, kontenery usług oraz każdy krok przepływu pracy wywołujący docker build wymagają demona Docker na hoście runnera. Należy zainstalować Docker w standardowy sposób opisany w Docker i Docker Compose na VPS, a następnie dodać użytkownika runnera do grupy docker.
Przed wykonaniem tej czynności należy zrozumieć związane z nią ryzyko. Członkostwo w grupie docker jest równoważne uprawnieniom root, ponieważ kontener może zamontować / i działać jako root wewnątrz kontenera. Oznacza to, że przepływ pracy mający dostęp do gniazda Docker może odczytywać i modyfikować każdy plik na VPS, w tym /etc/shadow. W prywatnym repozytorium, do którego mają dostęp zaufani współtwórcy, może to być akceptowalny kompromis. W każdym innym przypadku eliminuje to sens używania użytkownika bez uprawnień uprzywilejowanych. Rootless Docker ogranicza kompilowanie kontenerów do uprawnień samego użytkownika runnera, ale wiąże się z wolniejszym sterownikiem pamięci masowej i brakiem kontenerów uprzywilejowanych.
Aktualizacje i prawidłowe usuwanie runnera
Self-hosted runner domyślnie aktualizuje się automatycznie. Wykrywa nową wersję, zastępuje własne pliki i ponownie uruchamia usługę, więc zazwyczaj nie trzeba wykonywać żadnych czynności. ./config.sh --disableupdate wyłącza automatyczne aktualizacje, gdy wymagana jest stała wersja. Od tego momentu aktualizacje trzeba wykonywać ręcznie. Dokumentacja GitHub jednoznacznie wskazuje, że runner skonfigurowany z użyciem --disableupdate musi być aktualizowany ręcznie.
Ręczna aktualizacja zachowuje rejestrację, ponieważ .runner i .credentials nie znajdują się w archiwum tar. Należy zatrzymać usługę, pobrać nowe archiwum tar i sprawdzić jego sumę kontrolną jako gharunner, rozpakować je do tego samego katalogu za pomocą tar xzf, a następnie ponownie uruchomić usługę:
cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh startAby usunąć runnera, należy najpierw odinstalować usługę, a następnie wyrejestrować runnera. Token usuwania znajduje się na tej samej stronie Runners, pod przyciskiem Remove danego runnera.
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_HEREUsunięcie katalogu bez wyrejestrowania runnera powoduje, że pozostaje on oznaczony w repozytorium jako Offline, ponieważ GitHub dowiaduje się o jego usunięciu dopiero wtedy, gdy runner przekaże tę informację albo administrator ręcznie usunie jego wpis.
Tryby awarii wraz z wyświetlanymi komunikatami
Must not run with sudo. config.sh wyświetla ten komunikat i kończy działanie, gdy zostanie uruchomione jako root. Sprawdzenie jest celowe, ponieważ pliki należące do root w _work powodują awarie wszystkich kolejnych zadań uruchamianych przez użytkownika usługi. Uruchom ./config.sh jako gharunner. Zmienna RUNNER_ALLOW_RUNASROOT wyłącza to sprawdzenie, ale jej użycie tylko odracza wystąpienie awarii.
sudo: ./svc.sh: command not found. Znajdujesz się we właściwym katalogu. svc.sh jeszcze nie istnieje, ponieważ config.sh nie zakończyło rejestracji. Zarejestruj runner, a następnie zainstaluj usługę.
Http response code: NotFound from 'POST https://api.github.com/actions/runner-registration'. Token nie jest prawidłowym tokenem rejestracji. Wygasł, ponieważ jego ważność wynosi tylko jedną godzinę, albo w miejsce tokenu rejestracji ze strony Runners wklejono personal access token. Wygeneruj nowy token i wklej go ponownie.
Dependencies is missing for Dotnet Core 6.0. Uruchom sudo ./bin/installdependencies.sh z katalogu runner jako root, a następnie ponów rejestrację.
Runner Offline po ponownym uruchomieniu. Uruchom systemctl is-enabled 'actions.runner.*'. Jeśli nic nie zostanie wyświetlone, ./svc.sh install nie zostało uruchomione, więc runner istniał tylko w ramach sesji terminala. Jeśli jednostka jest włączona, a runner nadal ma stan Offline, odczytaj journalctl -u 'actions.runner.*' i sprawdź wychodzące połączenie HTTPS.
Dysk się zapełnia. Kopie robocze, pamięci podręczne kompilacji i obrazy Docker gromadzą się w _work oraz w katalogu domowym użytkownika runner, a system nie usuwa ich automatycznie. Monitoruj du -sh /home/gharunner/actions-runner/_work i dodaj zaplanowane czyszczenie, zanim dysk zapełni się samoczynnie.
FAQ
Dlaczego sudo ./svc.sh install zwraca komunikat command not found?
Ponieważ svc.sh nie znajduje się w archiwum tarball runnera. Jest generowany w katalogu runnera po zakończeniu rejestracji przez ./config.sh. Do utworzenia nazwy usługi używane są nazwa repozytorium i nazwa runnera. Najpierw uruchom ./config.sh jako użytkownik runnera. Następnie sudo ./svc.sh install gharunner znajdzie skrypt i zapisze jednostkę o nazwie actions.runner.OWNER-REPO.RUNNER-NAME.service w katalogu /etc/systemd/system.
Czy w przypadku self-hosted runnera trzeba otworzyć port zapory sieciowej?
Nie. Runner otwiera wychodzące połączenie HTTPS z GitHub i utrzymuje je podczas oczekiwania na zadania. GitHub nigdy nie inicjuje połączenia z VPS. Zezwól na ruch wychodzący przez port 443 i pozostaw reguły ruchu przychodzącego zamknięte. Jeśli runner wyświetla stan Offline, mimo że jego usługa działa, sprawdź filtrowanie ruchu wychodzącego i DNS, a nie reguły ruchu przychodzącego.
Czy można używać self-hosted runnera w publicznym repozytorium?
Tak, ale GitHub odradza takie rozwiązanie. Pull request z forka zawiera własny plik workflow, dlatego każda osoba, która może utworzyć fork repozytorium, może zaproponować polecenia uruchamiane na danym komputerze. Monit o zatwierdzenie obejmuje tylko pierwsze uruchomienie wykonane przez współtwórcę. Jeśli runner zostanie dołączony do publicznego repozytorium, należy wyłączyć w nim workflow pull requestów z forków, nie przechowywać na tym serwerze żadnych innych danych oraz regularnie odtwarzać system.
Dlaczego rejestracja kończy się błędem Http response code: NotFound?
Wywołanie rejestracji zwraca NotFound, gdy dane uwierzytelniające są nieprawidłowe, a nie tylko wtedy, gdy adres URL jest nieprawidłowy. Z tego powodu komunikat może wprowadzać w błąd. Tokeny rejestracyjne wygasają po upływie jednej godziny od wyświetlenia. Personal access token nie jest akceptowany w tym wywołaniu. Ponownie otwórz Settings, Actions, Runners, New self-hosted runner, skopiuj nowy token i sprawdź, czy wartość --url wskazuje repozytorium, do którego masz uprawnienia administratora.