Last active
May 6, 2026 12:42
-
-
Save terry90/3edf816e4cd7100367947b46bd106365 to your computer and use it in GitHub Desktop.
Install gitlab runner on LXC
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| set -euo pipefail | |
| # ---------- config (override via env) ---------- | |
| GITLAB_URL="${GITLAB_URL:-}" | |
| RUNNER_TOKEN="${RUNNER_TOKEN:-}" # new flow: glrt-... | |
| REGISTRATION_TOKEN="${REGISTRATION_TOKEN:-}" # legacy flow | |
| RUNNER_NAME="${RUNNER_NAME:-$(hostname)-runner}" | |
| RUNNER_TAGS="${RUNNER_TAGS:-lxc,docker}" # legacy flow only | |
| RUNNER_EXECUTOR="${RUNNER_EXECUTOR:-docker}" # docker | shell | |
| RUNNER_DEFAULT_IMAGE="${RUNNER_DEFAULT_IMAGE:-alpine:latest}" | |
| INSTALL_DOCKER="${INSTALL_DOCKER:-yes}" # yes | no | |
| CONCURRENT="${CONCURRENT:-2}" | |
| FORCE_REREGISTER="${FORCE_REREGISTER:-no}" # yes to drop existing & re-register | |
| # Docker cleanup (systemd timer) | |
| INSTALL_CLEANUP="${INSTALL_CLEANUP:-yes}" # yes | no | |
| CLEANUP_SCHEDULE="${CLEANUP_SCHEDULE:-daily}" # any OnCalendar value, e.g. 'daily', 'weekly', '03:30' | |
| CLEANUP_KEEP_HOURS="${CLEANUP_KEEP_HOURS:-72}" # only prune things unused for N hours | |
| CLEANUP_DISK_THRESHOLD="${CLEANUP_DISK_THRESHOLD:-80}" # also force prune if / use% > this | |
| CONFIG_FILE="/etc/gitlab-runner/config.toml" | |
| # ---------- helpers ---------- | |
| log() { printf '\033[1;32m[+]\033[0m %s\n' "$*"; } | |
| warn() { printf '\033[1;33m[!]\033[0m %s\n' "$*" >&2; } | |
| err() { printf '\033[1;31m[x]\033[0m %s\n' "$*" >&2; exit 1; } | |
| [[ $EUID -eq 0 ]] || err "Run as root." | |
| # ---------- detect distro ---------- | |
| . /etc/os-release | |
| case "${ID:-}" in | |
| debian|ubuntu) : ;; | |
| *) err "Unsupported distro: ${ID:-unknown}. Use a Debian/Ubuntu LXC template." ;; | |
| esac | |
| log "Detected ${PRETTY_NAME}" | |
| export DEBIAN_FRONTEND=noninteractive | |
| # ---------- prerequisites ---------- | |
| log "Ensuring prerequisites..." | |
| apt-get update -y | |
| apt-get install -y curl ca-certificates gnupg lsb-release apt-transport-https debian-archive-keyring | |
| # ---------- Docker (optional) ---------- | |
| if [[ "$INSTALL_DOCKER" == "yes" && "$RUNNER_EXECUTOR" == "docker" ]]; then | |
| if ! command -v docker >/dev/null 2>&1; then | |
| log "Installing Docker CE..." | |
| install -m 0755 -d /etc/apt/keyrings | |
| if [[ ! -f /etc/apt/keyrings/docker.gpg ]]; then | |
| curl -fsSL "https://download.docker.com/linux/${ID}/gpg" \ | |
| | gpg --dearmor -o /etc/apt/keyrings/docker.gpg | |
| chmod a+r /etc/apt/keyrings/docker.gpg | |
| fi | |
| echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ | |
| https://download.docker.com/linux/${ID} $(. /etc/os-release && echo "${VERSION_CODENAME}") stable" \ | |
| > /etc/apt/sources.list.d/docker.list | |
| apt-get update -y | |
| apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin | |
| systemctl enable --now docker | |
| else | |
| log "Docker already installed: $(docker --version)" | |
| fi | |
| if ! docker info >/dev/null 2>&1; then | |
| warn "Docker is installed but 'docker info' failed." | |
| warn "On the Proxmox host, ensure the LXC has features: nesting=1,keyctl=1" | |
| warn " pct set <vmid> -features nesting=1,keyctl=1" | |
| warn "Then restart the container." | |
| fi | |
| fi | |
| # ---------- gitlab-runner package ---------- | |
| if ! command -v gitlab-runner >/dev/null 2>&1; then | |
| log "Adding GitLab Runner apt repository..." | |
| if [[ ! -f /etc/apt/sources.list.d/runner_gitlab-runner.list ]]; then | |
| curl -fsSL "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | bash | |
| fi | |
| log "Installing gitlab-runner..." | |
| apt-get install -y gitlab-runner | |
| else | |
| log "gitlab-runner already installed: $(gitlab-runner --version | head -n1)" | |
| fi | |
| systemctl enable --now gitlab-runner | |
| # ---------- prompt for URL ---------- | |
| if [[ -z "$GITLAB_URL" ]]; then | |
| read -rp "GitLab URL [https://gitlab.com/]: " GITLAB_URL | |
| GITLAB_URL="${GITLAB_URL:-https://gitlab.com/}" | |
| fi | |
| # ---------- clean up stale runners ---------- | |
| # `verify --delete` removes any [[runners]] entry that GitLab no longer knows | |
| # about (e.g. you deleted it in the UI). This keeps config.toml tidy across runs. | |
| log "Verifying existing runners against GitLab..." | |
| gitlab-runner verify --delete || true | |
| # ---------- decide whether to register ---------- | |
| NEED_REGISTER="yes" | |
| if [[ "$FORCE_REREGISTER" == "yes" ]]; then | |
| log "FORCE_REREGISTER=yes → unregistering any runner named '${RUNNER_NAME}'" | |
| gitlab-runner unregister --name "$RUNNER_NAME" || true | |
| elif gitlab-runner list 2>&1 | grep -qE "^${RUNNER_NAME}\b|Name=${RUNNER_NAME}\b"; then | |
| log "A runner named '${RUNNER_NAME}' is already registered and verified — skipping registration." | |
| log " (set FORCE_REREGISTER=yes to replace it)" | |
| NEED_REGISTER="no" | |
| fi | |
| # ---------- choose registration mode (only if needed) ---------- | |
| if [[ "$NEED_REGISTER" == "yes" && -z "$RUNNER_TOKEN" && -z "$REGISTRATION_TOKEN" ]]; then | |
| echo | |
| echo "Choose registration mode:" | |
| echo " 1) NEW flow — paste an authentication token (glrt-...) created in the GitLab UI" | |
| echo " 2) LEGACY flow — paste a registration token (deprecated)" | |
| read -rp "Mode [1]: " MODE | |
| MODE="${MODE:-1}" | |
| if [[ "$MODE" == "1" ]]; then | |
| read -rsp "Authentication token (glrt-...): " RUNNER_TOKEN; echo | |
| else | |
| read -rsp "Registration token: " REGISTRATION_TOKEN; echo | |
| fi | |
| fi | |
| # ---------- register ---------- | |
| if [[ "$NEED_REGISTER" == "yes" ]]; then | |
| if [[ -n "$RUNNER_TOKEN" ]]; then | |
| log "Registering '${RUNNER_NAME}' via authentication token (new flow)..." | |
| REG_ARGS=( | |
| --non-interactive | |
| --url "$GITLAB_URL" | |
| --token "$RUNNER_TOKEN" | |
| --description "$RUNNER_NAME" | |
| --executor "$RUNNER_EXECUTOR" | |
| ) | |
| [[ "$RUNNER_EXECUTOR" == "docker" ]] && REG_ARGS+=( --docker-image "$RUNNER_DEFAULT_IMAGE" --docker-privileged=false ) | |
| gitlab-runner register "${REG_ARGS[@]}" | |
| elif [[ -n "$REGISTRATION_TOKEN" ]]; then | |
| warn "Using legacy registration-token flow (deprecated)." | |
| log "Registering '${RUNNER_NAME}' (executor=${RUNNER_EXECUTOR}, tags=${RUNNER_TAGS})..." | |
| REG_ARGS=( | |
| --non-interactive | |
| --url "$GITLAB_URL" | |
| --registration-token "$REGISTRATION_TOKEN" | |
| --description "$RUNNER_NAME" | |
| --tag-list "$RUNNER_TAGS" | |
| --executor "$RUNNER_EXECUTOR" | |
| ) | |
| [[ "$RUNNER_EXECUTOR" == "docker" ]] && REG_ARGS+=( --docker-image "$RUNNER_DEFAULT_IMAGE" --docker-privileged=false ) | |
| gitlab-runner register "${REG_ARGS[@]}" | |
| else | |
| err "No token provided. Set RUNNER_TOKEN (new) or REGISTRATION_TOKEN (legacy)." | |
| fi | |
| fi | |
| # ---------- patch config.toml (always, so this script can repair broken state) ---------- | |
| [[ -f "$CONFIG_FILE" ]] || err "Expected $CONFIG_FILE after install — something is wrong." | |
| # Backup once per run so a bad edit can be rolled back. | |
| cp -a "$CONFIG_FILE" "${CONFIG_FILE}.bak.$(date +%s)" | |
| # concurrent | |
| if grep -qE '^\s*concurrent\s*=' "$CONFIG_FILE"; then | |
| sed -i -E "s|^\s*concurrent\s*=.*|concurrent = ${CONCURRENT}|" "$CONFIG_FILE" | |
| else | |
| # Prepend if somehow missing | |
| sed -i "1i concurrent = ${CONCURRENT}" "$CONFIG_FILE" | |
| fi | |
| log "concurrent=${CONCURRENT}" | |
| # volumes (only meaningful for docker executor) | |
| if [[ "$RUNNER_EXECUTOR" == "docker" ]]; then | |
| DESIRED_VOLUMES='["/cache", "/var/run/docker.sock:/var/run/docker.sock"]' | |
| if grep -qE '^\s*volumes\s*=' "$CONFIG_FILE"; then | |
| sed -i -E "s|^(\s*)volumes\s*=\s*\[.*\]|\1volumes = ${DESIRED_VOLUMES}|" "$CONFIG_FILE" | |
| log "Updated volumes line in ${CONFIG_FILE}" | |
| elif grep -qE '^\s*\[runners\.docker\]' "$CONFIG_FILE"; then | |
| # Insert once, after the FIRST [runners.docker] header that has no volumes line below it. | |
| # awk is more robust than sed here for multi-section files. | |
| awk -v vols="$DESIRED_VOLUMES" ' | |
| BEGIN { added = 0 } | |
| { | |
| if (!added && $0 ~ /^[[:space:]]*\[runners\.docker\][[:space:]]*$/) { | |
| print " volumes = " vols | |
| added = 1 | |
| } | |
| } | |
| ' "$CONFIG_FILE" > "${CONFIG_FILE}.new" && mv "${CONFIG_FILE}.new" "$CONFIG_FILE" | |
| log "Inserted volumes line under [runners.docker]" | |
| else | |
| warn "No [runners.docker] section found — skipping volumes patch." | |
| fi | |
| warn "Docker socket is mounted into job containers — any job on this runner" | |
| warn "effectively has root on the host. Only use with trusted projects." | |
| fi | |
| # ---------- docker cleanup timer ---------- | |
| if [[ "$INSTALL_CLEANUP" == "yes" && "$RUNNER_EXECUTOR" == "docker" ]]; then | |
| log "Installing docker-cleanup systemd timer (schedule=${CLEANUP_SCHEDULE}, keep=${CLEANUP_KEEP_HOURS}h)..." | |
| cat > /usr/local/sbin/gitlab-runner-docker-cleanup <<EOF | |
| #!/usr/bin/env bash | |
| # Prune unused Docker resources to keep the LXC disk healthy. | |
| # Generated by install-gitlab-runner.sh — re-run that script to update. | |
| set -euo pipefail | |
| KEEP_HOURS=${CLEANUP_KEEP_HOURS} | |
| DISK_THRESHOLD=${CLEANUP_DISK_THRESHOLD} | |
| log() { printf '[docker-cleanup %s] %s\n' "\$(date -Is)" "\$*"; } | |
| # Disk usage of the filesystem holding /var/lib/docker (falls back to /). | |
| docker_root="\$(docker info --format '{{.DockerRootDir}}' 2>/dev/null || echo /var/lib/docker)" | |
| [[ -d "\$docker_root" ]] || docker_root=/ | |
| USE_PCT="\$(df --output=pcent "\$docker_root" | tail -n1 | tr -dc '0-9')" | |
| log "disk usage at \${docker_root}: \${USE_PCT}%" | |
| # Always prune anything older than KEEP_HOURS. | |
| log "pruning containers/networks/images/build cache older than \${KEEP_HOURS}h" | |
| docker container prune -f --filter "until=\${KEEP_HOURS}h" || true | |
| docker image prune -af --filter "until=\${KEEP_HOURS}h" || true | |
| docker network prune -f --filter "until=\${KEEP_HOURS}h" || true | |
| docker builder prune -af --filter "until=\${KEEP_HOURS}h" || true | |
| # If still above threshold, do a more aggressive sweep (ignores 'until'). | |
| if [[ "\${USE_PCT:-0}" -gt "\${DISK_THRESHOLD}" ]]; then | |
| log "above \${DISK_THRESHOLD}% — running aggressive prune" | |
| docker system prune -af --volumes || true | |
| fi | |
| # Trim the runner's /cache volume if it grows huge (>5GB). | |
| if docker volume inspect runner-cache >/dev/null 2>&1; then | |
| size_kb="\$(docker run --rm -v runner-cache:/c alpine du -sk /c 2>/dev/null | awk '{print \$1}')" | |
| if [[ -n "\${size_kb:-}" && "\$size_kb" -gt 5242880 ]]; then | |
| log "runner-cache volume is \$((size_kb/1024))MB — clearing" | |
| docker run --rm -v runner-cache:/c alpine sh -c 'rm -rf /c/* /c/.[!.]* 2>/dev/null || true' | |
| fi | |
| fi | |
| log "done — disk now: \$(df -h "\$docker_root" | tail -n1)" | |
| EOF | |
| chmod +x /usr/local/sbin/gitlab-runner-docker-cleanup | |
| cat > /etc/systemd/system/gitlab-runner-docker-cleanup.service <<EOF | |
| [Unit] | |
| Description=Prune unused Docker resources for GitLab Runner | |
| After=docker.service | |
| Requires=docker.service | |
| [Service] | |
| Type=oneshot | |
| ExecStart=/usr/local/sbin/gitlab-runner-docker-cleanup | |
| Nice=10 | |
| IOSchedulingClass=idle | |
| EOF | |
| cat > /etc/systemd/system/gitlab-runner-docker-cleanup.timer <<EOF | |
| [Unit] | |
| Description=Run docker cleanup on a schedule | |
| [Timer] | |
| OnCalendar=${CLEANUP_SCHEDULE} | |
| Persistent=true | |
| RandomizedDelaySec=15min | |
| [Install] | |
| WantedBy=timers.target | |
| EOF | |
| systemctl daemon-reload | |
| systemctl enable --now gitlab-runner-docker-cleanup.timer | |
| log "Cleanup timer active. Run on demand with: systemctl start gitlab-runner-docker-cleanup" | |
| fi | |
| # ---------- restart & report ---------- | |
| systemctl restart gitlab-runner | |
| log "Status:" | |
| gitlab-runner status || true | |
| gitlab-runner list || true | |
| if [[ "$INSTALL_CLEANUP" == "yes" && "$RUNNER_EXECUTOR" == "docker" ]]; then | |
| systemctl list-timers gitlab-runner-docker-cleanup.timer --no-pager || true | |
| fi | |
| log "Done. Verify in GitLab → CI/CD → Runners." |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment