-
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathreadme-vars.yml
More file actions
221 lines (198 loc) · 19.2 KB
/
Copy pathreadme-vars.yml
File metadata and controls
221 lines (198 loc) · 19.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
---
# project information
project_name: gh-runner
project_url: "https://github.com/actions/runner"
project_logo: ""
project_blurb: "[GitHub Actions Runner]({{ project_url }}) is the self-hosted runner application for GitHub Actions. This image is an unofficial community container built in a LinuxServer.io-style pattern, with hardened runtime defaults, s6 process supervision, and ephemeral mode support. Supports Balena deployment for IoT/edge devices. Sponsored and maintained by Blackout Secure (https://blackoutsecure.app)."
project_lsio_github_repo_url: "https://github.com/blackoutsecure/docker-github-runner"
project_categories: "CI/CD,DevOps,Automation"
# supported architectures
available_architectures:
- {arch: "{{ arch_x86_64 }}", tag: "amd64-latest"}
- {arch: "{{ arch_arm64 }}", tag: "arm64v8-latest"}
# common parameters
common_param_env_vars_enabled: true
param_container_name: "{{ project_name }}"
param_usage_include_vols: true
param_volumes:
- {vol_path: "/config", vol_host_path: "/path/to/runner/config", desc: "Runner configuration and persistent data"}
param_usage_include_net: false
param_usage_include_env: true
param_env_vars:
- {env_var: "TZ", env_value: "Etc/UTC", desc: "Timezone (see TZ database)"}
- {env_var: "RUNNER_URL", env_value: "https://github.com/OWNER/REPO", desc: "GitHub repository, organization, or enterprise URL for runner registration (required)"}
- {env_var: "RUNNER_TOKEN", env_value: "", desc: "Runner registration token from GitHub (required if GITHUB_PAT not set, expires in 1h)"}
- {env_var: "GITHUB_PAT", env_value: "", desc: "GitHub Personal Access Token — auto-generates registration tokens via API (recommended, required if RUNNER_TOKEN not set)"}
- {env_var: "GITHUB_TOKEN", env_value: "", desc: "GitHub repo secret or App token — auto-generates registration tokens via API (alternative to GITHUB_PAT, e.g. from ${{ secrets.RUNNER_PAT }} in workflows)"}
- {env_var: "LOG_LEVEL", env_value: "info", desc: "Minimum log verbosity: debug, info (default), warn, error, fatal"}
# optional env variables
opt_param_usage_include_env: true
opt_param_env_vars:
- {env_var: "RUNNER_NAME", env_value: "", desc: "Runner name (defaults to ${BALENA_DEVICE_NAME_AT_INIT}-${BALENA_SERVICE_NAME} in balena fleets, container hostname otherwise; auto-deduplicated if already online)"}
- {env_var: "RUNNER_NAME_SUFFIX", env_value: "", desc: "Optional explicit suffix appended to the auto-derived runner name (use to force distinct names for peer replicas outside balena)"}
- {env_var: "RUNNER_LABELS", env_value: "", desc: "Comma-separated custom labels (e.g. self-hosted,linux,x64,gpu)"}
- {env_var: "RUNNER_GROUP", env_value: "", desc: "Runner group (org/enterprise only; created via API if missing, defaults to Default)"}
- {env_var: "RUNNER_WORKDIR", env_value: "/config/work", desc: "Working directory for runner jobs"}
- {env_var: "RUNNER_EPHEMERAL", env_value: "false", desc: "Run in ephemeral mode — runner exits after one job (for autoscaling)"}
- {env_var: "RUNNER_REPLACE_EXISTING", env_value: "true", desc: "Replace any existing runner with the same name"}
- {env_var: "DISABLE_RUNNER_UPDATE", env_value: "false", desc: "Disable automatic upstream runner auto-updates (useful when you pin a runner version via the image tag)"}
- {env_var: "RUNNER_TOKEN_FILE", env_value: "", desc: "Path to file containing RUNNER_TOKEN (Docker secrets / Kubernetes projected volumes)"}
- {env_var: "RUNNER_URL_FILE", env_value: "", desc: "Path to file containing RUNNER_URL"}
- {env_var: "GITHUB_PAT_FILE", env_value: "", desc: "Path to file containing GITHUB_PAT"}
- {env_var: "GITHUB_TOKEN_FILE", env_value: "", desc: "Path to file containing GITHUB_TOKEN"}
- {env_var: "RUNNER_ENV_FILE", env_value: "", desc: "Path to env file (KEY=VALUE per line) loaded into runner job environment. Use to inject GitHub repo secrets."}
- {env_var: "RUNNER_SECRETS_DIR", env_value: "", desc: "Path to directory of secret files (filename=env var, contents=value). Works with Docker Compose secrets and Kubernetes projected volumes."}
- {env_var: "EXTRA_PACKAGES", env_value: "", desc: "Space-separated apt packages installed at startup as root (escape hatch — for repeated use, build a custom image; incompatible with read_only: true)"}
- {env_var: "EXTRA_APT_REPOS", env_value: "", desc: "Semicolon-separated apt sources lines added before installing EXTRA_PACKAGES"}
- {env_var: "EXTRA_INIT_SCRIPT", env_value: "", desc: "Path inside container to a shell script (typically bind-mounted) run as root before the runner starts"}
- {env_var: "RUNNER_SUDO", env_value: "true", desc: "When truthy (default) grants the `abc` runner user passwordless sudo via /etc/sudoers.d/10-abc-nopasswd, mirroring the GitHub-hosted `ubuntu-latest` convention so `sudo apt-get install ...` works in workflows. Set to `false` to remove the drop-in at startup for a locked-down runner (the `sudo` binary itself stays installed). Accepts true/false/1/0/yes/no/on/off."}
- {env_var: "DOCKER_IN_DOCKER", env_value: "false", desc: "Enable container-based jobs: binds the runner user to the gid of the mounted /var/run/docker.sock and auto-appends a `docker` runner label. Requires bind-mounting /var/run/docker.sock. Default false because it grants the runner significant host privileges."}
- {env_var: "AUTO_DOCKER_LABEL", env_value: "", desc: "When 'true' auto-append a `docker` label to RUNNER_LABELS so workflows can target `runs-on: [self-hosted, docker]`. Empty = follow DOCKER_IN_DOCKER."}
- {env_var: "DOCKER_HOST_SOCK", env_value: "", desc: "Override the in-container Docker socket path; empty = auto-detect /var/run/docker.sock then /var/run/balena-engine.sock."}
- {env_var: "HEARTBEAT_INTERVAL", env_value: "120", desc: "Seconds between heavyweight HEALTH HEARTBEAT log banners. Minimum 30."}
- {env_var: "JOB_HEARTBEAT_INTERVAL", env_value: "120", desc: "Seconds between mid-job JOB HEARTBEAT log banners (only while a job is running). 0 disables; minimum 30 when enabled."}
- {env_var: "FORCE_RUNNER_PERMISSIONS_FIX", env_value: "false", desc: "Force a defensive `chown -R abc:abc /opt/runner-bin` at startup, bypassing the build-time ownership marker. Useful after a manual chown changed file ownership."}
- {env_var: "ONLINE_PROBE_EVERY", env_value: "1", desc: "Verify runner is online with GitHub every N heartbeats; 0 disables (requires GITHUB_PAT or GITHUB_TOKEN). Default applies to ephemeral mode too -- needed for ON_OFFLINE_ACTION to fire when an idle listener silently disconnects"}
- {env_var: "ONLINE_FAIL_THRESHOLD", env_value: "3", desc: "Consecutive offline detections before triggering ON_OFFLINE_ACTION"}
- {env_var: "ON_OFFLINE_ACTION", env_value: "restart", desc: "Action when offline threshold trips: none | restart (s6-svc -r) | shutdown (container exit)"}
- {env_var: "IDLE_RECYCLE_AFTER", env_value: "(ephemeral-aware default)", desc: "Recycle runner after N seconds continuously idle. Defaults: 21600 (6 h) when RUNNER_EPHEMERAL=true, 172800 (2 d) when false. 0 disables; values 1..299 are clamped to 300. Timer resets when a worker starts so it never interrupts a job."}
- {env_var: "IDLE_RECYCLE_ACTION", env_value: "shutdown", desc: "Action when idle recycle threshold trips: restart (s6 svc only) | shutdown (full container recycle; orchestrator restart_always brings it back) | none (log only)"}
- {env_var: "HEALTH_STALE_AFTER", env_value: "300", desc: "Seconds before the Docker HEALTHCHECK reports unhealthy if the online sentinel goes stale"}
- {env_var: "LOG_WATCH_INTERVAL", env_value: "2", desc: "Seconds between iterations of the svc-gh-runner-logs main loop. Controls how quickly JOB STARTED / JOB FINISHED banners appear after a worker spawns/exits. Clamped to [1, 10]."}
- {env_var: "LISTENER_WAIT_TIMEOUT", env_value: "600", desc: "Seconds svc-gh-runner-logs waits for the Runner.Listener process before logging a warning and continuing. Prevents silent infinite spin when the listener crashes mid-init. 0 = wait forever."}
- {env_var: "DIAG_WAIT_TIMEOUT", env_value: "120", desc: "Seconds to wait for /opt/runner-bin/_diag/ before logging a warning and continuing. 0 = wait forever."}
- {env_var: "CONFIG_TIMEOUT", env_value: "90", desc: "Hard cap (seconds) on a single config.sh invocation during registration. A wedged TCP connection to api.github.com cannot hang init beyond this. 0 = disable wrapper (legacy)."}
- {env_var: "SKIP_DEREGISTER", env_value: "false", desc: "When 'true', svc-gh-runner/finish skips API DELETE + config.sh remove on container stop so the runner stays visible in GitHub across restarts. Recommended only for long-lived persistent runners."}
- {env_var: "CLEANUP_OFFLINE_RUNNERS", env_value: "false", desc: "Master toggle for the startup sweep that DELETEs offline runners from GitHub. Requires GITHUB_PAT/GITHUB_TOKEN with the same scope used for registration."}
- {env_var: "CLEANUP_OFFLINE_AFTER", env_value: "86400", desc: "Seconds a runner must be continuously offline before removal (threshold mode). Minimum 300 (5 min); default 86400 (24 h). Ignored when immediate mode is active."}
- {env_var: "CLEANUP_OFFLINE_IMMEDIATE", env_value: "", desc: "When 'true', skip the offline-since timer and remove any currently-offline runner. Empty = auto: true when RUNNER_EPHEMERAL=true, false otherwise. The runner this container is about to register is always skipped."}
- {env_var: "CLEANUP_OFFLINE_NAME_REGEX", env_value: "", desc: "Optional ERE pattern restricting cleanup to runners whose names match (e.g. '^aada' for ephemeral hash-named runners)."}
- {env_var: "CLEANUP_OFFLINE_DRY_RUN", env_value: "false", desc: "When 'true', log what would be removed without calling DELETE. Recommended for the first deploy after enabling cleanup."}
- {env_var: "CLEANUP_OFFLINE_MAX", env_value: "25", desc: "Maximum number of runners (count, not a duration) removed per startup sweep. Safety brake against an accidental mass-delete."}
- {env_var: "CLEANUP_OFFLINE_ANY_NAME", env_value: "false", desc: "Master toggle for an additional age-gated sweep that removes ANY offline runner (any name, any labels) older than CLEANUP_OFFLINE_ANY_NAME_AFTER seconds. Independent of CLEANUP_OFFLINE_RUNNERS / CLEANUP_OFFLINE_NAME_REGEX / CLEANUP_OFFLINE_IMMEDIATE -- always threshold-mode. Subject to CLEANUP_OFFLINE_MAX / CLEANUP_OFFLINE_DRY_RUN."}
- {env_var: "CLEANUP_OFFLINE_ANY_NAME_AFTER", env_value: "604800", desc: "Seconds a runner must be continuously offline before the any-name sweep removes it. Default 604800 (7 days) -- the recommended 'unambiguously dead' window for cross-fleet GC. Hard floor 86400 (24 h); values below the floor are clamped to 86400 with a warning. Persisted via /config/.gh-runner-offline-state.json so brief reboots don't reset the timer."}
- {env_var: "S6_SERVICES_GRACETIME", env_value: "30000", desc: "s6 service shutdown gracetime (ms) — must allow time for the runner to deregister from GitHub"}
- {env_var: "S6_KILL_GRACETIME", env_value: "30000", desc: "s6 hard-kill gracetime (ms) — keep aligned with stop_grace_period in compose"}
- {env_var: "PUID", env_value: "1000", desc: "User ID for file ownership (LinuxServer.io base image standard; ignored in LSIO non-root mode)"}
- {env_var: "PGID", env_value: "1000", desc: "Group ID for file ownership (LinuxServer.io base image standard; ignored in LSIO non-root mode)"}
# optional parameters
optional_parameters: |
For Docker-in-Docker support (container-based GitHub Actions):
```
-v /var/run/docker.sock:/var/run/docker.sock
```
# application setup block
app_setup_block_enabled: true
app_setup_block: |
The container runs the GitHub Actions self-hosted runner with s6 process supervision.
**Registration Token**: Generate a registration token from your repository or organization:
- Repository: Settings > Actions > Runners > New self-hosted runner
- Organization: Settings > Actions > Runners > New runner
- API: `curl -X POST -H "Authorization: token GITHUB_PAT" https://api.github.com/repos/OWNER/REPO/actions/runners/registration-token`
**Ephemeral mode + scaling**: Set `RUNNER_EPHEMERAL=true` to make the upstream
runner accept exactly one job, run it, and exit. Host concurrency comes from
running multiple replicas. Two patterns are supported:
- **Fixed pool (recommended)** — `docker compose up -d --scale gh-runner=N`
plus `restart: always`. Capacity is constant; exited containers are
recreated automatically. Works identically on plain Docker, Swarm,
Kubernetes, and Balena — no scaler container needed.
- **Dynamic scaling (advanced)** — run the same image as a `gh-runner-scaler`
sidecar with `RUNNER_ROLE=autoscaler` (or pass `autoscaler` as the
command argument, or override `entrypoint:` — all three forms produce
an identical container). The scaler polls the GitHub API for the
busy/online ratio and asks a configurable `SCALE_BACKEND` to adjust
the pool size:
- `compose` *(default)* — calls `docker compose --scale`. Plain Docker hosts.
- `exec` — invokes a user-supplied wrapper (`count`/`scale`/`remove` verbs). Portable to Balena CLI, kubectl, Nomad, etc.
- `emit` — read-only mode that writes a JSON state file each interval for an external scheduler to consume.
If you run more than one fleet against the same org/repo (e.g. arm64 + x64),
scope each sidecar to its own fleet with `RUNNER_SCOPE_LABELS` (subset
match, case-insensitive) and/or `RUNNER_SCOPE_NAME_REGEX` — otherwise the
autoscaler blends every fleet's metrics and makes the wrong decision.
See the Advanced: Dynamic scaling section of the README for the full env-var reference.
**Docker-in-Docker**: Mount the Docker socket (`/var/run/docker.sock`) and
set `DOCKER_IN_DOCKER=true` to enable container-based GitHub Actions (e.g.
`uses: docker://image`). Granting socket access is host-root equivalent; on
shared/public CI prefer a docker-socket-proxy.
**Health Monitoring**: The image includes a Docker `HEALTHCHECK` that combines
process liveness with an active "online" sentinel refreshed by the heartbeat
service. When `GITHUB_PAT` (or `GITHUB_TOKEN`) is provided, the heartbeat also
queries the GitHub API to verify the runner shows as `online`. If it stays
offline for `ONLINE_FAIL_THRESHOLD` consecutive checks the container takes
`ON_OFFLINE_ACTION` (default: restart the s6 service). Container restart
policies (`restart: always`) handle the rest.
**Idle recycle policy**: The heartbeat also tracks how long the runner has
been continuously **idle** (no `Runner.Worker` child) and recycles long-idle
processes via `IDLE_RECYCLE_ACTION` (default `shutdown`, so the orchestrator's
`restart: always` brings the container back fresh). The default
`IDLE_RECYCLE_AFTER` is ephemeral-aware: **6 hours** when
`RUNNER_EPHEMERAL=true` (matches the "fresh state per job" intent) and **2
days** when `RUNNER_EPHEMERAL=false` (long-lived listener hygiene). The timer
resets the instant a worker starts, so an idle recycle never interrupts a
running job. Override with `IDLE_RECYCLE_AFTER=<seconds>` or disable with
`IDLE_RECYCLE_AFTER=0`. The startup banner and `HEALTH HEARTBEAT` both show
the effective value and its provenance.
**Stale offline runner cleanup**: Crashes, SIGKILL, host reboots, and ephemeral
job exits all leave a runner registered as `offline` in GitHub. Set
`CLEANUP_OFFLINE_RUNNERS=true` (and provide a `GITHUB_PAT`/`GITHUB_TOKEN`) to
have the container DELETE offline runners via the API at startup.
`CLEANUP_OFFLINE_IMMEDIATE` auto-resolves to `true` when `RUNNER_EPHEMERAL=true`
and `false` otherwise (persistent runners get a `CLEANUP_OFFLINE_AFTER` grace
window, default 24 h). The runner this container is about to register is
always skipped; `CLEANUP_OFFLINE_MAX` (default 25) caps removals per sweep.
Use `CLEANUP_OFFLINE_DRY_RUN=true` to preview the first sweep.
For fleets that scope `CLEANUP_OFFLINE_NAME_REGEX` to their own runners but
also want to garbage-collect orphaned runners registered by anyone else,
enable the independent any-name sweep: `CLEANUP_OFFLINE_ANY_NAME=true`. It
removes every offline runner older than `CLEANUP_OFFLINE_ANY_NAME_AFTER`
seconds (default 7 d; hard floor 24 h) regardless of name or labels,
always in threshold-mode (immediate mode does not apply). Shares the same
state file, max cap, and dry-run flag as the other sweeps.
**Read-only rootfs**: `read_only: true` is not supported out of the box (the
runner writes registration state to `/opt/runner-bin`). For a hardened posture
rely on `cap_drop: ALL` plus the minimum capability set, and tmpfs for `/run`,
`/tmp`, `/var/log`. `no-new-privileges:true` is also available but **only** in
combination with `RUNNER_SUDO=false` — with the default `RUNNER_SUDO=true` it
silently breaks every `sudo apt-get install …` step (PR_SET_NO_NEW_PRIVS
blocks sudo, a setuid binary, from elevating).
**Filesystem layout**: The runner runs directly out of `/opt/runner-bin`,
which is the directory `config.sh` / `run.sh` write `.runner`,
`.credentials`, `_diag/`, and `_work/` into. Do **not** mount a tmpfs or
volume on top of `/opt/runner-bin` — the registration state lives there.
**Balena deployment**: This image supports deployment to Balena-powered IoT and
edge devices using `balena push <your-app-slug>`. See the `balena.yml` file and
[Balena documentation](https://docs.balena.io/) for details.
# hardware acceleration
readme_hwaccel: false
# read-only support
readonly_supported: false
readonly_message: |
A blanket `read_only: true` on the container is not supported out of the
box. The runner writes its registration state (`.runner`, `.credentials`,
`_diag/`, `_work/`) into `/opt/runner-bin`, which is on the container's
writable layer. Setting `read_only: true` blocks those writes and the
runner fails to register.
For a hardened posture, use the defaults already shipped with the image:
- `cap_drop: [ ALL ]` plus only `CHOWN, SETUID, SETGID, DAC_OVERRIDE, FOWNER, AUDIT_WRITE`
(`AUDIT_WRITE` lets `sudo` emit kernel audit records without
printing a benign "unable to send audit message: Operation not
permitted" warning to stderr on every invocation)
- tmpfs at `/run`, `/tmp`, `/var/log` so log/scratch data lives in volatile memory
- `RUNNER_EPHEMERAL=true` so any one container only handles one job before being recreated
- pin the image tag (`:<runner-version>` or `:sha-<commit>`) and audit upgrades
- `security_opt: [ no-new-privileges:true ]` is available as belt-and-braces
hardening but **only** alongside `RUNNER_SUDO=false` — it is mutually
exclusive with the image's default NOPASSWD sudo for the runner user
# non-root support
nonroot_supported: true
nonroot_message: |
Two modes are supported:
1. Default (recommended): container starts as root, then the runner service
is dropped to the LSIO `abc` user (uid 911) via s6-setuidgid. PUID/PGID,
Docker Mods, and EXTRA_PACKAGES all work normally.
2. LSIO non-root opt-in (https://docs.linuxserver.io/misc/non-root/): set
`user: "911:911"` (or any uid:gid) on the service. The container's PID 1
runs unprivileged. Caveats: PUID/PGID and EXTRA_PACKAGES are ignored;
/run tmpfs MUST be owned by the chosen uid (required for s6); the chosen
uid must have docker-socket access for Docker-in-Docker actions. See the
README for a full compose example.