In this guide, I'll walk you through setting up a Linux server on a DigitalOcean Droplet with Ubuntu installed, which you can use as a self-hosted runner for GitHub Actions.

What is a self-hosted runner?

Self-hosted runners work similarly to GitHub's default runners, but with the key difference that you manage the server yourself. While the default runners are convenient, they come with some limitations—most notably, they timeout after six hours.

This can be a challenge for long-running jobs, particularly for users of our Neon Twin workflow who may encounter issues with large databases. In these scenarios, setting up your own self-hosted runner is a more reliable solution.

Create a Neon Twin

A Neon Twin is a full or partial clone of your production or staging database, providing developers and teams with isolated, sandboxed environments that closely mirror production.

Learn how to create a Twin here.

GitHub's default runners come with several preinstalled packages and dependencies, which you can review in the GitHub runner-images repository README. The default runner image also includes specific user permissions. To set up an effective self-hosted runner, you'll need to manually configure these packages, dependencies, and permissions. But don't worry—I'll guide you through each step.

Getting started with Digital Ocean

If you don't have a Digital Ocean account already, create one here. You will need to enter payment details before continuing to the next step.

Create a Droplet

From the navigation list on the left hand side select Droplets, then click Create Droplet.

Screenshot of Digital Ocean Create Droploet

On the next screen you'll have a number of options to choose from. In this example I'll be deploying the Droplet to Digital Ocean's New York Datacenter and using Ubuntu for the Droplet image.

Screenshot of Digital Ocean Droplet Config - Datacenter

Scroll down to the next section and choose the Droplet Size and CPU Options. In ths example, I've chosen to use a Shared CPU and The smallest Disk size.

Screenshot of Digital Ocean Droplet Config - CPU Size

The next step is to select an authentication method. Choose Password and set a password that you'll use to log in as the root user.

Screenshot of Digital Ocean Droplet Config - Auth Method

The final step is to give your Droplet a name—I've chosen self-hosted-actions-runner. This name will appear in the GitHub UI, which I'll explain in a later step. Once you're ready, click Create Droplet.

Screenshot of Digital Ocean Droplet Config- Droplet Name

After your Droplet is created, it will appear in the DigitalOcean UI. From there, you can copy the IP address, which you'll need for the next step.

Screenshot of Digital Ocean Droplet Config- Droplet IP

Configure Droplet

Now that your Droplet is created, run the following command in your terminal to log in as the root user. You'll be prompted for your password—enter it to complete the login.

ssh root@<Your Droplet's IP address>

Create a new user

It's generally not recommended to provide external services with root access to your server. In this step, you'll create a new user named runneruser and grant it the necessary privileges to function as a self-hosted runner for GitHub Actions.

In your terminal, run the following command to create the runneruser and set a password.

adduser runneruser

You'll be asked to re-enter the password. For the following prompts, you can simply press ENTER to accept the default values for fields like Full Name, Room Number, Work Phone, and so on. When prompted, press Y to confirm that all the information is correct.

Add user to sudo group

The runneruser needs to be added to the sudo group and granted permission to install packages without being prompted for a password.

To add the runneruser to the sudo group, run the following command in your terminal.

adduser runneruser sudo

Next you need to update the permissions. To do this run the following in the terminal.

visudo

Granting privileges

Scroll down until you find the following section, then update the configuration to grant runneruser permission to execute specific sudo commands.

In this guide, I’m allowing runneruser to:

  • Use the apt command
  • Run a specific command to install the postgresql-common package
  • Execute these commands without being prompted for a password using NOPASSWD

The exact permissions you need to grant will depend on the specific requirements of your Action.

# Allow members of group sudo to execute any command
%sudo   ALL=(ALL:ALL) ALL
runneruser ALL=(ALL) NOPASSWD: /usr/bin/apt, /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh

To exit visudo, press ^X and confirm the changes. Once you're done, log out as the root user by typing exit in your terminal.

Log in as runneruser

Now that the runneruser has been created, you can log in to configure the system for the self-hosted runner. Run the following command in your terminal to log in as runneruser.

ssh runneruser@<Your Droplet's IP address>

After logging in, got back to GitHub and navigate to Settings > Actions > Runners, then click New self-hosted runner.

From the options select Linux under the Runner Image section and x64 in the Architecture section.

Screenshot of GitHub - Self-hosted Runners

Follow the Download and Configure steps. When you reach the final step, Create the runner and start the configuration experience, press Enter to accept the default options. However, skip the last step, which runs ./run.sh—I'll explain why next.

Running the Self-Hosted Runner

While running ./run.sh is the simplest way to start your self-hosted runner on your Droplet, it operates as a foreground process. This means that once you close your terminal, the runner stops. If you intend to use the runner for long-running workflows, you’ll need to ensure it runs in the background.

To do this, you can set up your self-hosted runner to run as a service. This involves an additional setup step, and the method for starting the service is slightly different, but it guarantees the runner continues to operate in the background even after you close your terminal.

For further details, refer to the GitHub documentation: Configuring the self-hosted runner application as a service. Just keep in mind, you'll need to change directories before running any of the commands listed on that page.

To change the directory, run the following in your terminal:

cd actions-runner

After completing these steps, refresh the GitHub page. Your Droplet should now appear in the UI with an Idle status.

Screenshot of GitHub - Runner Idle

Creating a test GitHub Action

This test Action installs Postgres using APT, then echoes both the DEV_DATABASE_URL's Postgres version and the version of Postgres installed in the Action environment.

To use this Action, you'll need to create an environment variable named DEV_DATABASE_URL with a valid Postgres connection string and add it to your GitHub Repository Secrets.

To do this navigate to Settings > Secrets and variables > Actions and add the environment variable under Repository secrets.

Screenshot of GitHub - Repository secrets

One key detail to note is the line: runs-on: self-hosted. This tells GitHub that the Action should run on your self-hosted runner rather than the default shared infrastructure.

name: Self Hosted Runner

on:
  workflow_dispatch:

env:
  DEV_DATABASE_URL: ${{ secrets.DEV_DATABASE_URL }}
  PG_VERSION: 16

jobs:
  check-pg-version:
    runs-on: self-hosted

    steps:
      - name: Install PostgreSQL Common Package
        run: |
          sudo apt update
          sudo apt install -y postgresql-common

      - name: Install PostgreSQL
        run: |
          sudo apt update
          yes '' | sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh
          sudo apt install -y postgresql-${{ env.PG_VERSION }}

      - name: Set PostgreSQL binary path
        run: echo "POSTGRES=/usr/lib/postgresql/${{ env.PG_VERSION }}/bin" >> $GITHUB_ENV

      - name: PostgreSQL version
        run: |
          "$POSTGRES/psql" "$DEV_DATABASE_URL" -c "SELECT version();"

      - name: Check psql Version
        run: |
          $POSTGRES/psql --version

Finished

While this Action may seem somewhat complex, it effectively demonstrates how the steps within the workflow align with the permissions set on the Droplet.

Only the commands explicitly allowed for runneruser are permitted to run. If you need to add more steps to your Action that require extra permissions, you’ll need to update runneruser's sudo privileges accordingly.

That said, with a self-hosted runner, you have full control. You can deploy a high-performance Droplet capable of running longer and faster than GitHub's default shared infrastructure runner, helping to overcome any limitations you may have encountered.