Dockerized self-hosted GitHub Actions runner, built on ghcr.io/actions/actions-runner.
Copy the ./config.sh --url ... --token ... line from GitHub's "New self-hosted runner" page and paste it after the script:
./new-runner.sh ./config.sh --url https://github.com/owner/repo --token XXX --name my-runner
This builds the image and starts a detached container named after the runner (--name is optional; defaults to runner-<repo>).
- Get a registration token: repo → Settings → Actions → Runners → New self-hosted runner (docs). Tokens expire after 1 hour.
- Copy
.env.exampleto.envand fill in:REPO_URL—https://github.com/<owner>/<repo>RUNNER_TOKEN— the registration tokenDOCKER_GID— gid of the host's docker socket (stat -c %g /var/run/docker.sock), so the runner can use it
- Start:
docker compose up -d --build
Jobs can run docker build, docker push, and most other Docker CLI commands, subject to the limits below — sharing the host daemon supports some commands and not others. The image already ships the Docker CLI and buildx; both the compose file and new-runner.sh mount the host's /var/run/docker.sock and add its gid as a supplementary group so the runner user can reach it.
This shares the host daemon rather than running a second one inside the container, so:
- Jobs get effective root on the host. A job can mount
/into a container. Use this runner only for repositories with trusted contributors, never for workflows that run fork pull requests. - Containers started by a job are siblings, not children.
docker buildis unaffected, butdocker run -v "$PWD:/src"resolves$PWDagainst the host filesystem, where the workspace does not exist, and mounts an empty directory instead of failing. Bind-mount_workat a matching host path if workflows need this. - Jobs cannot use
container:orservices:. Not supported. The runner writes each step's script into its own_work/_temp, then starts the job container with-v /home/runner/_work:/__w. The host daemon resolves that source against the host filesystem, where the path does not exist, so it mounts an empty directory and the step fails withcannot open /__w/_temp/<uuid>.sh: No such file. The injected Node used byuses:steps (/__e) is missing for the same reason. Upstream issue: actions/runner#406. Provision toolchains withsetup-*actions instead; they work identically on GitHub-hosted and self-hosted runners. - Images and build cache land on the host and are not covered by the
_work/_tempcleanup. Prune them on the host (docker system prune). docker composeis not available inside jobs; the compose plugin is absent from the base image.
Remove the socket mount and the group in whichever entry point you use:
- compose — delete the
volumesandgroup_addblocks fromdocker-compose.yaml, and theDOCKER_GIDline from.env. Leavinggroup_addin place withDOCKER_GIDunset makes compose fail. new-runner.sh— delete the-v /var/run/docker.sock...and--group-add ...lines.
Then recreate the container. Jobs that shell out to Docker will fail with Cannot connect to the Docker daemon.
- Container restarts reuse the existing registration. Recreating the container (
--force-recreate, rebuild) re-registers, which needs a fresh token in.env. _work/_tempis cleaned daily inside the container. Override the interval withCLEANUP_INTERVAL_SECSin.env.docker compose downstops the runner but does not deregister it; GitHub lists it as "Offline" and purges it after 14 days. Remove it manually in repo settings if it will not return.