Set up a self-hosted GitHub actions runner in minutes
In my homelab, I run a Lenovo ThinkCentre M920 (i5, 32GB RAM) for various tasks—including a self-hosted GitHub Actions runner. The setup is straightforward and works out of the box.
Heads up: GitHub recommends using self-hosted runners only for private repositories, as public repos pose security risks.
Setting up the server
In my setup, I’m using Proxmox, so I spun up a new LXC container with Docker pre-installed:
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/docker.sh)"
Depending on the actions you plan to run, I recommend allocating at least 8GB of disk space.
Once your machine is ready, create a new user and set a password:
root@runner-server:~$ useradd -m -G sudo -s /bin/bash runner
root@runner-server:~$ sudo passwd runner
If you’ll be running Docker builds, make sure to add the new user to the docker
group to avoid permission issues:
root@runner-server:~$ sudo usermod -a -G runner docker
You can verify the user's Docker access by running:
runner@runner-server:~$ docker info
Client: Docker Engine - Community
Version: 28.0.4
Context: default
Debug Mode: false
Plugins:
buildx: Docker Buildx (Docker Inc.)
Version: v0.22.0
Path: /usr/libexec/docker/cli-plugins/docker-buildx
compose: Docker Compose (Docker Inc.)
Version: v2.34.0
Path: /usr/libexec/docker/cli-plugins/docker-compose
Creating the runner
Head to your repository: Settings → Actions → Runners, then click “New self-hosted runner” to start the setup process.
On the next screen, choose your operating system and follow the step-by-step instructions provided to download and configure the runner.
When setting up the runner, GitHub will prompt you for the following:
Group name
Enter the name of the runner group to add this runner to: [press Enter for Default]
If you have multiple runners, you can group them. Unless you have a specific setup in mind, just press Enter to use the "Default" group.
Runner name
Enter the name of runner: [press Enter for runner-server]
This will be the name of your self-hosted runner—helpful for identifying it later, especially if you have more than one.
Labels
This runner will have the following labels: 'self-hosted', 'Linux', 'X64'
Enter any additional labels (ex. label-1,label-2): [press Enter to skip]
These are the labels. You can add custom ones to help target specific runners when defining your workflows—for example, ubuntu, docker, or lxc.
jobs:
build:
# The type of runner that the job will run on
runs-on: [self-hosted, Linux, X64, label-1]
Work folder
Enter name of work folder: [press Enter for _work]
This sets the default working directory for your workflows on the self-hosted runner. You can safely leave it as-is.
Once you’ve followed GitHub’s setup steps, just start the runner—you should see something like this:
runner@runner-server:~/actions-runner$ ./run.sh
√ Connected to GitHub
Current runner version: '2.323.0'
2025-04-10 12:16:52Z: Listening for Jobs
And in your repository under Settings → Actions → Runners, you should now see your self-hosted runner listed and marked as Idle.
Now we just need to setup a job.
Running a test job
For this test I'm using a simple job:
on:
workflow_dispatch:
jobs:
build:
# The type of runner that the job will run on
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- name: Run a one-line script
run: echo Hello, world!
- name: Run a multi-line script
run: |
echo Add other actions to build,
echo test, and deploy your project.
Once you've created the job, head to the Actions tab in your repository and click "Run workflow" to trigger it manually.
You should see:
Requested labels: self-hosted
Job defined at: self-hosted-runner-test/.github/workflows/test-job.yml@refs/heads/main
Waiting for a runner to pick up this job...
Job is about to start running on the runner: runner-server (repository)
And in your runner-server:
runner@runner-server:~/actions-runner$ ./run.sh
√ Connected to GitHub
Current runner version: '2.323.0'
2025-04-10 12:41:12Z: Listening for Jobs
2025-04-10 12:42:16Z: Running job: build
2025-04-10 12:42:29Z: Job build completed with result: Succeeded
In GitHub, you can also monitor the runner's current job execution:
Running as a service
Instead of manually starting the runner with ./run.sh
, you can configure it to run as a service. To do this, first install the runner as a service:
./svc.sh install USERNAME
In our example, since we added a new user runner
:
runner@runner-server:~/actions-runner$ sudo ./svc.sh install runner
[sudo] password for runner:
Creating launch runner in /etc/systemd/system/actions.runner.self-hosted-runner-test.runner-server.service
Run as user: runner
Run as uid: 1000
gid: 1000
Created symlink /etc/systemd/system/multi-user.target.wants/actions.runner.self-hosted-runner-test.runner-server.service → /etc/systemd/system/actions.runner.self-hosted-runner-test.runner-server.service.
Now you just need to start the service:
runner@runner-server:~/actions-runner$ sudo ./svc.sh start
/etc/systemd/system/actions.runner.self-hosted-runner-test.runner-server.service
● actions.runner.self-hosted-runner-test.runner-server.service - GitHub Actions Runner (self-hosted-runner-test.runner-server)
Loaded: loaded (/etc/systemd/system/actions.runner.self-hosted-runner-test.runner-server.service; enabled; preset: enabled)
Active: active (running) since Thu 2025-04-10 08:50:00 -04; 11ms ago
Main PID: 65251 (runsvc.sh)
Tasks: 2 (limit: 38247)
Memory: 1.6M
CPU: 7ms
CGroup: /system.slice/actions.runner.self-hosted-runner-test.runner-server.service
├─65251 /bin/bash /home/runner/actions-runner/runsvc.sh
└─65253 ./externals/node20/bin/node ./bin/RunnerService.js
Apr 10 08:50:00 docker systemd[1]: Started actions.runner.self-hosted-runner-test.runner-server.service - GitHub Actions Runner (self-ho…nner-server).
Apr 10 08:50:00 docker runsvc.sh[65251]: .path=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Hint: Some lines were ellipsized, use -l to show in full.
Running scripts before/after
For more complex jobs, you might need to run scripts before or after a job. You can do this using two environment variables:
ACTIONS_RUNNER_HOOK_JOB_STARTED
: Runs before the job starts.ACTIONS_RUNNER_HOOK_JOB_COMPLETED
: Runs after the job finishes.
Here’s an example script. Save it outside your runner’s working directory:
runner@runner-server:~$ cd ~
runner@runner-server:~$ echo 'ls -la' > list-files.sh
runner@runner-server:~$ chmod +x list-files.sh
Go to your runner's directory and create a .env
file:
runner@runner-server:~$ cd actions-runner/
runner@runner-server:~/actions-runner$ echo "ACTIONS_RUNNER_HOOK_JOB_STARTED=/home/runner/list-files.sh" > .env
runner@runner-server:~/actions-runner$ cat .env
ACTIONS_RUNNER_HOOK_JOB_STARTED=/home/runner/list-files.sh
If you're running the runner as a service, restart it to apply the hook changes:
runner@runner-server:~$ sudo ./svc.sh stop
runner@runner-server:~$ sudo ./svc.sh start
You should see the script's output in your build log:
Considerations
There's a few considerations when running your self-hosted runner:
- Security & access control: Restrict which workflows can use your runner (e.g. disable forks, enforce branch protections), run it on a hardened host, and secure the runner’s registration token;
- Updates & maintenance: You’re responsible for OS and software patches—keep the runner app up‑to‑date, clear out old workspaces regularly, and automate workspace cleanup to avoid disk exhaustion;
- Resource sizing & scaling: Ensure your machine has enough CPU, memory, and disk;
- Networking & connectivity: The runner needs outbound access to GitHub’s APIs (and any artifact registries). If you’re behind a proxy or firewall, configure proxy settings and open required ports;
- Workspace hygiene: By default each job clones into the working directory; auto‑remove or periodically prune old workspaces to prevent disk bloat;