SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-10-18

Cài GitHub Actions runner trên VPS Ubuntu 24.04

Hướng dẫn đăng ký self-hosted GitHub Actions runner trên Ubuntu 24.04, dùng user riêng, kiểm tra checksum, config.sh, systemd và xử lý rủi ro fork pull request.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, July 30, 2026.

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 job rồi chạy các job đó trên phần cứng của bạn. Bạn đăng ký runner cho một repository, cài nó dưới dạng systemd service, và runner sẽ hoạt động lại sau mỗi lần reboot. GitHub lên lịch chạy job. Server của bạn thực hiện công việc.

CI (continuous integration) trên một máy do bạn sở hữu có hai lợi ích chính. Số phút build không còn bị tính theo hạn mức. Job cũng có thể truy cập những tài nguyên chỉ máy của bạn có, như build cache đã được làm nóng hoặc private network. Đổi lại là rủi ro bảo mật. Runner thực thi mọi nội dung workflow file chỉ định, với user mà bạn cấp cho nó. Vì vậy, về bản chất, workflow file chính là remote code execution. Với private repository, điều này thường an toà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 nói về pull request từ fork sẽ giải thích cơ chế này.

Toàn bộ nội dung bên dưới sử dụng Ubuntu 24.04 và runner version 2.338.0, là release hiện tại vào ngày 12 October 2026.

Bạn cần chuẩn bị gì 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 bạn đạt được trong mười phút đầu tiên trên một VPS mới. Bạn không cần mở cổng inbound. Runner mở một kết nối HTTPS (hypertext transfer protocol secure) outbound đến GitHub và giữ kết nối đó trong khi chờ công việc, nên GitHub không bao giờ kết nối đến server của bạn. Firewall có thể vẫn chặn kết nối từ Internet, nhưng các job vẫn đến được.

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 của repository.

Tạo user riêng cho runner

Không bao giờ chạy runner bằng root hoặc user admin của bạn. Mỗi job kế thừa quyền của user chạy runner, nên workflow gọi sudo sẽ thành công nếu user này được phép dùng sudo. Tạo một user không có quyền đặc biệt và chỉ sở hữu thư mục home của chính nó. Tài khoản user với quyền tối thiểu trên VPS trình bày mẫu cấu hình chung. 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-runner

passwd -l khóa mật khẩu, nên không ai có thể dùng mật khẩu đó để đăng nhập bằng gharunner. Quyền 700 trên thư mục runner rất quan trọng vì runner lưu credential ở đó dưới dạng văn bản thuần túy, và một lần 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 gharunner

passwd -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 này đang thuộc một sudo group và cơ chế cô lập 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 dưới user 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"

Trước tiên, chạy uname -m nếu bạn không chắc về kiến trúc. x86_64 nhận file linux-x64 ở trên. aarch64 nhận 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 là giá trị của tarball x64 phiên bản 2.338.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 "af4b794c1bc41d73d40535e3fe092a39f9679cd8d965954c2aca25a05ca41d32  actions-runner-linux-x64-2.338.0.tar.gz" | sha256sum -c

Nếu tải xuống đúng, lệnh sẽ in một dòng:

actions-runner-linux-x64-2.338.0.tar.gz: OK

File bị cắt ngắn hoặc đã bị thay đổi sẽ in lỗi và cảnh báo:

actions-runner-linux-x64-2.338.0.tar.gz: FAILED
sha256sum: WARNING: 1 computed checksum did NOT match

Không được bỏ qua bước kiểm tra rồi để tar phát hiện vấn đề. Archive được ghi chưa hoàn chỉnh sẽ fail 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.338.0.tar.gz
ls

Nội dung tarball và những gì không có

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 binary của runner và bin/installdependencies.sh. externals/ chứa Node runtime đi kèm mà các action JavaScript thực thi.

Hiện chưa có svc.sh. Tài liệu của GitHub mô tả đây là script “được tạo sau khi thêm runner thành công”, vì script này được tạo từ một template với repository và tên runner được đưa vào tên service. Do đó, sudo ./svc.sh install trước ./config.sh sẽ thất bại với sudo: ./svc.sh: command not found. Hãy đăng ký trước, rồi 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 các library bằng sudo, vì script sẽ ghi vào cơ sở dữ liệu package của hệ thống.

exit
cd /home/gharunner/actions-runner
sudo ./bin/installdependencies.sh

Trê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 có trong bản release của bạn. Vì vậy, cùng một script có thể chạy trên các bản Ubuntu cũ hơn và trên Debian.

Nếu bỏ qua bước này và thiếu một trong các library đó, ./config.sh sẽ dừng trước khi thực hiện 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 có dòng đầu tiên khác là Libicu's dependencies is missing for Dotnet Core 6.0. Cả hai đều xuất phát từ cùng một bước kiểm tra: trước khi chạy, config.sh kiểm tra các library đi kèm bằng ldd và libicu bằng ldconfig. Vì vậy, một link chưa được resolve sẽ làm script dừng, thay vì gây ra lỗi crash khó hiểu về sau.

Đăng ký runner với repository

Lấy token từ repository. Mở Settings, chọn Actions, chọn Runners, rồi chọn New self-hosted runner. Trang này hiển thị registration token. 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 không cho phép chạy bằng 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 \
  --replace

Các flag này có tác dụng như sau. --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 6 tháng. --labels thêm các label tùy chỉnh của bạn; 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 lần checkout, nằm bên trong thư mục runner. --unattended tự động trả lời các prompt tương tác bằng giá trị mặc định. Đây là điều bạn cần khi đặt lệnh trong một script. --replace tiếp quản registration hiện có cùng tên thay vì báo lỗi. Đây là điều bạn cần khi dựng lại server.

Lệnh chạy thành công sẽ 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 giá trị cuối dùng để xác thực runner với GitHub, vì vậy bất kỳ ai đọc được chúng đều có thể giả mạo runner. Vì lý do đó, thư mục có mode 700 và user không có quyền sudo.

Cài runner dưới dạng systemd service

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 cùng phiên SSH của bạn. Hãy cài service để runner tự khởi động khi boot. systemd service và timer trên VPS giải thích về chính các unit file. Ở đây, svc.sh sẽ tự 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 status

svc.sh yêu cầu quyền root vì script này ghi một unit vào /etc/systemd/system và enable unit đó. Đối số sau install là user mà service sẽ chạy bằng. 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 admin của bạn, khiến mọi job chạy bằng user có thể dùng sudo.

Unit được đặt tên theo 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-pager

Runner 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ị là Offline nghĩa là runner không chạy hoặc không thể kết nối đến GitHub qua cổng 443.

Gửi job đến runner

runs-on chọn runner theo label. Hãy dùng self-hosted cùng với label của bạn để job không chạy trên runner ngoài dự kiến.

name: build
on:
  push:
    branches: [main]
jobs:
  build:
    runs-on: [self-hosted, linux, vps]
    steps:
      - uses: actions/checkout@v5
      - run: uname -a

Nếu job chờ tại Waiting for a runner to pick up this job, các label không khớp. Mọi label trong runs-on đều phải tồn tại trên runner. Chỉ cần thừa một từ, job sẽ tiếp tục nằm trong hàng đợi mà không có lỗi ở đâ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 không phù hợp với repository công khai

Đâ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 sẽ chạy trong các máy ảo tạm thời, sạch và có thể bị mã độc trong workflow xâm nhập dai dẳng”.

Cơ chế rất đơn giản. Pull request từ fork mang theo bản sao riêng của file workflow. Nếu repository công khai của bạn chạy workflow cho pull request trên runner của mình, 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 được vấn đề. Chính sách mặc định của repository công khai yêu cầu maintainer phê duyệt workflow từ fork của contributor lần đầu. GitHub chỉ xem một người là contributor lần đầu cho đến khi bất kỳ commit hoặc pull request nào của người đó được merge vào repository. Sau đó, các pull request tiếp theo của họ sẽ chạy mà không cần hỏi lại. Chính GitHub cũng cảnh báo rằng chỉ cần một bản sửa nhỏ được merge là đã đủ để vượt qua bước này. Vì vậy, lớp kiểm soát chỉ là một người đọc diff; payload bị giấu sâu ba cấp trong build script rất dễ bị bỏ sót.

Pull request từ fork không nhận được secrets của bạn, và GITHUB_TOKEN của nó chỉ có quyền đọ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. Kẻ tấn công có shell dưới quyền gharunner, nên có thể đọc mọi file mà user đó đọc được, truy cập mọi tài nguyên mà VPS có thể truy cập trên private network, và để lại mã chạy ngầm trong ~/.bashrc hoặc một user systemd unit được chạy trong job tiếp theo.

Đăng ký bằng --ephemeral khiến runner chỉ nhận một job rồi tự deregister, nên một job không thể đọc workspace của job tiếp theo. Cách này chỉ có tác dụng nếu có cơ chế rebuild máy 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 lần đăng ký mới.

Các quy tắc tiếp theo rất ngắn. Chỉ 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 để bất kỳ thứ gì khác trên server, và xem máy này là tài nguyên có thể hủy bỏ.

Docker jobs và group thực chất có quyền root

Các job chạy trong container, service container và mọi bước workflow gọi docker build đều cần Docker daemon trên host của runner. Cài Docker theo cách thông thường, như hướng dẫn Cài Docker và Docker Compose trên VPS, sau đó thêm user chạy runner vào group docker.

Hãy 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 quyền root, vì container có thể bind mount / và chạy với quyền root bên trong container. Vì vậy, workflow có thể giao tiếp với Docker socket sẽ đọc và ghi được mọi file trên VPS, bao gồm cả /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 mọi trường hợp khác, việc 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 chạy runner, nhưng storage driver sẽ chậm hơn và không hỗ trợ privileged container.

Cập nhật và gỡ runner sạch sẽ

Theo mặc định, self-hosted runner tự cập nhật. Runner phát hiện bản release mới, thay thế các file 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ì. Để tắt tự cập nhật, truyền --disableupdate cho ./config.sh khi đăng ký runner. Với runner đã đăng ký, bạn phải đăng ký lại runner kèm flag này. Cách này không cho phép bạn giữ runner cố định ở một version mãi mãi. Tài liệu của GitHub nêu rõ runner được cấu hình với --disableupdate vẫn phải được cập nhật thủ công trong vòng 30 ngày kể từ khi có release mới. Nếu không, GitHub sẽ ngừng xếp job cho runner đó.

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 uninstall service, sau đó deregister runner. Removal token lấy từ cùng trang Runners, trong nút Remove của chính 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_HERE

Xóa thư mục mà không deregister runner sẽ khiến runner vẫn hiển thị là Offline trong repository. Runner vẫn còn trong danh sách cho đến khi admin bấm Force remove this runner hoặc GitHub tự động xóa runner sau 14 ngày không có kết nối.

Các tình huống lỗi và chuỗi bạn sẽ thấy

Must not run with sudo. config.sh in thông báo này rồi thoát khi được chạy bằng root. Kiểm tra này là có 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 bằng user dịch vụ. Chạy ./config.sh bằng gharunner. Biến RUNNER_ALLOW_RUNASROOT sẽ bỏ qua kiểm tra này, nhưng dùng nó chỉ làm lỗi xuất hiện 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 đăng ký. Hãy đăng ký runner, rồi cài service.

Http response code: NotFound from 'POST https://api.github.com/actions/runner-registration'. Thông thường 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 trong trang Runners. Tạo token mới rồi dán lại, đồng thời kiểm tra để bảo đảm --url là URL đầy đủ của repository.

Dependencies is missing for Dotnet Core 6.0. Chạy sudo ./bin/installdependencies.sh từ thư mục runner bằng root, rồi đă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 nhưng runner vẫn Offline, đọc journalctl -u 'actions.runner.*' và kiểm tra HTTPS outbound.

Đĩa đầy. Các checkout, build cache và Docker image tích lũy trong _work và thư mục home của user chạy runner. Không có cơ chế nào tự prune 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ự gây ra sự cố.

FAQ

Tại sao sudo ./svc.sh install báo command not found?

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à tên runner để tạo tên service. Trước tiên, hãy chạy ./config.sh với tư cách người dùng 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ở cổng firewall cho self-hosted runner 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, nên 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 rule inbound.

Tôi có thể dùng self-hosted runner cho public repository không?

Có, 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 command chạy trên máy của bạn. Prompt phê duyệt mặc định chỉ áp dụng cho những contributor chưa từng có commit hoặc pull request được merge vào repository. Nếu gắn runner vào public repository, hãy tắt workflow của pull request từ fork trên runner đó, không chạy dịch vụ nào khác trên server, và định kỳ rebuild máy.

Tại sao đăng ký thất bại với Http response code: NotFound?

Lệnh đăng ký trả về NotFound khi credential không đúng, chứ không chỉ khi URL không đúng, 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. Mở lại Settings, Actions, Runners, New self-hosted runner, sao chép token mới, rồi xác nhận giá trị --url trỏ đến repository mà bạn có quyền admin.

Bài viết này có hữu ích không?

#github-actions#ci#self-hosted#runner#ubuntu-24-04