06Self-Hosted

Self Hosted Runner using Docker

Run GitHub Actions on your own computer without cluttering your OS. Learn how to containerize your runner infrastructure using Docker.

Prerequisites

  • Docker Desktop Installed
  • GitHub Account & Repository
  • Basic Terminal Knowledge
Security First

Never share your Personal Access Token (PAT). We will use a .env file to keep it safe on your machine.

The 7-Step Setup Guide

1

Generate PAT (Classic)

Create a Personal Access Token with 'repo' and 'workflow' scopes. This allows your runner to talk to GitHub.

2

Prepare Workspace

Create a new folder (e.g., 'my-runner') on your machine to hold the configuration files.

3

The Configuration Trio

Create Dockerfile, start.sh, and docker-compose.yml. These define your runner's environment.

4

Connect with Secrets

Create a .env file with your GITHUB_TOKEN and REPO_URL. Never commit this to Git!

5

Spin it up!

Run 'docker compose up --build' in your terminal and watch your runner go online.

6

Verification

Check 'Settings > Actions > Runners' in your repo. You should see a green 'Idle' dot.

7

Deploy to Runner

Update your workflow file to use 'runs-on: self-hosted' and trigger a manual run.

Technical Deep Dive

1. The Dockerfile (The Machine)

infrastructure-as-code
Dockerfile
# FILE: Dockerfile
FROM ubuntu:22.04

# Don't ask for user input during install
ENV DEBIAN_FRONTEND=noninteractive

# Install dependencies (curl, jq, sudo, etc.)
RUN apt-get update && apt-get install -y \
    curl git jq sudo unzip ca-certificates \
    && rm -rf /var/lib/apt/lists/*

# Install Node.js 20
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
    && apt-get install -y nodejs \
    && rm -rf /var/lib/apt/lists/*

# Create a safe non-root user called "runner"
RUN useradd -m -s /bin/bash runner \
    && echo "runner ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers

# Download and extract the GitHub Actions runner app
ENV RUNNER_VERSION=2.316.1
WORKDIR /home/runner
RUN curl -o runner.tar.gz -L \
    "https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz" \
    && tar xzf runner.tar.gz \
    && rm runner.tar.gz \
    && ./bin/installdependencies.sh

# Copy startup script and set permissions
COPY start.sh /home/runner/start.sh
RUN chmod +x /home/runner/start.sh \
    && chown -R runner:runner /home/runner

USER runner
ENTRYPOINT ["/home/runner/start.sh"]

This file defines a lightweight Ubuntu machine, installs Node.js, and downloads the official GitHub Runner binary. We use a non-root runner user for improved security.

2. start.sh (The Brain)

start.sh
#!/bin/bash
# FILE: start.sh
set -e

# Get registration token from GitHub API
echo "🔑 Getting registration token from GitHub..."
REPO_PATH=$(echo "$REPO_URL" | sed 's|https://github.com/||')
REG_TOKEN=$(curl -s -X POST \
  -H "Authorization: Bearer $GITHUB_TOKEN" \
  -H "Accept: application/vnd.github+json" \
  "https://api.github.com/repos/${REPO_PATH}/actions/runners/registration-token" \
  | jq -r '.token')

echo "⚙️ Registering runner..."
./config.sh --url "$REPO_URL" --token "$REG_TOKEN" --unattended --replace

# Cleanup on exit
cleanup() {
  echo "🛑 Removing runner..."
  ./config.sh remove --token "$REG_TOKEN"
}
trap cleanup EXIT SIGTERM SIGINT

echo "🚀 Runner is ONLINE!"
./run.sh

This script handles the heavy lifting: it uses your PAT to request a short-lived Registration Token from GitHub, configures the runner, and ensures it removes itself cleanly when you stop the container.

3. docker-compose.yml (The Coordinator)

docker-compose.yml
# FILE: docker-compose.yml
services:
  runner:
    build: .
    env_file: .env
    environment:
      RUNNER_NAME: my-docker-runner
    restart: unless-stopped

Instead of long docker commands, we use Compose to map our .env secrets and ensure the runner automatically restarts if your computer reboots.

Testing Your Runner

Save this workflow to see your Docker container in action.

RUNS-ON: SELF-HOSTED
.github/workflows/test.yml
name: Use My Self-Hosted Runner
on:
  workflow_dispatch:

jobs:
  run-on-my-machine:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v4
      - name: System Info
        run: |
          echo "Hostname : $(hostname)"
          echo "OS       : $(lsb_release -d | cut -f2)"
          node --version