Jak zainstalować self-hosted runner GitHub Actions na VPS
Instrukcja konfiguracji runnera na Ubuntu 24.04. Opisano tworzenie dedykowanego użytkownika, weryfikację sum kontrolnych, skrypt config.sh oraz usługę systemd. Uwaga na ryzyko.
Czym jest self-hosted GitHub Actions runner
Self-hosted GitHub Actions runner to program instalowany na własnym VPS, który pobiera zadania z GitHub i wykonuje je na posiadanym sprzęcie. Rejestruje się go dla konkretnego repozytorium, instaluje jako usługę systemd, dzięki czemu uruchamia się automatycznie po każdym restarcie systemu. GitHub planuje zadanie, a serwer wykonuje pracę.
CI (ciągła integracja) na własnej maszynie jest opłacalna z dwóch powodów. Minuty budowania przestają być limitowane, a zadanie może uzyskać dostęp do zasobów dostępnych tylko lokalnie, takich jak rozgrzana pamięć podręczna budowania czy sieć prywatna. Ceną jest bezpieczeństwo. Runner wykonuje wszystko, co zawiera plik workflow, działając jako użytkownik, któremu nadano uprawnienia. Z założenia plik workflow umożliwia zdalne wykonanie kodu (remote code execution). W przypadku prywatnego repozytorium jest to akceptowalne, ponieważ tylko zaufane osoby mogą dodawać pliki. W repozytorium publicznym stanowi to realne zagrożenie, a sekcja dotycząca fork pull requests wyjaśnia ten mechanizm.
Wszystkie poniższe instrukcje dotyczą Ubuntu 24.04 oraz runner w wersji 2.338.0, która jest aktualnym wydaniem na dzień 12 października 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.
Pobranie runnera i weryfikacja archiwum
Od tego momentu pracuj jako użytkownik 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"Jeśli nie masz pewności co do architektury, najpierw uruchom uname -m. x86_64 przyjmuje plik linux-x64 wskazany powyżej. aarch64 przyjmuje actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz.
Teraz zweryfikuj pobrany plik. Poniższa suma kontrolna SHA256 (secure hash algorithm, 256 bit) dotyczy archiwum w wersji 2.338.0 dla architektury x64. GitHub wyświetla wartość dla bieżącego wydania na stronie wydań oraz na ekranie dodawania nowego self-hosted runnera. Wartość ta zmienia się wraz z każdą wersją, dlatego należy skopiować ją z odpowiedniego miejsca podczas instalacji innej wersji.
echo "af4b794c1bc41d73d40535e3fe092a39f9679cd8d965954c2aca25a05ca41d32 actions-runner-linux-x64-2.338.0.tar.gz" | sha256sum -cPoprawne pobranie skutkuje wyświetleniem jednego wiersza:
actions-runner-linux-x64-2.338.0.tar.gz: OKUszkodzony lub zmodyfikowany plik spowoduje wyświetlenie błędu oraz ostrzeżenia:
actions-runner-linux-x64-2.338.0.tar.gz: FAILED
sha256sum: WARNING: 1 computed checksum did NOT matchNie pomijaj weryfikacji, pozwalając, aby to tar wykryło problem. Niekompletne archiwum kończy się błędem gzip: stdin: unexpected end of file oraz tar: Unexpected EOF in archive, co informuje o uszkodzeniu pliku, ale nie wskazuje, czy został on ucięty, czy podmieniony.
tar xzf ./actions-runner-linux-x64-2.338.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ę.
Instalacja zależności runnera
Runner jest aplikacją .NET, dlatego wymaga kilku bibliotek współdzielonych. Należy opuścić powłokę użytkownika runnera i zainstalować je za pomocą sudo, ponieważ skrypt zapisuje dane w systemowej bazie pakietów.
exit
cd /home/gharunner/actions-runner
sudo ./bin/installdependencies.shW systemie Ubuntu 24.04 pobierane są libkrb5-3, zlib1g, liblttng-ust1t64, libssl3t64 oraz libicu74. Skrypt sprawdza kilka nazw wersji dla każdej biblioteki i zachowuje tę, którą dostarcza używane wydanie systemu, dzięki czemu ten sam skrypt działa zarówno na starszych wersjach Ubuntu, jak i na Debianie.
Jeśli ten krok zostanie pominięty, a jedna z bibliotek będzie brakować, ./config.sh zatrzyma się przed wykonaniem jakichkolwiek działań:
Dependencies is missing for Dotnet Core 6.0
Execute sudo ./bin/installdependencies.sh to install any missing Dotnet Core 6.0 dependencies.Brak libicu skutkuje wyświetleniem tej samej porady pod inną pierwszą linią, Libicu's dependencies is missing for Dotnet Core 6.0. Obie pochodzą z tego samego źródła: config.sh sprawdza dołączone biblioteki za pomocą ldd, a libicu za pomocą ldconfig przed uruchomieniem, dzięki czemu nierozwiązane dowiązanie zatrzymuje skrypt, zamiast powodować mylącą awarię w późniejszym czasie.
Rejestracja runnera w repozytorium
Pobierz token z repozytorium. Otwórz Settings, następnie Actions, Runners i New self-hosted runner. Strona wyświetli token rejestracyjny. Wygasa on po godzinie od wygenerowania, więc utwórz go dopiero w momencie, gdy będziesz gotowy do wklejenia.
Rejestrację przeprowadź jako użytkownik runner. config.sh odmawia uruchomienia z uprawnieniami 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 \
--replaceDziałanie tych flag: --name określa nazwę, pod jaką runner będzie widoczny w repozytorium, więc wybierz nazwę, którą rozpoznasz po sześciu miesiącach. --labels dodaje własne etykiety; runner automatycznie posiada już self-hosted, Linux oraz X64. --work definiuje nazwę katalogu, w którym będą umieszczane pobrane pliki (checkout), wewnątrz katalogu runnera. --unattended automatycznie zatwierdza domyślne odpowiedzi na monity interaktywne, co jest pożądane, gdy polecenie jest częścią skryptu. --replace przejmuje istniejącą rejestrację o tej samej nazwie zamiast zwracać błąd, co jest przydatne podczas przebudowy serwera.
Poprawne zakończenie procesu kończy się wyświetleniem poniższych linii:
√ Runner successfully added
√ Runner connection is good
√ Settings Saved.Rejestracja jest zapisana w katalogu runnera w plikach .runner, .credentials oraz .credentials_rsaparams. Dwa ostatnie pliki identyfikują runnera w GitHub, więc każdy, kto ma do nich dostęp, może podszyć się pod tę instancję. Z tego powodu katalog posiada uprawnienia 700, a użytkownik nie posiada 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 powinny być łączone
To jest część, którą ludzie pomijają. Wytyczne GitHub są jednoznaczne: self-hosted runners „prawie nigdy nie powinny być używane w publicznych repozytoriach” i „nie gwarantują uruchomienia w efemerycznych, czystych maszynach wirtualnych, przez co mogą zostać trwale przejęte przez niezaufany kod w workflow”.
Mechanizm jest prosty. Pull request z forka zawiera własną kopię pliku workflow. Jeśli publiczne repozytorium uruchamia workflowy pull requestów na Twoim runnerze, każdy, kto może zrobić fork repozytorium, może zaproponować workflow wykonujący jego polecenia na Twoim VPS. Nie potrzebuje uprawnień do zapisu, ponieważ to, co proponuje, jest właśnie tym, co zostaje uruchomione.
Ustawienia zatwierdzania łagodzą ten problem, ale go nie rozwiązują. Domyślna polityka dla publicznego repozytorium wymaga od opiekuna zatwierdzenia workflowu forka od nowego kontrybutora. GitHub uznaje użytkownika za nowego tylko do momentu, aż jakikolwiek jego commit lub pull request zostanie scalony z repozytorium. Po tym fakcie kolejne pull requesty uruchamiają się bez pytania, a sam GitHub ostrzega, że wystarczy drobna scalona poprawka, aby osiągnąć ten stan. Bramką jest więc człowiek czytający diff, a ładunek ukryty trzy poziomy głębiej w skrypcie budowania jest łatwy do przeoczenia.
Pull request z forka nie otrzymuje Twoich sekretów, a jego GITHUB_TOKEN jest tylko do odczytu. Ogranicza to szkody wewnątrz GitHub. Nie chroni to jednak Twojego serwera. Atakujący uzyskuje powłokę jako gharunner, więc może odczytać każdy plik dostępny dla tego użytkownika, uzyskać dostęp do wszystkiego, co VPS widzi w sieci prywatnej, oraz zostawić coś w ~/.bashrc lub w jednostce systemd użytkownika, która uruchomi się podczas następnego zadania.
Rejestracja z --ephemeral sprawia, że runner przyjmuje jedno zadanie, a następnie wyrejestrowuje się, więc jedno zadanie nie może odczytać obszaru roboczego kolejnego. Pomaga to tylko wtedy, gdy coś przebudowuje maszynę lub kontener dla każdego zadania, ponieważ backdoor zapisany w katalogu domowym użytkownika runnera przetrwa ponowną rejestrację.
Poniższe zasady są krótkie. Używaj self-hosted runners dla prywatnych repozytoriów. Jeśli musisz podłączyć taki runner do publicznego repozytorium, nie uruchamiaj na nim pull requestów z forków, nie przechowuj na tym serwerze niczego innego i traktuj maszynę jako jednorazową.
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 poprawne usuwanie runnera
Self-hosted runner aktualizuje się domyślnie automatycznie. Wykrywa nową wersję, zastępuje własne pliki i restartuje usługę, więc zazwyczaj nie wymaga żadnych działań. Aby wyłączyć automatyczną aktualizację, należy przekazać flagę --disableupdate do ./config.sh podczas rejestracji runnera. W przypadku już zarejestrowanego runnera oznacza to konieczność ponownej rejestracji z użyciem tej flagi. Nie pozwala to jednak na trwałe pozostanie przy jednej wersji. Dokumentacja GitHub wyraźnie wskazuje, że runner skonfigurowany z --disableupdate nadal musi być aktualizowany ręcznie w ciągu 30 dni od wydania nowej wersji, w przeciwnym razie GitHub przestanie przydzielać mu zadania.
Ręczna aktualizacja zachowuje rejestrację, ponieważ pliki .runner oraz .credentials nie znajdują się w archiwum tarball. Należy zatrzymać usługę, pobrać i zweryfikować sumę kontrolną nowego archiwum jako gharunner, wypakować je w tym samym 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, najpierw odinstaluj usługę, a następnie wyrejestruj ją. 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 powoduje, że runner pozostaje widoczny jako Offline w repozytorium. Pozostaje on na liście, dopóki administrator nie kliknie Force remove this runner lub dopóki GitHub nie usunie go automatycznie po 14 dniach braku połączenia.
Tryby awarii i towarzyszące im komunikaty
Must not run with sudo. config.sh wyświetla ten komunikat i kończy działanie, jeśli jest uruchamiany jako root. Sprawdzenie to jest celowe, ponieważ pliki należące do roota w _work uniemożliwiają poprawne działanie wszystkich późniejszych zadań wykonywanych na koncie użytkownika usługi. Uruchom ./config.sh jako gharunner. Zmienna RUNNER_ALLOW_RUNASROOT pozwala pominąć to sprawdzenie, jednak jej użycie jedynie przesuwa wystąpienie błędu w czasie.
sudo: ./svc.sh: command not found. Znajdujesz się w poprawnym katalogu. svc.sh jeszcze nie istnieje, ponieważ config.sh nie zakończył procesu rejestracji. Zarejestruj runnera, a następnie zainstaluj usługę.
Http response code: NotFound from 'POST https://api.github.com/actions/runner-registration'. Zazwyczaj token nie jest poprawnym tokenem rejestracyjnym. Albo wygasł (tokeny są ważne przez godzinę), albo wklejono osobisty token dostępu (personal access token) zamiast tokena rejestracyjnego ze strony Runners. Wygeneruj nowy token, wklej go ponownie i sprawdź, czy --url zawiera pełny adres URL repozytorium.
Dependencies is missing for Dotnet Core 6.0. Uruchom sudo ./bin/installdependencies.sh z katalogu runnera jako root, a następnie zarejestruj go ponownie.
Runner w stanie Offline po restarcie. Uruchom systemctl is-enabled 'actions.runner.*'. Jeśli lista jest pusta, ./svc.sh install nigdy nie został uruchomiony, co oznacza, że runner istniał tylko w ramach sesji terminala. Jeśli jednostka jest włączona, a runner nadal pozostaje w stanie Offline, zapoznaj się z journalctl -u 'actions.runner.*' i sprawdź ruch wychodzący HTTPS.
Zapełnienie dysku. Dane z checkoutów, cache budowania oraz obrazy Docker gromadzą się w _work oraz w katalogu domowym użytkownika runnera, a system nie usuwa ich automatycznie. Monitoruj du -sh /home/gharunner/actions-runner/_work i skonfiguruj cykliczne czyszczenie, zanim brak miejsca na dysku wymusi przerwanie pracy.
FAQ
Dlaczego sudo ./svc.sh install zwraca błąd command not found?
Ponieważ svc.sh nie znajduje się w archiwum tarball runnera. Skrypt jest generowany w katalogu runnera po zakończeniu rejestracji przez ./config.sh, z wykorzystaniem nazwy repozytorium oraz nazwy runnera do utworzenia nazwy usługi. Uruchom ./config.sh jako użytkownik runnera. Następnie sudo ./svc.sh install gharunner odnajdzie skrypt i zapisze jednostkę o nazwie actions.runner.OWNER-REPO.RUNNER-NAME.service w /etc/systemd/system.
Czy muszę otwierać port w zaporze dla self-hosted runnera?
Nie. Runner otwiera wychodzące połączenie HTTPS do GitHub i utrzymuje je, oczekując na zadania, więc GitHub nigdy nie inicjuje połączenia z Twoim VPS. Zezwól na ruch wychodzący na port 443 i pozostaw reguły przychodzące zamknięte. Jeśli runner wykazuje stan Offline mimo działającej usługi, sprawdź filtrowanie ruchu wychodzącego oraz DNS, zamiast reguł przychodzących.
Czy mogę użyć self-hosted runnera w publicznym repozytorium?
Można, jednak GitHub odradza takie rozwiązanie. Pull request z forka zawiera własny plik workflow, więc każda osoba mogąca wykonać fork Twojego repozytorium może zaproponować polecenia wykonywane na Twojej maszynie. Domyślny monit o zatwierdzenie obejmuje tylko kontrybutorów, których commit lub pull request nie został wcześniej scalony z repozytorium. Jeśli przypiszesz runnera do publicznego repozytorium, wyłącz na nim obsługę workflow dla pull requestów z forków, nie przechowuj na tym serwerze żadnych innych danych i regularnie odtwarzaj maszynę.
Dlaczego rejestracja kończy się błędem Http response code: NotFound?
Wywołanie rejestracji zwraca NotFound, gdy poświadczenia są błędne, a nie tylko w przypadku nieprawidłowego adresu URL, co czyni ten komunikat mylącym. Tokeny rejestracyjne wygasają godzinę po ich wygenerowaniu, a personal access token nie jest akceptowany w tym wywołaniu. Otwórz ponownie Settings, Actions, Runners, New self-hosted runner, skopiuj nowy token i upewnij się, że wartość --url wskazuje na repozytorium, w którym posiadasz uprawnienia administratora.