This commit is contained in:
Jason Stedwell
2026-06-29 21:56:12 -05:00
parent bc207c1cf7
commit 1757777a32
32 changed files with 1468 additions and 44 deletions
+48
View File
@@ -0,0 +1,48 @@
# docker-compose.example.yml — custom-gitea-runner deployment
# Copy and adjust for your environment. Use pinned image tags in production.
#
# Modes:
# Mode A (pre-generated config): mount config/runner.yaml — set RUNNER_CONFIG_PATH
# Mode B (bootstrap on first run): set GITEA_INSTANCE_URL, GITEA_RUNNER_TOKEN, GITEA_RUNNER_NAME
services:
gitea-runner:
image: registry.alwisp.com/custom-gitea-runner:v0.1.0 # pin your version here
container_name: gitea-runner
restart: unless-stopped
environment:
# ── Required for Mode B (bootstrap registration) ───────────────────────
GITEA_INSTANCE_URL: https://git.example.com
GITEA_RUNNER_NAME: unraid-runner-01
GITEA_RUNNER_TOKEN: ${GITEA_RUNNER_TOKEN} # set in .env file
# ── Optional ──────────────────────────────────────────────────────────
GITEA_RUNNER_LABELS: ubuntu-latest:docker://node:20-bullseye
RUNNER_CONFIG_PATH: /config/runner.yaml
RUNNER_WORKDIR: /work
TZ: America/Chicago
volumes:
# Runner config (persisted across restarts; generated on first boot in Mode B)
- ./config:/config
# Job workspace (writable scratch space for running jobs)
- ./work:/work
# Docker socket — required for docker build/push workflows
# Grants the runner full Docker daemon access. Understand the trust model.
- /var/run/docker.sock:/var/run/docker.sock
# Limit resources to avoid runaway jobs starving the host
deploy:
resources:
limits:
cpus: "4"
memory: 4G
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
+75
View File
@@ -0,0 +1,75 @@
# Unraid Template Notes — custom-gitea-runner
This document covers the recommended Unraid Docker container configuration for the custom-gitea-runner.
---
## Container basics
| Field | Value |
|---|---|
| Repository | `registry.alwisp.com/custom-gitea-runner:v0.1.0` |
| Container name | `gitea-runner` |
| Network type | `Bridge` |
| Privileged | `No` (leave unchecked — socket mount handles Docker access) |
---
## Environment variables
Set these in the "Environment Variables" section of the Unraid container template.
| Variable | Required | Value |
|---|---|---|
| `GITEA_INSTANCE_URL` | Yes | `https://git.example.com` |
| `GITEA_RUNNER_TOKEN` | Yes | Registration token from Gitea admin |
| `GITEA_RUNNER_NAME` | Yes | e.g. `unraid-runner-01` |
| `GITEA_RUNNER_LABELS` | No | `ubuntu-latest:docker://node:20-bullseye` |
| `RUNNER_CONFIG_PATH` | No | `/config/runner.yaml` |
| `RUNNER_WORKDIR` | No | `/work` |
| `TZ` | No | `America/Chicago` |
---
## Volume mounts
| Container path | Host path | Notes |
|---|---|---|
| `/config` | `/mnt/user/appdata/gitea-runner/config` | Config persistence. Generated on first boot. |
| `/work` | `/mnt/user/appdata/gitea-runner/work` | Job workspace. Can be cleaned between runs. |
| `/var/run/docker.sock` | `/var/run/docker.sock` | Docker socket. Required for build/push workflows. |
---
## Post-install checklist
1. Set all required environment variables.
2. Start the container.
3. Check logs — look for `[BOOTSTRAP] Registration complete` or `[START] Starting act_runner daemon`.
4. In Gitea admin panel, verify the runner appears under **Admin → Runners**.
5. Trigger a test workflow and confirm job pickup.
---
## Update workflow
1. Pull the new image tag from the registry.
2. In Unraid, update the Repository field to the new pinned tag.
3. Stop → Start the container (Unraid recreates from the new image).
4. Confirm registration still valid in Gitea admin panel.
5. Run a smoke workflow.
---
## Rollback workflow
1. Stop the container.
2. Change the Repository field back to the prior known-good tag.
3. Start the container.
4. Verify registration and smoke workflow.
---
## Notes on Docker socket
Mounting `/var/run/docker.sock` gives the runner access to the host Docker daemon. This is intentional for build/push workflows. Understand this means jobs running on this runner can interact with all containers on the Unraid host. For a self-owned, trusted environment this is acceptable. Future hardening options are documented in `docs/architecture.md`.
+83
View File
@@ -0,0 +1,83 @@
# syntax=docker/dockerfile:1
# custom-gitea-runner — self-owned Gitea Actions runner
# All dependencies installed at build time; no runtime apt-get.
ARG UBUNTU_VERSION=22.04
FROM ubuntu:${UBUNTU_VERSION}
ARG ACT_RUNNER_VERSION=0.2.11
ARG NODE_MAJOR=20
ARG BUILD_DATE
ARG VCS_REF
ARG VERSION=dev
LABEL org.opencontainers.image.title="custom-gitea-runner" \
org.opencontainers.image.description="Self-owned Gitea Actions runner with bundled tooling" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.revision="${VCS_REF}"
ENV DEBIAN_FRONTEND=noninteractive \
IMAGE_VERSION="${VERSION}" \
GIT_REVISION="${VCS_REF}" \
RUNNER_CONFIG_PATH=/config/runner.yaml \
RUNNER_WORKDIR=/work
# ─── Base packages ────────────────────────────────────────────────────────────
RUN apt-get update && apt-get install -y --no-install-recommends \
bash \
ca-certificates \
curl \
git \
gnupg \
jq \
tini \
tzdata \
&& rm -rf /var/lib/apt/lists/*
# ─── Node.js (NodeSource — Ubuntu 22.04 default is 12.x, too old) ─────────────
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_MAJOR}.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/*
# ─── Docker CLI ───────────────────────────────────────────────────────────────
RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
| gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
> /etc/apt/sources.list.d/docker.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends docker-ce-cli \
&& rm -rf /var/lib/apt/lists/*
# ─── act_runner binary (pinned) ───────────────────────────────────────────────
# To update: bump ACT_RUNNER_VERSION in the build command or here.
# Releases: https://gitea.com/gitea/act_runner/releases
RUN ARCH=$(dpkg --print-architecture) \
&& curl -fsSL \
"https://gitea.com/gitea/act_runner/releases/download/v${ACT_RUNNER_VERSION}/act_runner-${ACT_RUNNER_VERSION}-linux-${ARCH}" \
-o /usr/local/bin/act_runner \
&& chmod +x /usr/local/bin/act_runner
# ─── Runtime directories and scripts ─────────────────────────────────────────
RUN mkdir -p /config /work /config/trusted-ca
COPY docker/runner/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
COPY docker/runner/healthcheck.sh /usr/local/bin/healthcheck.sh
COPY docker/runner/smoke-test.sh /usr/local/bin/smoke-test.sh
RUN chmod +x \
/usr/local/bin/docker-entrypoint.sh \
/usr/local/bin/healthcheck.sh \
/usr/local/bin/smoke-test.sh
VOLUME ["/config", "/work"]
HEALTHCHECK \
--interval=30s \
--timeout=10s \
--start-period=60s \
--retries=3 \
CMD ["/usr/local/bin/healthcheck.sh"]
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/docker-entrypoint.sh"]
+139
View File
@@ -0,0 +1,139 @@
#!/usr/bin/env bash
# docker-entrypoint.sh — custom-gitea-runner bootstrap and daemon startup
# Runs as PID 1 via tini. Handles validation, optional CA trust, registration, and daemon start.
set -euo pipefail
RUNNER_CONFIG_PATH="${RUNNER_CONFIG_PATH:-/config/runner.yaml}"
RUNNER_WORKDIR="${RUNNER_WORKDIR:-/work}"
# ─── Banner ───────────────────────────────────────────────────────────────────
print_banner() {
echo "======================================================"
echo " custom-gitea-runner"
echo " Image version : ${IMAGE_VERSION:-unknown}"
echo " Git revision : ${GIT_REVISION:-unknown}"
echo "======================================================"
echo "Runtime:"
echo " act_runner : $(act_runner --version 2>&1 | head -1 || echo 'unknown')"
echo " node : $(node --version 2>&1 || echo 'unknown')"
echo " npm : $(npm --version 2>&1 || echo 'unknown')"
echo " git : $(git --version 2>&1 | awk '{print $3}' || echo 'unknown')"
echo " docker : $(docker --version 2>&1 | awk '{print $3}' | tr -d ',' || echo 'unavailable')"
echo "Config:"
echo " Config path : ${RUNNER_CONFIG_PATH}"
echo " Work dir : ${RUNNER_WORKDIR}"
echo " Gitea URL : ${GITEA_INSTANCE_URL:-[not set]}"
echo " Runner name : ${GITEA_RUNNER_NAME:-[not set]}"
echo " Labels : ${GITEA_RUNNER_LABELS:-[not set]}"
echo "======================================================"
}
# ─── Binary validation ────────────────────────────────────────────────────────
validate_binaries() {
local missing=0
for bin in act_runner git node npm; do
if ! command -v "$bin" &>/dev/null; then
echo "[ERROR] Required binary not found: ${bin}"
missing=1
fi
done
if [ "$missing" -ne 0 ]; then
echo "[FATAL] Missing required binaries. This is an image build issue."
exit 1
fi
echo "[OK] All required binaries present."
}
# ─── Custom CA trust (optional) ───────────────────────────────────────────────
install_custom_ca() {
local ca_dir="/config/trusted-ca"
if [ -d "$ca_dir" ] && compgen -G "${ca_dir}/*.crt" >/dev/null 2>&1; then
echo "[CA] Installing custom CA certificates from ${ca_dir} ..."
cp "${ca_dir}"/*.crt /usr/local/share/ca-certificates/ 2>/dev/null || true
update-ca-certificates --fresh 2>&1 | grep -v "^$" || true
echo "[CA] Custom CA trust updated."
fi
}
# ─── Registration (Mode B: bootstrap from env vars) ───────────────────────────
require_env() {
local var="$1"
if [ -z "${!var:-}" ]; then
echo "[FATAL] Required environment variable not set: ${var}"
exit 1
fi
}
# Rewrite the `labels:` list in the runner config. act_runner reads labels
# from the config file at registration time and IGNORES the `register --labels`
# flag whenever the config already exists — so to honor GITEA_RUNNER_LABELS we
# must edit the generated config before registering.
apply_labels() {
local labels_csv="$1" config="$2" tmp
echo "[BOOTSTRAP] Applying labels: ${labels_csv}"
tmp="$(mktemp)"
awk -v csv="${labels_csv}" '
BEGIN { n = split(csv, arr, ",") }
/^[[:space:]]+labels:[[:space:]]*$/ {
print
for (i = 1; i <= n; i++) {
gsub(/^[ \t]+|[ \t]+$/, "", arr[i])
printf " - \"%s\"\n", arr[i]
}
inlabels = 1
next
}
inlabels == 1 {
if ($0 ~ /^[[:space:]]+-[[:space:]]/) { next }
inlabels = 0
}
{ print }
' "${config}" > "${tmp}" && mv "${tmp}" "${config}"
}
register_runner() {
echo "[BOOTSTRAP] No config found at ${RUNNER_CONFIG_PATH}. Initiating registration."
require_env GITEA_INSTANCE_URL
require_env GITEA_RUNNER_TOKEN
require_env GITEA_RUNNER_NAME
mkdir -p "$(dirname "${RUNNER_CONFIG_PATH}")"
# Generate the default config template (includes cache/container defaults),
# then inject the operator's labels into it before registering.
act_runner generate-config > "${RUNNER_CONFIG_PATH}"
local labels="${GITEA_RUNNER_LABELS:-ubuntu-latest:docker://node:20-bullseye}"
apply_labels "${labels}" "${RUNNER_CONFIG_PATH}"
act_runner register \
--config "${RUNNER_CONFIG_PATH}" \
--instance "${GITEA_INSTANCE_URL}" \
--token "${GITEA_RUNNER_TOKEN}" \
--name "${GITEA_RUNNER_NAME}" \
--no-interactive
echo "[BOOTSTRAP] Registration complete. Config at ${RUNNER_CONFIG_PATH}."
}
# ─── Main ─────────────────────────────────────────────────────────────────────
print_banner
validate_binaries
install_custom_ca
mkdir -p "${RUNNER_WORKDIR}"
if [ ! -f "${RUNNER_CONFIG_PATH}" ]; then
register_runner
else
echo "[INFO] Existing config found at ${RUNNER_CONFIG_PATH}. Skipping registration."
fi
echo "[START] Starting act_runner daemon ..."
exec act_runner daemon --config "${RUNNER_CONFIG_PATH}"
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# healthcheck.sh — reports runner health for Docker HEALTHCHECK instruction
set -euo pipefail
RUNNER_CONFIG_PATH="${RUNNER_CONFIG_PATH:-/config/runner.yaml}"
exit_code=0
check_ok() { echo "[HEALTH] OK : $1"; }
check_warn() { echo "[HEALTH] WARN : $1"; }
check_fail() { echo "[HEALTH] FAIL : $1"; exit_code=1; }
# Runner process
if pgrep -x act_runner > /dev/null 2>&1; then
check_ok "act_runner process is running"
else
check_fail "act_runner process not found"
fi
# Config file
if [ -f "${RUNNER_CONFIG_PATH}" ]; then
check_ok "Config file present at ${RUNNER_CONFIG_PATH}"
else
check_warn "Config file not found at ${RUNNER_CONFIG_PATH}"
fi
# Required binaries
for bin in act_runner git node npm; do
if command -v "$bin" &>/dev/null; then
check_ok "Binary present: ${bin}"
else
check_fail "Binary missing: ${bin}"
fi
done
# Docker socket (warn only — some label configurations don't need it)
if [ -S /var/run/docker.sock ]; then
if docker info &>/dev/null 2>&1; then
check_ok "Docker socket accessible"
else
check_warn "Docker socket present but not accessible (permission issue?)"
fi
else
check_warn "Docker socket not mounted — Docker build/push workflows will fail"
fi
exit $exit_code
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# smoke-test.sh — quick runtime self-test for the runner image
# Run inside the container to validate the tool chain before production use.
set -euo pipefail
PASS=0
FAIL=0
ok() { echo "[SMOKE] PASS : $1"; PASS=$((PASS + 1)); }
fail() { echo "[SMOKE] FAIL : $1"; FAIL=$((FAIL + 1)); }
echo "======================================"
echo " custom-gitea-runner smoke test"
echo "======================================"
# Binary presence and basic execution
for bin in bash curl git jq node npm act_runner tini docker; do
if command -v "$bin" &>/dev/null; then
ok "${bin} found: $(command -v "$bin")"
else
fail "${bin} not found"
fi
done
# Version output checks
echo "--- versions ---"
git --version && ok "git --version" || fail "git --version failed"
node --version && ok "node --version" || fail "node --version failed"
npm --version && ok "npm --version" || fail "npm --version failed"
act_runner --version 2>&1 | head -1 && ok "act_runner --version" || fail "act_runner --version failed"
docker --version && ok "docker --version" || fail "docker --version failed"
# Node.js can run a basic script
if node -e "process.exit(0)" 2>/dev/null; then
ok "node can execute scripts"
else
fail "node cannot execute scripts"
fi
# jq parses JSON
if echo '{"test":1}' | jq .test > /dev/null 2>&1; then
ok "jq parses JSON"
else
fail "jq cannot parse JSON"
fi
# curl is usable (don't actually make a request in smoke test)
if curl --version &>/dev/null; then
ok "curl is usable"
else
fail "curl is not usable"
fi
echo "======================================"
echo " Results: ${PASS} passed, ${FAIL} failed"
echo "======================================"
[ "$FAIL" -eq 0 ] && exit 0 || exit 1