Switch to Buildah for container builds #51

Merged
eblume merged 30 commits from feature/p5-container-builds into main 2026-01-24 13:30:26 -08:00
10 changed files with 199 additions and 62 deletions
Showing only changes of commit fdf5153130 - Show all commits

Containerize forgejo-runner with Tailscale gateway for tailnet access
Some checks failed
Test CI / test (pull_request) Failing after 48s

Architecture:
- tailscale_ci_gateway role: Runs Tailscale container on tailnet-jobs network
- forgejo_runner role: Runs runner daemon in container on same network
- Job containers also use tailnet-jobs network

This allows the runner and jobs to reach forge.tail8d86e.ts.net via
the Tailscale gateway, avoiding hairpinning issues with localhost.

Changes:
- Add tailscale_ci_gateway role with launchd management
- Refactor forgejo_runner to use containerized daemon
- Runner registers with Tailscale URL instead of localhost
- Job containers run on tailnet-jobs network
- Update playbook role ordering (gateway before runner)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Erich Blume 2026-01-24 11:28:35 -08:00

View file

@ -78,6 +78,23 @@
no_log: true
tags: [forgejo_runner]
# Tailscale CI gateway auth key (for job container tailnet access)
- name: Fetch tailscale ci-gateway auth key
ansible.builtin.command:
cmd: op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get w3663ffnvkewbftncqxtcpeavy --fields ci-gateway-ts-auth-key --reveal
delegate_to: localhost
register: _tailscale_ci_gateway_auth_key
changed_when: false
no_log: true
check_mode: false
tags: [tailscale_ci_gateway]
- name: Set tailscale ci-gateway auth key fact
ansible.builtin.set_fact:
tailscale_ci_gateway_auth_key: "{{ _tailscale_ci_gateway_auth_key.stdout }}"
no_log: true
tags: [tailscale_ci_gateway]
roles:
- role: alloy
tags: alloy
@ -99,5 +116,7 @@
tags: plex_metrics
- role: tailscale_serve
tags: tailscale-serve
- role: tailscale_ci_gateway
tags: tailscale_ci_gateway
- role: forgejo_runner
tags: forgejo_runner

View file

@ -1,25 +1,37 @@
---
forgejo_runner_repo_dir: /Users/erichblume/code/3rd/forgejo-runner
forgejo_runner_binary: "{{ forgejo_runner_repo_dir }}/forgejo-runner"
# Forgejo Runner - containerized daemon on tailnet-jobs network
#
# The runner daemon runs in a Docker container with access to the tailnet
# via the tailscale-ci-gateway. This allows it to register with Forgejo
# using the Tailscale URL, so job containers can also reach Forgejo.
forgejo_runner_data_dir: /Users/erichblume/.forgejo-runner
forgejo_runner_config_dir: /Users/erichblume/.config/forgejo-runner
forgejo_runner_log_dir: /Users/erichblume/Library/Logs
# Runner registration
forgejo_runner_instance_url: "http://localhost:3001"
# Container settings
forgejo_runner_container_name: forgejo-runner
forgejo_runner_image: code.forgejo.org/forgejo/runner:6.2.1
forgejo_runner_network: tailnet-jobs
# Runner registration - use Tailscale URL since we're on tailnet-jobs network
forgejo_runner_instance_url: "https://forge.tail8d86e.ts.net"
forgejo_runner_name: "indri-docker-runner"
# Labels format: label:docker://image
#
# Bootstrap mode (use upstream images to build our own):
# Job containers also run on tailnet-jobs network and can reach:
# - forge.tail8d86e.ts.net for git clone
# - registry.tail8d86e.ts.net for container push/pull
#
# Bootstrap mode (use upstream images until we build ci-base):
# docker-builder:docker://docker:27-cli
# ubuntu-latest:docker://catthehacker/ubuntu:act-22.04
#
# Production mode (use our own images from zot via host.docker.internal):
# docker-builder:docker://host.docker.internal:5050/blumeops/ci-base:latest
# ubuntu-latest:docker://host.docker.internal:5050/blumeops/ci-base:latest
# Production mode (use our own images from zot):
# docker-builder:docker://registry.tail8d86e.ts.net/blumeops/ci-base:latest
# ubuntu-latest:docker://registry.tail8d86e.ts.net/blumeops/ci-base:latest
#
# Note: Docker daemon.json must include host.docker.internal:5050 in insecure-registries
# Currently using bootstrap mode until ci-base is built
forgejo_runner_labels: "docker-builder:docker://docker:27-cli,ubuntu-latest:docker://catthehacker/ubuntu:act-22.04,ubuntu-22.04:docker://catthehacker/ubuntu:act-22.04"
# Runner config
@ -27,5 +39,4 @@ forgejo_runner_capacity: 2
forgejo_runner_timeout: 3h
# Docker container settings for jobs
# Note: network is hardcoded to "host" so containers can reach localhost:3001 (Forgejo)
forgejo_runner_privileged: true # Needed for container builds

View file

@ -2,6 +2,6 @@
- name: Restart forgejo-runner
listen: Restart forgejo-runner
ansible.builtin.shell: |
launchctl unload ~/Library/LaunchAgents/mcquack.eblume.forgejo-runner.plist 2>/dev/null || true
launchctl load ~/Library/LaunchAgents/mcquack.eblume.forgejo-runner.plist
launchctl unload ~/Library/LaunchAgents/mcquack.forgejo-runner.plist 2>/dev/null || true
launchctl load ~/Library/LaunchAgents/mcquack.forgejo-runner.plist
changed_when: true

View file

@ -1,34 +1,12 @@
---
# Forgejo Runner on indri
# Forgejo Runner - containerized daemon on tailnet-jobs network
#
# Uses Docker container mode for job isolation.
# Can build containers using Docker (via socket).
# The runner daemon runs in a Docker container with access to the tailnet
# via the tailscale-ci-gateway. Job containers also run on tailnet-jobs
# and can reach Forgejo via Tailscale.
#
# ONE-TIME SETUP (before running ansible):
#
# 1. Clone forgejo-runner from forge mirror:
# ssh indri 'git clone https://forge.tail8d86e.ts.net/eblume/forgejo-runner.git ~/code/3rd/forgejo-runner'
#
# 2. Set up Go via mise:
# ssh indri 'cd ~/code/3rd/forgejo-runner && mise use go@1.24'
#
# 3. Build:
# ssh indri 'cd ~/code/3rd/forgejo-runner && mise x -- make build'
#
# 4. Run ansible to deploy config and LaunchAgent
- name: Verify forgejo-runner binary exists
ansible.builtin.stat:
path: "{{ forgejo_runner_binary }}"
register: forgejo_runner_binary_stat
- name: Fail if forgejo-runner binary not found
ansible.builtin.fail:
msg: |
Forgejo-runner binary not found at {{ forgejo_runner_binary }}.
Please build from source first:
ssh indri 'cd ~/code/3rd/forgejo-runner && mise x -- make build'
when: not forgejo_runner_binary_stat.stat.exists
# DEPENDENCIES:
# - tailscale_ci_gateway role must run first (creates tailnet-jobs network)
- name: Ensure forgejo-runner directories exist
ansible.builtin.file:
@ -46,38 +24,47 @@
mode: '0644'
notify: Restart forgejo-runner
- name: Pull forgejo-runner image
ansible.builtin.command:
cmd: docker pull {{ forgejo_runner_image }}
register: forgejo_runner_pull
changed_when: "'Downloaded newer image' in forgejo_runner_pull.stdout or 'Pull complete' in forgejo_runner_pull.stdout"
- name: Check if runner is registered
ansible.builtin.stat:
path: "{{ forgejo_runner_data_dir }}/.runner"
register: forgejo_runner_registered
- name: Register runner with Forgejo
- name: Register runner with Forgejo (via tailnet)
ansible.builtin.command:
cmd: >
{{ forgejo_runner_binary }} register
docker run --rm
--network {{ forgejo_runner_network }}
-v {{ forgejo_runner_data_dir }}:/data
{{ forgejo_runner_image }}
forgejo-runner register
--instance "{{ forgejo_runner_instance_url }}"
--token "{{ forgejo_runner_token }}"
--name "{{ forgejo_runner_name }}"
--labels "{{ forgejo_runner_labels }}"
--no-interactive
chdir: "{{ forgejo_runner_data_dir }}"
when: not forgejo_runner_registered.stat.exists
changed_when: true
- name: Deploy forgejo-runner LaunchAgent plist
- name: Deploy forgejo-runner launchd plist
ansible.builtin.template:
src: forgejo-runner.plist.j2
dest: ~/Library/LaunchAgents/mcquack.eblume.forgejo-runner.plist
dest: ~/Library/LaunchAgents/mcquack.forgejo-runner.plist
mode: '0644'
notify: Restart forgejo-runner
- name: Check if forgejo-runner LaunchAgent is loaded
ansible.builtin.command: launchctl list mcquack.eblume.forgejo-runner
- name: Check if forgejo-runner is loaded
ansible.builtin.command: launchctl list mcquack.forgejo-runner
register: forgejo_runner_launchctl_check
changed_when: false
failed_when: false
- name: Load forgejo-runner LaunchAgent if not loaded
ansible.builtin.command: launchctl load ~/Library/LaunchAgents/mcquack.eblume.forgejo-runner.plist
- name: Load forgejo-runner if not loaded
ansible.builtin.command: launchctl load ~/Library/LaunchAgents/mcquack.forgejo-runner.plist
when: forgejo_runner_launchctl_check.rc != 0
changed_when: true

View file

@ -3,16 +3,15 @@ log:
level: info
runner:
file: {{ forgejo_runner_data_dir }}/.runner
# Path inside the container (data dir mounted at /data)
file: /data/.runner
capacity: {{ forgejo_runner_capacity }}
timeout: {{ forgejo_runner_timeout }}
container:
network: "bridge"
# Use tailnet-jobs network so job containers can reach Forgejo via Tailscale gateway
network: "{{ forgejo_runner_network }}"
privileged: {{ forgejo_runner_privileged | lower }}
# Map localhost to Docker host so containers can reach Forgejo at localhost:3001
# host-gateway is a special Docker value that resolves to the host IP
options: "--add-host localhost:host-gateway"
# Mount Docker socket so jobs can build containers
valid_volumes:
- /var/run/docker.sock

View file

@ -4,16 +4,30 @@
<plist version="1.0">
<dict>
<key>Label</key>
<string>mcquack.eblume.forgejo-runner</string>
<string>mcquack.forgejo-runner</string>
<key>ProgramArguments</key>
<array>
<string>{{ forgejo_runner_binary }}</string>
<string>daemon</string>
<string>--config</string>
<string>{{ forgejo_runner_config_dir }}/config.yaml</string>
<string>/bin/bash</string>
<string>-c</string>
<string><![CDATA[
# Stop and remove existing container if present
docker stop {{ forgejo_runner_container_name }} 2>/dev/null || true
docker rm {{ forgejo_runner_container_name }} 2>/dev/null || true
# Run the forgejo-runner daemon in a container
# - On tailnet-jobs network (can reach Forgejo via Tailscale gateway)
# - Mounts docker socket to spawn job containers
# - Mounts config and data directories
exec docker run --rm \
--name {{ forgejo_runner_container_name }} \
--network {{ forgejo_runner_network }} \
-v /var/run/docker.sock:/var/run/docker.sock \
-v {{ forgejo_runner_config_dir }}/config.yaml:/config.yaml:ro \
-v {{ forgejo_runner_data_dir }}:/data \
{{ forgejo_runner_image }} \
forgejo-runner daemon --config /config.yaml
]]></string>
</array>
<key>WorkingDirectory</key>
<string>{{ forgejo_runner_data_dir }}</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>

View file

@ -0,0 +1,9 @@
---
# Tailscale CI Gateway - provides tailnet access for Forgejo runner job containers
tailscale_ci_gateway_state_dir: /Users/erichblume/.tailscale-ci-gateway
tailscale_ci_gateway_network: tailnet-jobs
tailscale_ci_gateway_network_subnet: "172.30.0.0/24"
tailscale_ci_gateway_container_name: tailscale-ci-gateway
tailscale_ci_gateway_hostname: ci-gateway
tailscale_ci_gateway_image: tailscale/tailscale:latest

View file

@ -0,0 +1,7 @@
---
- name: Restart tailscale-ci-gateway
listen: Restart tailscale-ci-gateway
ansible.builtin.shell: |
launchctl unload ~/Library/LaunchAgents/mcquack.tailscale-ci-gateway.plist 2>/dev/null || true
launchctl load ~/Library/LaunchAgents/mcquack.tailscale-ci-gateway.plist
changed_when: true

View file

@ -0,0 +1,46 @@
---
# Tailscale CI Gateway role
# Manages a Tailscale container that provides tailnet access for CI job containers
- name: Ensure state directory exists
ansible.builtin.file:
path: "{{ tailscale_ci_gateway_state_dir }}"
state: directory
mode: "0700"
- name: Check if Docker network exists
ansible.builtin.command:
cmd: docker network inspect {{ tailscale_ci_gateway_network }}
register: tailscale_ci_gateway_network_check
failed_when: false
changed_when: false
- name: Create Docker network for CI jobs
ansible.builtin.command:
cmd: >-
docker network create
--driver bridge
--subnet {{ tailscale_ci_gateway_network_subnet }}
{{ tailscale_ci_gateway_network }}
when: tailscale_ci_gateway_network_check.rc != 0
changed_when: true
- name: Pull Tailscale image
ansible.builtin.command:
cmd: docker pull {{ tailscale_ci_gateway_image }}
register: tailscale_ci_gateway_pull
changed_when: "'Downloaded newer image' in tailscale_ci_gateway_pull.stdout or 'Pull complete' in tailscale_ci_gateway_pull.stdout"
- name: Deploy launchd plist for Tailscale CI gateway
ansible.builtin.template:
src: tailscale-ci-gateway.plist.j2
dest: ~/Library/LaunchAgents/mcquack.tailscale-ci-gateway.plist
mode: "0644"
notify: Restart tailscale-ci-gateway
- name: Ensure Tailscale CI gateway is loaded
ansible.builtin.command:
cmd: launchctl load ~/Library/LaunchAgents/mcquack.tailscale-ci-gateway.plist
register: tailscale_ci_gateway_load
failed_when: false
changed_when: tailscale_ci_gateway_load.rc == 0

View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>mcquack.tailscale-ci-gateway</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>-c</string>
<string><![CDATA[
# Stop and remove existing container if present
docker stop {{ tailscale_ci_gateway_container_name }} 2>/dev/null || true
docker rm {{ tailscale_ci_gateway_container_name }} 2>/dev/null || true
# Run the container (foreground so launchd manages lifecycle)
exec docker run --rm \
--name {{ tailscale_ci_gateway_container_name }} \
--hostname {{ tailscale_ci_gateway_hostname }} \
--network {{ tailscale_ci_gateway_network }} \
--cap-add NET_ADMIN \
--cap-add NET_RAW \
-v {{ tailscale_ci_gateway_state_dir }}:/var/lib/tailscale \
-e TS_AUTHKEY="{{ tailscale_ci_gateway_auth_key }}" \
-e TS_STATE_DIR=/var/lib/tailscale \
-e TS_USERSPACE=false \
-e TS_ACCEPT_DNS=true \
{{ tailscale_ci_gateway_image }}
]]></string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>{{ ansible_env.HOME }}/Library/Logs/mcquack.tailscale-ci-gateway.out.log</string>
<key>StandardErrorPath</key>
<string>{{ ansible_env.HOME }}/Library/Logs/mcquack.tailscale-ci-gateway.err.log</string>
</dict>
</plist>