feat(gitea): docker-mode support in add-host-runner.sh + capacity guidance

- add-host-runner.sh: optional [mode] arg (host|docker); docker mode sets
  dedicated 'docker' label, container.docker_host/force_pull/options, and
  appends host.docker.internal to NO_PROXY so containerized jobs reach the
  host Gitea through the corp proxy (avoids HTTP 504)
- GITEA_VM_SETUP.md 11.5: docker-mode runner setup + proxy-bypass caveat;
  fleet now 3 host runners x capacity 3 + 1 docker runner

Validated: runs-on: docker job runs in Ubuntu 24.04 container and reaches
Gitea /api/v1/version.
This commit is contained in:
saravanakumardb1 2026-05-28 19:00:00 -07:00
parent 0e89dafa43
commit 6381cabe68
2 changed files with 72 additions and 10 deletions

View File

@ -445,8 +445,36 @@ bash scripts/gitea/add-host-runner.sh 3 2
- idempotent: re-running just reloads the service
The Homebrew `act_runner` service is runner #1; `add-host-runner.sh` adds
#2, #3, … Three runners × capacity 2 ≈ **6 parallel job slots**. Verified:
pushing a multi-job workflow lights up all three runners simultaneously.
#2, #3, … Current fleet: 3 host runners × capacity 3 ≈ **9 parallel host
slots**. Verified: pushing a multi-job workflow distributes jobs across all
three runners simultaneously.
**Add a docker-mode runner (stronger isolation):**
```bash
# Pull the act image once (≈2.3 GB; works through the corp proxy):
docker pull catthehacker/ubuntu:act-latest
# Stand up runner #4 in docker mode (capacity 1):
bash scripts/gitea/add-host-runner.sh 4 1 docker
```
Docker mode advertises a dedicated **`docker`** label (not `ubuntu-latest`),
so it does not hijack the host-mode `ubuntu-latest` jobs. Opt a job in with
`runs-on: docker`. The generated config:
- sets `runner.labels` to `docker:docker://<image>` (act_runner reads labels
from the config file, **not** `register --labels`)
- `container.docker_host: "-"`, `force_pull: false` (use the locally-pulled
image), `options: --add-host=host.docker.internal:host-gateway`
- **adds `host.docker.internal` to `NO_PROXY`/`no_proxy`** — without this,
containerized jobs inherit the corp proxy env and route
`host.docker.internal:3300` through the proxy, getting an HTTP 504. Jobs must
reach Gitea via `host.docker.internal:3300` (not `localhost`) from inside the
container.
Validated end-to-end: a `runs-on: docker` job runs in an `Ubuntu 24.04`
container and reaches Gitea (`GET /api/v1/version` → `{"version":"…"}`).
List + prune the fleet:

View File

@ -16,8 +16,13 @@
# - its own launchd plist ~/Library/LaunchAgents/com.bytelyst.act_runner-<N>.plist
#
# Usage:
# bash add-host-runner.sh <N> [capacity] # e.g. add-host-runner.sh 2 2
# bash add-host-runner.sh 2 2 && bash add-host-runner.sh 3 2
# bash add-host-runner.sh <N> [capacity] [mode] # mode: host (default) | docker
# bash add-host-runner.sh 2 2 && bash add-host-runner.sh 3 2 # two host runners
# bash add-host-runner.sh 4 1 docker # a docker-mode runner
#
# Docker mode advertises a dedicated `docker` label (not `ubuntu-latest`) so it
# does not hijack existing host-mode jobs. Target it with `runs-on: docker` and
# reach Gitea from inside the job container via host.docker.internal:3300.
#
# Requires: act_runner on PATH, a Gitea admin PAT at ~/.gitea_c5_pat,
# and the canonical runner.env to already exist (created during runner hardening).
@ -25,9 +30,11 @@
# Idempotent: if runner <N> is already registered it reloads the service and exits.
set -euo pipefail
N="${1:?usage: add-host-runner.sh <N> [capacity]}"
N="${1:?usage: add-host-runner.sh <N> [capacity] [mode] (mode: host|docker)}"
CAP="${2:-2}"
MODE="${3:-host}" # host | docker
INSTANCE="${GITEA_INSTANCE:-http://localhost:3300}"
DOCKER_IMAGE="${RUNNER_DOCKER_IMAGE:-catthehacker/ubuntu:act-latest}"
PAT_FILE="${GITEA_PAT_FILE:-$HOME/.gitea_c5_pat}"
CANONICAL_CONFIG="${CANONICAL_CONFIG:-/opt/homebrew/etc/act_runner/config.yaml}"
SHARED_ENV_FILE="${SHARED_ENV_FILE:-/opt/homebrew/etc/act_runner/runner.env}"
@ -48,7 +55,18 @@ PAT="$(cat "$PAT_FILE")"
mkdir -p "$BASE" "$WORKDIR" "$LOG_DIR"
echo "── add host runner #$N (capacity $CAP) ──"
echo "── add $MODE runner #$N (capacity $CAP) ──"
[ "$MODE" = "docker" ] && echo " image : $DOCKER_IMAGE"
# ── labels per mode (act_runner uses config-file labels, not --labels) ──────
if [ "$MODE" = "docker" ]; then
# Dedicated `docker` label so existing `runs-on: ubuntu-latest` jobs (which
# assume host mode + localhost Gitea) are NOT hijacked. Opt in with
# `runs-on: docker` and reach Gitea via host.docker.internal.
LABELS="docker:docker://$DOCKER_IMAGE,ubuntu-docker:docker://$DOCKER_IMAGE,self-hosted:host"
else
LABELS="ubuntu-latest:host,macos-latest:host,macos-15:host,self-hosted:host"
fi
# ── already registered? just (re)load the service ───────────────────────────
if [ -f "$RUNNER_FILE" ]; then
@ -61,16 +79,32 @@ fi
# ── derive a per-runner config from the canonical one ───────────────────────
# Preserve the proxy/env block + env_file; override file path, capacity, workdir.
python3 - "$CANONICAL_CONFIG" "$CONFIG" "$RUNNER_FILE" "$CAP" "$WORKDIR" "$SHARED_ENV_FILE" <<'PY'
python3 - "$CANONICAL_CONFIG" "$CONFIG" "$RUNNER_FILE" "$CAP" "$WORKDIR" "$SHARED_ENV_FILE" "$MODE" "$LABELS" <<'PY'
import sys, yaml
src, dst, runner_file, cap, workdir, env_file = sys.argv[1:7]
src, dst, runner_file, cap, workdir, env_file, mode, labels = sys.argv[1:9]
cfg = yaml.safe_load(open(src)) or {}
cfg.setdefault("runner", {})
cfg["runner"]["file"] = runner_file
cfg["runner"]["capacity"] = int(cap)
cfg["runner"]["env_file"] = env_file
# act_runner reads labels from the config file (it ignores `register --labels`).
cfg["runner"]["labels"] = labels.split(",")
cfg.setdefault("host", {})
cfg["host"]["workdir_parent"] = workdir
if mode == "docker":
c = cfg.setdefault("container", {})
c["docker_host"] = "-" # auto-detect host docker daemon
c["force_pull"] = False # use locally-pulled image (corp proxy)
c["privileged"] = False
# Let job containers reach the host's Gitea on Docker Desktop.
c["options"] = "--add-host=host.docker.internal:host-gateway"
# Containerized jobs inherit the corp proxy env; without this they route
# host.docker.internal:3300 through the proxy and get a 504. Bypass it.
envs = cfg["runner"].setdefault("envs", {})
for k in ("NO_PROXY", "no_proxy", "NPM_CONFIG_NOPROXY"):
v = envs.get(k, "")
if "host.docker.internal" not in v:
envs[k] = (v + "," if v else "") + "host.docker.internal"
yaml.safe_dump(cfg, open(dst, "w"), default_flow_style=False, sort_keys=False)
print(f" + wrote {dst}")
PY
@ -81,13 +115,13 @@ REG_TOKEN=$(curl -fsS -H "Authorization: token $PAT" \
| python3 -c "import json,sys; print(json.load(sys.stdin)['token'])")
[ -n "$REG_TOKEN" ] || { echo "✗ could not fetch registration token" >&2; exit 1; }
# ── register (host-mode labels) ─────────────────────────────────────────────
# ── register (labels come from config file; --labels is informational) ──────
act_runner register \
--no-interactive \
--instance "$INSTANCE" \
--token "$REG_TOKEN" \
--name "$RUNNER_NAME" \
--labels "ubuntu-latest:host,macos-latest:host,macos-15:host,self-hosted:host" \
--labels "$LABELS" \
--config "$CONFIG"
echo " ✓ registered as $RUNNER_NAME"