Skip to content
Self-Hosted GitHub Actions Runner on RHEL 10 with SELinux

Self-Hosted GitHub Actions Runner on RHEL 10 with SELinux

May 2, 2026·Alex Kraker

Standing up a self-hosted GitHub Actions Runner on RHEL 10 as a daemonized service.

This assumes you have a RHEL 10 server that can host a self-hosted runner. I’m playing around with CI/CD pipelines to automate building custom RHEL images to use for development/testing. That’s my current use-case, but I’m sure there are many reasons you might want to stand up self-hosted runners that are Red Hat flavored. Some things in the Red Hat ecosystem are tough to do without a system registered with Red Hat Subscription Manager. SELinux is worth figuring out for secure enterprise contexts.

1. Create github-runner system user

Security best practices want us to create a devoted system user for our service to run as. Since this isn’t a human user, we’ll create a home directory in /opt rather than /home. Also, the executables that we install to /opt will inherit the usr_t file-context, which allows systemd to run them. More on this later.

Create the user:

sudo useradd --system \
  --home-dir /opt/github-runner \
  --create-home \
  --shell /usr/sbin/nologin \
  --comment "GitHub Actions Runner" \
  github-runner

2. Install and configure the self-hosted runner

First switch to the github-runner user and change to the home directory:

sudo -u github-runner -H bash && cd
Note that we have to specify the login shell (e.g. -H bash) or our interactive login won’t work having set /usr/sbin/nologin above.

A quick sanity check that we’re in the right directory couldn’t hurt:

github-runner@hal9000:~$ pwd
/opt/github-runner

You can add a new runner to a repository or an organization. This is for my personal use, so I’m adding the runner to a repository.

From a project in GitHub navigate to Settings » Actions » Runners » New self-hosted runner and GitHub populates a set of commands you can run to install and configure the runner.

See Adding a self-hosted runner to a repository for detailed steps to get there.

If you’re following along, here’s my install:

# Create a folder
$ mkdir actions-runner && cd actions-runner
# Download the latest runner package
$ curl -o actions-runner-linux-x64-2.334.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.334.0/actions-runner-linux-x64-2.334.0.tar.gz
# Optional: Validate the hash
$ echo "048024cd2c848eb6f14d5646d56c13a4def2ae7ee3ad12122bee960c56f3d271  actions-runner-linux-x64-2.334.0.tar.gz" | shasum -a 256 -c
# Extract the installer
$ tar xzf ./actions-runner-linux-x64-2.334.0.tar.gz

And your configure steps should look similar to this:

# Create the runner and start the configuration experience
$ ./config.sh --url https://github.com/kraker/homelab --token <REDACTED>
# Last step, run it!
$ ./run.sh  # Optional to skip this, since we'll set the service up next

The config script is interactive and will ask you for input. I just went with the defaults, which is ENTER 4 times. But you may wish to override some of them for your needs.

3. Install the service

Root privileges are required for these next steps so logout of the github-runner user. And then elevate yourself to root and change to the actions-runner directory.

sudo -i
cd /opt/github-runner/actions-runner

Install the service for the github-runner user, then start it:

./svc.sh install github-runner
./svc.sh start

Why /opt/github-runner?

Why /opt/github-runner and not somewhere else like /home (or /var/lib) for example?

A bit of behind-the-scenes on why this works: systemd is allowed to exec files with the usr_t context, and our runner files all inherit usr_t from /opt. So when svc.sh install generates runsvc.sh for us, the file is created with usr_t and systemd (which runs as init_t) is allowed to run it. If we had picked /var/lib/github-runner/ instead, that same file would inherit var_lib_t, which systemd isn’t allowed to exec. The service would fail to start with a “Permission denied”. It would be the same problem if we had opted for /home/github-runner.

One other thing worth mentioning: svc.sh install runs restorecon on the systemd unit file itself, so we don’t need to.

4. Verify

The service should be active and the runner connected. Status:

$ sudo systemctl status 'actions.runner.*' --no-pager
● actions.runner.<owner>-<repo>.<runner-name>.service - GitHub Actions Runner ...
     Active: active (running) since ...
   Main PID: ... (runsvc.sh)

And the journal will show it picked up its credentials and connected:

$ sudo journalctl -u 'actions.runner.*' -n 5 --no-pager
... runsvc.sh[...]: Started running service
... runsvc.sh[...]: √ Connected to GitHub
... runsvc.sh[...]: Listening for Jobs

Check your repo’s Settings » Actions » Runners page in GitHub and the runner should be sitting there with a green Idle badge, ready to pick up jobs.

References