Cài GitHub Actions runner self-hosted trên VPS
Hướng dẫn Ubuntu 24.04 đăng ký self-hosted GitHub Actions runner với user riêng, checksum, config.sh, systemd và rủi ro fork pull request.
Self-hosted GitHub Actions runner thực hiện gì
Self-hosted GitHub Actions runner là một chương trình bạn cài trên VPS của mình. Nó yêu cầu GitHub cung cấp các job rồi chạy các job đó trên phần cứng của bạn. Bạn đăng ký runner với một repository, cài runner dưới dạng systemd service, và runner sẽ khởi động lại sau mỗi lần reboot. GitHub lên lịch cho job. Server của bạn thực hiện công việc.
CI (continuous integration) trên server bạn sở hữu có giá trị vì 2 lý do. Build minutes không còn bị tính theo mức sử dụng, và job có thể truy cập những tài nguyên chỉ máy của bạn có, chẳng hạn build cache đã được làm nóng hoặc private network. Đổi lại là vấn đề bảo mật. Runner thực thi mọi lệnh được workflow file chỉ định, với tư cách user mà bạn cấp cho runner, nên workflow file về bản chất được thiết kế để thực thi mã từ xa. Với private repository, điều này không vấn đề vì chỉ những người bạn tin cậy mới có thể thêm workflow file. Với public repository, đây là rủi ro thực sự. Phần về fork pull request giải thích cơ chế này.
Toàn bộ nội dung dưới đây sử dụng Ubuntu 24.04 với runner version 2.336.0, là bản release hiện tại vào tháng 7 năm 2026.
Những gì bạn cần trước khi bắt đầu
Bắt đầu với một VPS có tài khoản admin thông thường và quyền sudo, ở trạng thái đạt được trong mười phút đầu tiên trên một VPS mới. Bạn không cần mở port inbound. Runner mở một kết nối HTTPS (hypertext transfer protocol secure) outbound đến GitHub và giữ kết nối này trong khi chờ công việc, vì vậy GitHub không bao giờ kết nối đến server của bạn. Firewall có thể tiếp tục chặn kết nối từ bên ngoài, nhưng các job vẫn được gửi đến.
Bạn cũng cần quyền admin trên repository, vì registration token được hiển thị trong phần cài đặt repository.
Tạo user riêng cho runner
Không bao giờ chạy runner bằng root hoặc user quản trị của bạn. Mỗi job kế thừa quyền của user chạy runner, vì vậy workflow gọi sudo sẽ chạy thành công nếu user chạy runner có thể dùng sudo. Tạo một user không có đặc quyền và chỉ sở hữu thư mục home của chính nó. Tài khoản user theo nguyên tắc đặc quyền tối thiểu trên VPS trình bày mẫu triển khai tổng quát. Dưới đây là cấu hình cụ thể.
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 khóa mật khẩu, nên không ai có thể dùng mật khẩu đó để đăng nhập dưới tên gharunner. Quyền 700 trên thư mục runner rất quan trọng vì runner lưu credentials ở đó dưới dạng plain text, và một checkout có thể chứa source riêng tư.
Kiểm tra cả hai thuộc tính trước khi tiếp tục:
sudo passwd -S gharunner
sudo -l -U gharunnerpasswd -S in ra một dòng bắt đầu bằng gharunner L, trong đó L nghĩa là mật khẩu đã bị khóa. sudo -l -U gharunner phải trả về is not allowed to run sudo. Nếu lệnh in ra danh sách các command được phép thay vì vậy, account đã thuộc một sudo group và cơ chế cô lập bạn vừa tạo không còn hiệu lực.
Tải runner và kiểm tra tarball
Từ đây, hãy làm việc bằng user 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"Chạy uname -m trước nếu bạn không chắc về kiến trúc. x86_64 sử dụng file linux-x64 ở trên. aarch64 sử dụng actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz.
Bây giờ hãy xác minh file đã tải xuống. SHA256 (secure hash algorithm, 256 bit) bên dưới dùng cho tarball x64 phiên bản 2.336.0. GitHub hiển thị giá trị của release hiện tại trên trang release và màn hình New self-hosted runner. Giá trị này thay đổi theo từng phiên bản, vì vậy hãy sao chép giá trị từ đó khi cài phiên bản khác.
echo "04cf0be1aff4c3ec3554466c39124ca250e3effd8873bb7e8d68535aa9505d5d actions-runner-linux-x64-2.336.0.tar.gz" | sha256sum -cNếu tải xuống thành công, lệnh sẽ in một dòng:
actions-runner-linux-x64-2.336.0.tar.gz: OKFile bị cắt ngắn hoặc đã bị thay đổi sẽ in lỗi và cảnh báo:
actions-runner-linux-x64-2.336.0.tar.gz: FAILED
sha256sum: WARNING: 1 computed checksum did NOT matchKhông được bỏ qua bước kiểm tra rồi để tar phát hiện lỗi. Archive được ghi dở sẽ lỗi với gzip: stdin: unexpected end of file và tar: Unexpected EOF in archive. Các lỗi này cho biết file bị hỏng, nhưng không cho biết file bị cắt ngắn hay đã bị thay thế.
tar xzf ./actions-runner-linux-x64-2.336.0.tar.gz
lsTarball chứa gì và không chứa gì
Sau khi giải nén, thư mục chứa config.sh, run.sh, env.sh, safe_sleep.sh, bin/ và externals/. bin/ chứa các runner binary và bin/installdependencies.sh. externals/ chứa Node runtime đi kèm, được các action JavaScript thực thi.
Hiện chưa có svc.sh. Tài liệu GitHub mô tả đây là script “được tạo sau khi thêm runner thành công”, vì script này được ghi từ một template có tên repository và tên runner được đưa vào tên service. Vì vậy, chạy sudo ./svc.sh install trước ./config.sh sẽ thất bại với sudo: ./svc.sh: command not found. Trước tiên hãy đăng ký runner, sau đó cài service.
Cài đặt các dependency của runner
Runner là một ứng dụng .NET nên cần một số shared library. Giữ nguyên shell của user chạy runner và cài đặt chúng bằng sudo, vì script ghi vào cơ sở dữ liệu package của hệ thống.
exit
cd /home/gharunner/actions-runner
sudo ./bin/installdependencies.shTrên Ubuntu 24.04, lệnh này cài libkrb5-3, zlib1g, liblttng-ust1t64, libssl3t64 và libicu74. Script thử nhiều tên phiên bản cho từng library và giữ lại tên mà bản release của bạn cung cấp. Vì vậy, cùng một script vẫn hoạt động trên các bản Ubuntu cũ hơn và trên Debian.
Bỏ qua bước này thì ./config.sh sẽ dừng trước khi thực hiện bất kỳ thao tác nào:
Dependencies is missing for Dotnet Core 6.0
Execute sudo ./bin/installdependencies.sh to install any missing Dotnet Core 6.0 dependencies.Thiếu libicu cũng cho cùng hướng dẫn đó nhưng dòng đầu tiên khác là Libicu's dependencies is missing for Dotnet Core 6.0. Cả hai lỗi đều xuất phát từ cùng một nguyên nhân: config.sh chạy ldd trên các library đi kèm trước khi khởi động. Vì vậy, một liên kết không được resolve sẽ khiến script dừng thay vì gây ra lỗi crash khó hiểu về sau.
Đăng ký runner với repository
Lấy một token từ repository. Mở Settings, sau đó chọn Actions, Runners rồi New self-hosted runner. Trang này hiển thị một registration token bắt đầu bằng A. Token hết hạn sau một giờ kể từ khi được tạo, vì vậy hãy tạo token khi bạn sẵn sàng dán token vào lệnh.
Đăng ký bằng user chạy runner. config.sh sẽ từ chối chạy dưới 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 \
--replaceCông dụng của các flag. --name là tên runner hiển thị trong repository, vì vậy hãy chọn tên mà bạn vẫn nhận ra sau sáu tháng. --labels thêm các label tùy chỉnh; runner đã có sẵn self-hosted, Linux và X64 mà không cần chỉ định. --work đặt tên cho thư mục chứa các bản checkout, nằm bên trong thư mục runner. --unattended trả lời các prompt tương tác bằng giá trị mặc định, phù hợp khi lệnh nằm trong một script. --replace tiếp quản registration hiện có cùng tên thay vì báo lỗi, phù hợp khi bạn dựng lại server.
Lần chạy thành công kết thúc bằng các dòng sau:
√ Runner successfully added
√ Runner connection is good
√ Settings Saved.Thông tin registration hiện được lưu trong thư mục runner dưới dạng .runner, .credentials và .credentials_rsaparams. Hai tệp cuối dùng để xác thực runner này với GitHub, nên bất kỳ ai có thể đọc chúng đều có thể mạo danh runner. Vì lý do đó, thư mục có mode 700 và user này không có quyền sudo.
Cài runner dưới dạng dịch vụ systemd
Chạy ./run.sh trong terminal phù hợp cho một lần kiểm tra, nhưng tiến trình sẽ dừng khi phiên SSH kết thúc. Cài dịch vụ để runner tự khởi động khi boot. dịch vụ và timer systemd trên VPS giải thích chi tiết về các unit file. Ở đây, svc.sh sẽ tạo unit file cho bạn.
exit
cd /home/gharunner/actions-runner
sudo ./svc.sh install gharunner
sudo ./svc.sh start
sudo ./svc.sh statussvc.sh yêu cầu root vì lệnh này ghi một unit vào /etc/systemd/system và enable unit đó. Đối số sau install là user mà dịch vụ chạy bằng quyền của user đó. Hãy truyền rõ gharunner. Nếu không có đối số, script sẽ dùng $SUDO_USER, tức tài khoản quản trị của bạn. Khi đó, mọi job sẽ chạy bằng user có thể dùng sudo.
Tên unit được tạo từ repository và runner, theo dạng actions.runner.YOUR-USER-YOUR-REPO.vps-runner-1.service. Bạn không cần tự nhập tên này:
systemctl list-units 'actions.runner.*'
sudo journalctl -u 'actions.runner.*' -n 20 --no-pagerRunner hoạt động bình thường sẽ ghi log √ Connected to GitHub, sau đó là một dòng kết thúc bằng Listening for Jobs. Trang Runners của repository sẽ hiển thị runner ở trạng thái Idle. Runner hiển thị Offline nghĩa là runner không chạy hoặc không thể kết nối đến GitHub qua port 443.
Gửi một job đến runner
runs-on chọn runner theo label. Hãy yêu cầu self-hosted cùng label của bạn, để job không thể chạy trên runner mà bạn không chủ định chọn.
name: build
on:
push:
branches: [main]
jobs:
build:
runs-on: [self-hosted, linux, vps]
steps:
- uses: actions/checkout@v5
- run: uname -aNếu job chờ ở Waiting for a runner to pick up this job, các label không khớp. Mọi label trong runs-on phải tồn tại trên runner. Chỉ cần thừa một từ là job sẽ nằm trong hàng đợi mà không có lỗi ở bất kỳ đâu. So sánh danh sách này với các label hiển thị bên cạnh runner trong phần cài đặt repository.
Vì sao self-hosted runner và repository công khai không thể dùng chung
Đây là phần nhiều người bỏ qua. Hướng dẫn của GitHub nêu rất rõ: self-hosted runner “gần như không bao giờ nên được dùng cho repository công khai”, vì chúng “không đảm bảo workflow chạy trong các máy ảo tạm thời, sạch và có thể bị mã độc từ code không đáng tin cậy trong workflow xâm nhập lâu dài”.
Cơ chế này rất đơn giản. Một pull request từ fork chứa bản sao riêng của file workflow. Nếu repository công khai của bạn chạy workflow pull request trên runner của bạn, bất kỳ ai có thể fork repository đều có thể đề xuất một workflow chạy lệnh của họ trên VPS của bạn. Họ không cần quyền ghi, vì chính nội dung họ đề xuất sẽ được chạy.
Cài đặt phê duyệt chỉ giảm rủi ro mà không giải quyết vấn đề. Chính sách mặc định cho repository công khai yêu cầu maintainer phê duyệt workflow từ fork của người đóng góp lần đầu. Sau khi bạn phê duyệt người đó một lần, các pull request sau của họ sẽ chạy mà không cần yêu cầu mới. Vì vậy, lớp kiểm soát là một người phải đọc diff mỗi lần. Payload ẩn sâu ba cấp trong build script rất dễ bị bỏ sót.
Một pull request từ fork không nhận được secrets của bạn và GITHUB_TOKEN của nó là chỉ đọc. Điều này giới hạn thiệt hại bên trong GitHub. Nó không bảo vệ server của bạn. Attacker có shell dưới quyền gharunner. Vì vậy, họ có thể đọc mọi file mà user đó đọc được, truy cập mọi thứ VPS có thể truy cập qua private network, và để lại backdoor trong ~/.bashrc hoặc trong một user systemd unit được chạy ở job tiếp theo.
Đăng ký với --ephemeral khiến runner chấp nhận một job rồi deregister, nên một job không thể đọc workspace của job tiếp theo. Cách này chỉ hữu ích nếu có tiến trình rebuild machine hoặc container cho từng job, vì backdoor được ghi vào home directory của runner user vẫn tồn tại sau khi đăng ký lại.
Các quy tắc sau rất ngắn. Dùng self-hosted runner cho repository riêng tư. Nếu bắt buộc phải gắn một runner vào repository công khai, không chạy pull request từ fork trên runner đó, không đặt thêm bất kỳ thứ gì khác trên server, và coi machine này là disposable.
Docker job và group thực sự có quyền root
Job container, service container và mọi workflow step gọi docker build đều cần Docker daemon trên host runner. Cài Docker theo cách thông thường, như hướng dẫn Docker và Docker Compose trên VPS, sau đó thêm user runner vào group docker.
Hiểu rõ đánh đổi trước khi thực hiện. Thành viên của group docker tương đương với root, vì một container có thể bind mount / và chạy dưới quyền root bên trong container đó. Vì vậy, workflow có thể truy cập Docker socket sẽ đọc và ghi được mọi file trên VPS, bao gồm /etc/shadow. Với repository private và các contributor đáng tin cậy, đây có thể là một đánh đổi chấp nhận được. Trong các trường hợp khác, điều này làm mất ý nghĩa của user không có đặc quyền. Rootless Docker giữ các bản build container trong phạm vi quyền của chính user runner, nhưng storage driver chậm hơn và không hỗ trợ container có đặc quyền.
Cập nhật và gỡ runner đúng cách
Self-hosted runner mặc định tự cập nhật. Runner phát hiện bản phát hành mới, thay thế các tệp của chính nó rồi khởi động lại service, nên thông thường bạn không cần làm gì. ./config.sh --disableupdate tắt cơ chế tự cập nhật khi bạn cần cố định một phiên bản. Sau đó, bạn phải tự cập nhật: tài liệu của GitHub nêu rõ runner được cấu hình với --disableupdate phải được cập nhật thủ công.
Cập nhật thủ công vẫn giữ nguyên thông tin đăng ký, vì .runner và .credentials không nằm trong tarball. Dừng service, tải xuống và kiểm tra checksum của tarball mới bằng gharunner, giải nén đè lên cùng thư mục bằng tar xzf, rồi khởi động lại service:
cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh startĐể gỡ runner, trước tiên hãy gỡ cài đặt service, sau đó hủy đăng ký runner. Removal token lấy từ cùng trang Runners, trong nút Remove của 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_HEREXóa thư mục mà không hủy đăng ký sẽ khiến runner vẫn hiển thị là Offline trong repository, vì GitHub chỉ biết runner đã bị xóa khi runner tự báo trạng thái đó hoặc admin xóa entry theo cách thủ công.
Các chế độ lỗi và chuỗi bạn sẽ thấy
Must not run with sudo. config.sh in chuỗi này rồi thoát khi được chạy dưới tài khoản root. Việc kiểm tra này là có chủ đích, vì các file thuộc sở hữu của root trong _work sẽ làm hỏng mọi job tiếp theo chạy dưới tài khoản dịch vụ. Chạy ./config.sh dưới tài khoản gharunner. Biến RUNNER_ALLOW_RUNASROOT ghi đè việc kiểm tra này, nhưng dùng nó chỉ khiến lỗi xảy ra muộn hơn.
sudo: ./svc.sh: command not found. Bạn đang ở đúng thư mục. svc.sh chưa tồn tại vì config.sh chưa hoàn tất việc đăng ký. Hãy đăng ký runner, sau đó cài đặt service.
Http response code: NotFound from 'POST https://api.github.com/actions/runner-registration'. Token không phải là registration token hợp lệ. Token có thể đã hết hạn vì chỉ có hiệu lực trong một giờ, hoặc bạn đã dán personal access token thay cho registration token trên trang Runners. Tạo token mới rồi dán lại.
Dependencies is missing for Dotnet Core 6.0. Chạy sudo ./bin/installdependencies.sh từ thư mục của runner dưới tài khoản root, sau đó đăng ký lại.
Runner Offline sau khi reboot. Chạy systemctl is-enabled 'actions.runner.*'. Nếu không có gì được liệt kê, ./svc.sh install chưa từng được chạy, nên runner chỉ tồn tại trong phiên terminal của bạn. Nếu unit đã được enable mà runner vẫn Offline, đọc journalctl -u 'actions.runner.*' và kiểm tra HTTPS outbound.
Ổ đĩa đầy. Checkout, build cache và Docker image tích tụ trong _work và thư mục home của người dùng chạy runner; không có cơ chế nào tự dọn chúng. Theo dõi du -sh /home/gharunner/actions-runner/_work và thêm tác vụ dọn dẹp theo lịch trước khi ổ đĩa tự quyết định thay bạn.
FAQ
Tại sao sudo ./svc.sh install báo không tìm thấy lệnh?
Vì svc.sh không có trong tarball của runner. Nó được tạo trong thư mục runner sau khi ./config.sh đăng ký xong, bằng cách dùng tên repository và runner của bạn để tạo tên service. Trước tiên, hãy chạy ./config.sh bằng user chạy runner. Sau đó, sudo ./svc.sh install gharunner sẽ tìm thấy script và ghi một unit có tên actions.runner.OWNER-REPO.RUNNER-NAME.service vào /etc/systemd/system.
Tôi có cần mở port trên firewall cho runner tự lưu trữ không?
Không. Runner mở kết nối HTTPS outbound đến GitHub và giữ kết nối này trong khi chờ job, vì vậy GitHub không bao giờ khởi tạo kết nối đến VPS của bạn. Cho phép outbound 443 và giữ các rule inbound ở trạng thái đóng. Nếu runner hiển thị Offline trong khi service vẫn đang chạy, hãy kiểm tra bộ lọc outbound và DNS thay vì kiểm tra các rule inbound.
Tôi có thể dùng runner tự lưu trữ cho repository công khai không?
Bạn có thể, nhưng GitHub khuyến cáo không nên làm vậy. Pull request từ fork chứa file workflow riêng, nên bất kỳ ai có thể fork repository của bạn đều có thể đề xuất các lệnh chạy trên máy của bạn. Prompt phê duyệt chỉ áp dụng cho lần chạy đầu tiên của contributor. Nếu gắn runner vào repository công khai, hãy tắt workflow pull request từ fork trên runner đó, không chạy thành phần nào khác trên server, và dựng lại máy theo lịch định kỳ.
Tại sao đăng ký thất bại với Http response code: NotFound?
Lệnh đăng ký trả về NotFound khi credential sai, không chỉ khi URL sai, nên thông báo này dễ gây hiểu nhầm. Registration token hết hạn sau một giờ kể từ khi được hiển thị, và personal access token không được chấp nhận cho lệnh gọi này. Hãy mở lại Settings, Actions, Runners, New self-hosted runner, sao chép token mới, và xác nhận giá trị --url trỏ đến repository mà bạn có quyền admin.