Software Engineer @ fuhrmanns
Open Source /#linux
4 min read

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.

Meta/Imagens/self-hosted-github-action-runner/add-new-runner-button.png

On the next screen, choose your operating system and follow the step-by-step instructions provided to download and configure the runner.

Meta/Imagens/self-hosted-github-action-runner/new-self-hosted-instructions.png

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.

Meta/Imagens/self-hosted-github-action-runner/self-hosted-runner-list.png

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.

Meta/Imagens/self-hosted-github-action-runner/run-workflow-example.png

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:

Meta/Imagens/self-hosted-github-action-runner/runner-current-job.png

Meta/Imagens/self-hosted-github-action-runner/action-output-example.png

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:

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:

Meta/Imagens/self-hosted-github-action-runner/script-output-build-log.png

Considerations

There's a few considerations when running your self-hosted runner: