Set Up a Self-Hosted GitHub Actions Runner on Linux (Securely) for Faster CI/CD

Why self-hosted runners are worth it

If your builds are slow, your workflow uses special tools, or you need access to an internal network, a self-hosted GitHub Actions runner can be a game-changer. Instead of relying on GitHub-hosted runners (which are shared and time-limited), you run jobs on your own Linux machine. This tutorial shows how to set up a runner on Ubuntu Server with a clean, secure approach: a dedicated user, a systemd service, and basic hardening tips.

What you need before you start

You’ll need: (1) an Ubuntu Server 22.04/24.04 machine (VM or bare metal), (2) outbound internet access to GitHub, (3) a GitHub repository or organization where you can register runners, and (4) sudo privileges on the Linux host. For best results, use a separate machine or VM for CI jobs—treat it as disposable infrastructure.

Step 1: Update the server and install prerequisites

First, patch the system and install common dependencies used by build pipelines. Run the following commands:

sudo apt update && sudo apt -y upgrade

sudo apt -y install curl tar git ca-certificates

If your workflows build containers, install Docker later (and consider isolating it). For now, keep the base runner simple.

Step 2: Create a dedicated runner user

Avoid running CI as your personal account or as root. Create a dedicated user and a working directory:

sudo adduser --disabled-password --gecos "" actions

sudo mkdir -p /opt/actions-runner

sudo chown -R actions:actions /opt/actions-runner

This helps with least privilege and keeps runner files in a predictable location for maintenance.

Step 3: Download the GitHub Actions runner

Switch to the runner user and download the latest Linux x64 runner package. You can find the current version on GitHub’s official runner releases page, but the process is always the same:

sudo -iu actions

cd /opt/actions-runner

curl -o actions-runner-linux-x64.tar.gz -L https://github.com/actions/runner/releases/latest/download/actions-runner-linux-x64-2.0.0.tar.gz

tar xzf actions-runner-linux-x64.tar.gz

Note: the filename in the URL can change as new versions are released. If you get a 404 error, open the releases page and copy the exact download link for Linux x64.

Step 4: Register the runner with your repo or organization

In GitHub, go to your repository: Settings > Actions > Runners > New self-hosted runner. Choose Linux, and GitHub will display a short set of commands including a registration token.

Back on your server (still as the actions user), run the configuration script using the URL and token GitHub provides:

./config.sh --url https://github.com/OWNER/REPO --token YOUR_TOKEN

When prompted, set a clear runner name (for example, ubuntu-ci-01) and add labels that match your use case (like linux, self-hosted, docker, gpu). Labels let you target specific runners in workflows.

Step 5: Install the runner as a systemd service

Running the runner in a terminal works, but it’s not reliable. Install it as a service so it starts on boot and restarts on failure:

sudo ./svc.sh install

sudo ./svc.sh start

Then verify status:

sudo ./svc.sh status

Within a minute, the runner should show as Idle in GitHub under Actions runners.

Step 6: Update a workflow to use your self-hosted runner

In your workflow YAML, set runs-on to include self-hosted plus any labels you assigned:

runs-on: [self-hosted, linux]

If you want to guarantee the job lands on a specific capability (like Docker), use a custom label such as docker and specify it in runs-on.

Security and hardening tips (don’t skip these)

A self-hosted runner executes code from your repository, including pull requests if you allow it. Treat it like a production entry point. Use these practical safeguards: (1) run on a dedicated VM, (2) restrict which branches and events can use the runner, (3) avoid running untrusted fork PRs on self-hosted runners, and (4) keep the OS patched.

Also consider network segmentation: if the runner can reach internal services, use firewall rules so it can only access what it truly needs. If you install Docker, be careful with granting the runner user access to the Docker socket—Docker can effectively become root on the host. For higher-risk environments, run builds inside isolated containers or ephemeral VMs.

Troubleshooting common problems

Runner is offline: check service status (sudo ./svc.sh status) and logs with journalctl -u actions.runner.* -n 200 --no-pager. Reboot-safe service configuration usually fixes “works in terminal, fails after reboot” issues.

Token expired: registration tokens are short-lived. Generate a new token in GitHub and re-run ./config.sh after removing the old configuration (./config.sh remove).

Jobs stuck waiting: your workflow’s runs-on labels must match the runner labels exactly. If the workflow requires [self-hosted, linux, docker] but your runner is only labeled linux, GitHub will keep the job queued.

Conclusion

With a self-hosted GitHub Actions runner on Linux, you can speed up CI/CD, use specialized build tools, and keep deployment workflows closer to your infrastructure. The key is to set it up cleanly (dedicated user and systemd service) and to treat security as part of the installation—not an afterthought.

Comments