Run GitHub Actions workflows on your own infrastructure using Docker. One image, multiple containers—each container is a dedicated runner for a specific repository. Supports Apple Silicon (M1/M2/M3) and x86_64, and uses the host’s Docker daemon so jobs can run Docker and Docker Compose inside workflows.
- Self-hosted runners in containers — Each container registers as a GitHub Actions runner for a single repo. You can run several containers (one per repo or per env file) from the same image.
- Uses host Docker — The container mounts the host’s Docker socket (
/var/run/docker.sock), so workflows can rundockeranddocker-composewithout Docker-in-Docker. Work directory is shared so volume paths match the host. - Multi-architecture — Builds the correct runner binary for your platform (e.g.
actions-runner-linux-arm64on Apple Silicon,actions-runner-linux-x64on amd64), avoiding “not a dynamic executable” and Rosetta errors on Mac. - Config via env files — One env file per runner (e.g. per repo). Use
docker compose --env-file env/your-repo.envto choose which runner to start.
- Docker and Docker Compose
- A GitHub repo where you can add a self-hosted runner
- A one-time registration token from that repo (see below)
- Open your GitHub repo in the browser.
- Go to Settings → Actions → Runners.
- Click New self-hosted runner.
- On the setup page, copy the token (the long string under “Configure”). Use it as
RUNNER_TOKENin your env file.
The token is one-time use: after the runner registers, you don’t need it again. Keep it secret and don’t commit it.
-
Clone this repo and go to its directory.
-
Copy the sample env file and edit it with your repo and token:
cp env/sample.env env/my-repo.env
Edit
env/my-repo.envand set at least:REPO_URL— e.g.https://github.com/your-org/your-repoRUNNER_TOKEN— paste the token from GitHub (see How to Get the GitHub Runner Token)RUNNER_NAME— unique name for this runner (e.g.docker-my-repo)ENV_FILE— path to this file (e.g.env/my-repo.env)
-
Build and run the runner:
docker compose -f compose.yml --env-file env/sample.env build docker compose -f compose.yml --env-file env/sample.env up -d
Use your actual env file (e.g.
env/my-repo.env) once you’ve created it. Withsample.envyou must at least set a validREPO_URLandRUNNER_TOKENfor the runner to register. -
In GitHub: Repo → Settings → Actions → Runners. The new runner should appear and become “Idle” when ready.
All configuration is via environment variables, typically in an env file under env/.
| Variable | Required | Description |
|---|---|---|
REPO_URL |
Yes | GitHub repo URL (e.g. https://github.com/org/repo). |
RUNNER_TOKEN |
Yes | One-time registration token from the repo’s Runners settings. |
RUNNER_NAME |
Yes | Display name for the runner; must be unique per repo. |
ENV_FILE |
Yes* | Path to this env file (e.g. env/my-repo.env). Used by Compose for substitution. |
RUNNER_LABELS |
No | Comma-separated labels (default: self-hosted,docker). Use in workflows as runs-on: [self-hosted, docker]. |
RUNNER_WORK_DIR |
No | Work directory for job files (default: /tmp/github-runner-work). |
DOCKER_GID |
No | Host Docker group GID if different from 999 (Linux: getent group docker | cut -d: -f3). |
* Required for Compose to resolve ${ENV_FILE} and other vars in compose.yml.
Important: Do not commit env files that contain real RUNNER_TOKEN values. Use env/sample.env as a template and add real env files to .gitignore if needed.
Create one env file (e.g. env/my-repo.env), set REPO_URL, RUNNER_TOKEN, RUNNER_NAME, and ENV_FILE, then:
docker compose -f compose.yml --env-file env/my-repo.env up -dUse one env file per repo and start a container per env file:
# Runner for repo 1
docker compose -f compose.yml --env-file env/repo-one.env up -d
# Runner for repo 2 (different terminal or after stopping the first)
docker compose -f compose.yml --env-file env/repo-two.env up -dContainers are distinguished by RUNNER_NAME (and thus by container_name). Use different env files and different RUNNER_NAME values for each.
The README and env/sample.env use env/sample.env in examples. For real use:
- Copy
env/sample.envto something likeenv/my-repo.env. - Fill in
REPO_URL,RUNNER_TOKEN,RUNNER_NAME, and setENV_FILE=env/my-repo.env. - Run with that file:
docker compose -f compose.yml --env-file env/my-repo.env up -d.
The image picks the right runner binary for the build platform:
- Apple Silicon (M1/M2/M3):
actions-runner-linux-arm64 - x86_64 / amd64:
actions-runner-linux-x64
So you can build and run natively on Apple Silicon without “not a dynamic executable” or Rosetta ELF errors.
The container mounts the host’s Docker socket and installs the Docker CLI and Docker Compose. So in your workflow you can do:
runs-on: [self-hosted, docker]
steps:
- run: docker compose up -dThe work directory is shared between host and container so volume paths used in Compose can match the host. If your host’s Docker group GID is not 999, set DOCKER_GID in your env file.
After changing the Dockerfile or Compose setup:
docker compose -f compose.yml --env-file env/sample.env build --no-cache
docker compose -f compose.yml --env-file env/sample.env up -dUse your real env file (e.g. env/my-repo.env) when you run this for a specific runner.
| Goal | Command |
|---|---|
| Build image | docker compose -f compose.yml --env-file env/sample.env build |
| Start runner | docker compose -f compose.yml --env-file env/sample.env up -d |
| Stop runner | docker compose -f compose.yml --env-file env/sample.env down |
| Rebuild from scratch | docker compose -f compose.yml --env-file env/sample.env build --no-cache |
Replace env/sample.env with your own env file (e.g. env/my-repo.env) and ensure it contains valid REPO_URL, RUNNER_TOKEN, RUNNER_NAME, and ENV_FILE.