diff --git a/CLAUDE.md b/CLAUDE.md index 286115f..06641ea 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -75,6 +75,16 @@ The mail role deploys four containers on `mail_internal`: `mail-db` (PostgreSQL - Fetchmail 6.6.x rcfile syntax: `ssl` is a user-level option (inline after `password`), port is `smtphost dovecot/24` (slash-separated), `smtpport` keyword does not exist - `mail.ladkau.de` is the Roundcube webmail client, not an MTA — no outgoing SMTP server is configured +### Gitea Actions runners + +The `act_runner` role deploys three `gitea/act_runner` containers (Docker executor). Each runner handles one concurrent job. Key details: + +- Runners connect to Gitea via the public HTTPS URL — no internal Docker network needed +- Each runner gets its own data dir (`/opt/act_runner/runner-N/`) for its `.runner` registration file +- All runners share a single `config.yml` at `/opt/act_runner/config.yml` +- `/var/run/docker.sock` is mounted — job containers are spawned directly on the host +- `gitea_runner_registration_token` is obtained from Gitea admin UI **after** Gitea is running; add it to the vault and then deploy with `--tags act_runner` + ### SSO Keycloak at `sso.ladkau.de` is the identity provider. Gitea and Nextcloud are configured post-provisioning to use it (see `docs/runbook-configuration.md`). diff --git a/ansible/group_vars/all/vars.yml b/ansible/group_vars/all/vars.yml index cfe72e5..88603b0 100644 --- a/ansible/group_vars/all/vars.yml +++ b/ansible/group_vars/all/vars.yml @@ -89,6 +89,13 @@ roundcube_smtp_port: "587" # ssl: true # default: true # keep: true # default: true — set false to delete from source after fetch +# Gitea Actions runners (act_runner with Docker executor) +act_runner_version: "latest" +act_runner_data_dir: /opt/act_runner +act_runner_count: 3 +# Secrets — store values in ansible/group_vars/all/vault.yml (Ansible Vault) +# gitea_runner_registration_token: "" # Gitea admin → Site Administration → Runners → Create runner token + # Container registry (Docker Registry v2) registry_data_dir: /opt/registry # Registry users — plaintext passwords, Ansible generates bcrypt hashes at deploy time diff --git a/ansible/roles/act_runner/handlers/main.yml b/ansible/roles/act_runner/handlers/main.yml new file mode 100644 index 0000000..b3ae7d5 --- /dev/null +++ b/ansible/roles/act_runner/handlers/main.yml @@ -0,0 +1,7 @@ +--- +- name: Restart act_runner + community.docker.docker_compose_v2: + project_src: "{{ act_runner_data_dir }}" + state: present + pull: missing + recreate: always diff --git a/ansible/roles/act_runner/tasks/main.yml b/ansible/roles/act_runner/tasks/main.yml new file mode 100644 index 0000000..122401e --- /dev/null +++ b/ansible/roles/act_runner/tasks/main.yml @@ -0,0 +1,46 @@ +--- +- name: Create act_runner base directory + ansible.builtin.file: + path: "{{ act_runner_data_dir }}" + state: directory + owner: root + group: root + mode: "0755" + tags: act_runner + +- name: Create per-runner data directories + ansible.builtin.file: + path: "{{ act_runner_data_dir }}/runner-{{ item }}" + state: directory + owner: root + group: root + mode: "0755" + loop: "{{ range(1, act_runner_count + 1) | list }}" + tags: act_runner + +- name: Deploy act_runner config + ansible.builtin.template: + src: config.yml.j2 + dest: "{{ act_runner_data_dir }}/config.yml" + owner: root + group: root + mode: "0644" + notify: Restart act_runner + tags: act_runner + +- name: Deploy Docker Compose file + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ act_runner_data_dir }}/docker-compose.yml" + owner: root + group: root + mode: "0644" + notify: Restart act_runner + tags: act_runner + +- name: Start act_runner + community.docker.docker_compose_v2: + project_src: "{{ act_runner_data_dir }}" + state: present + pull: missing + tags: act_runner diff --git a/ansible/roles/act_runner/templates/config.yml.j2 b/ansible/roles/act_runner/templates/config.yml.j2 new file mode 100644 index 0000000..bb8d365 --- /dev/null +++ b/ansible/roles/act_runner/templates/config.yml.j2 @@ -0,0 +1,21 @@ +# Managed by Ansible — do not edit manually +log: + level: info + +runner: + file: .runner # stored in each runner's /data volume + capacity: 1 # one concurrent job per runner container + timeout: 3h + fetch_timeout: 5s + fetch_interval: 2s + +cache: + enabled: false # disable built-in cache server; use actions/cache if needed + +container: + network: bridge # job containers get default bridge network with internet access + privileged: false + valid_volumes: + - /tmp + force_pull: false + force_rebuild: false diff --git a/ansible/roles/act_runner/templates/docker-compose.yml.j2 b/ansible/roles/act_runner/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..b8fe01d --- /dev/null +++ b/ansible/roles/act_runner/templates/docker-compose.yml.j2 @@ -0,0 +1,17 @@ +# Managed by Ansible — do not edit manually +services: +{% for i in range(1, act_runner_count + 1) %} + runner-{{ i }}: + image: gitea/act_runner:{{ act_runner_version }} + container_name: act-runner-{{ i }} + restart: unless-stopped + environment: + - GITEA_INSTANCE_URL=https://{{ domain_gitea }} + - GITEA_RUNNER_REGISTRATION_TOKEN={{ gitea_runner_registration_token }} + - GITEA_RUNNER_NAME=runner-{{ i }} + - CONFIG_FILE=/config.yml + volumes: + - {{ act_runner_data_dir }}/runner-{{ i }}:/data + - {{ act_runner_data_dir }}/config.yml:/config.yml:ro + - /var/run/docker.sock:/var/run/docker.sock +{% endfor %} diff --git a/ansible/roles/registry/templates/docker-compose.yml.j2 b/ansible/roles/registry/templates/docker-compose.yml.j2 index f866d38..7a565f8 100644 --- a/ansible/roles/registry/templates/docker-compose.yml.j2 +++ b/ansible/roles/registry/templates/docker-compose.yml.j2 @@ -20,7 +20,7 @@ services: - "traefik.http.routers.registry.entrypoints=websecure" - "traefik.http.routers.registry.tls.certresolver=letsencrypt" - "traefik.http.services.registry.loadbalancer.server.port=5000" - - "traefik.http.routers.registry.middlewares=registry-buffering,rate-limit@docker" + - "traefik.http.routers.registry.middlewares=registry-buffering" # Remove body size limit so large image layers can be pushed - "traefik.http.middlewares.registry-buffering.buffering.maxRequestBodyBytes=0" diff --git a/ansible/site.yml b/ansible/site.yml index 2e202ff..e30261e 100644 --- a/ansible/site.yml +++ b/ansible/site.yml @@ -7,6 +7,7 @@ - docker - traefik - gitea + - act_runner - nextcloud - sso - mail diff --git a/docs/runbook-configuration.md b/docs/runbook-configuration.md index eed1b77..d2c75b4 100644 --- a/docs/runbook-configuration.md +++ b/docs/runbook-configuration.md @@ -209,6 +209,55 @@ With the alias in place you can use the shorter SCP-like URL: git clone git@gitea.ladkau.de:/myrepo.git ``` +### 3.4 Deploy Gitea Actions runners + +Three `act_runner` containers (Docker executor) are provisioned by the +`act_runner` Ansible role. Each runner handles one concurrent job; together +they allow up to three parallel workflow jobs. + +**This step requires Gitea to be running and the admin account to exist +(step 3.1). Runners cannot register until a token is obtained from Gitea.** + +**Step 1 — Get a runner registration token** + +1. Go to `https://gitea.ladkau.de` → sign in as admin. +2. Navigate to **Site Administration** (top-right menu) → + **Actions** → **Runners**. +3. Click **Create runner token** and copy the token. + +**Step 2 — Add the token to the vault** + +```bash +ansible-vault edit ansible/group_vars/all/vault.yml +``` + +Add the key: + +```yaml +gitea_runner_registration_token: "" +``` + +**Step 3 — Deploy the runners** + +```bash +ansible-playbook -i ansible/inventory.ini ansible/site.yml \ + --tags act_runner --ask-vault-pass +``` + +Ansible creates `/opt/act_runner/` with a shared `config.yml` and three +per-runner data directories (`runner-1/`, `runner-2/`, `runner-3/`). On first +start each container auto-registers with Gitea and writes a `.runner` file to +its data directory. Subsequent restarts reuse the saved registration. + +**Verify** + +Back in Gitea **Site Administration → Actions → Runners**, all three runners +should appear as **Online** within a few seconds. + +> **Security note:** Each runner container mounts `/var/run/docker.sock`. +> This gives workflow jobs root-equivalent access to the host Docker daemon. +> Only run workflows from trusted repositories. + ## 4. Nextcloud The admin credentials are set via `nextcloud_admin_user` and diff --git a/docs/runbook-provisioning.md b/docs/runbook-provisioning.md index 62c4fb4..9cd2192 100644 --- a/docs/runbook-provisioning.md +++ b/docs/runbook-provisioning.md @@ -121,6 +121,10 @@ registry_users: - username: alice password: "your-password" +# Gitea Actions runners — token obtained after Gitea is running +# See step 3.4 of runbook-configuration.md; replace after Gitea admin account is created +gitea_runner_registration_token: "placeholder" + # Vaultwarden password vault vaultwarden_admin_token: "" # generate: openssl rand -hex 32 # vaultwarden_sso_client_secret is added after Keycloak is configured — see @@ -203,20 +207,28 @@ ansible-playbook -i ansible/inventory.ini ansible/site.yml --ask-vault-pass This runs all roles in order: -| # | Role | What it does | -|---|------------|--------------| -| 1 | `base` | OS hardening, deploy user, SSH config, ufw firewall, fail2ban | -| 2 | `docker` | Docker Engine + Compose plugin, shared Traefik network | -| 3 | `traefik` | Reverse proxy, automatic TLS via Let's Encrypt | -| 4 | `gitea` | Self-hosted Git with PostgreSQL | -| 5 | `nextcloud`| File storage with PostgreSQL, Redis, cron sidecar | -| 6 | `sso` | Keycloak single sign-on with PostgreSQL | -| 7 | `mail` | Roundcube webmail client with PostgreSQL | -| 8 | `registry` | Docker Registry v2 with htpasswd auth | -| 9 | `k8s` | Placeholder page at k8s.ladkau.de | -| 10 | `vaultwarden` | Vaultwarden password vault at vault.ladkau.de | -| 11 | `dl` | Public download server at dl.ladkau.de — nginx HTTPS + SFTP upload | -| 12 | `dashboard` | Public status dashboard at cloud.ladkau.de — service health and server stats | +| # | Role | What it does | +|----|---------------|--------------| +| 1 | `base` | OS hardening, deploy user, SSH config, ufw firewall, fail2ban | +| 2 | `docker` | Docker Engine + Compose plugin, shared Traefik network | +| 3 | `traefik` | Reverse proxy, automatic TLS via Let's Encrypt | +| 4 | `gitea` | Self-hosted Git with PostgreSQL | +| 5 | `act_runner` | Three Gitea Actions runners with Docker executor | +| 6 | `nextcloud` | File storage with PostgreSQL, Redis, cron sidecar | +| 7 | `sso` | Keycloak single sign-on with PostgreSQL | +| 8 | `mail` | Roundcube webmail client with PostgreSQL | +| 9 | `registry` | Docker Registry v2 with htpasswd auth | +| 10 | `k8s` | Placeholder page at k8s.ladkau.de | +| 11 | `vaultwarden` | Vaultwarden password vault at vault.ladkau.de | +| 12 | `dl` | Public download server at dl.ladkau.de — nginx HTTPS + SFTP upload | +| 13 | `dashboard` | Public status dashboard at cloud.ladkau.de — service health and server stats | + +> **Note:** The `act_runner` role requires `gitea_runner_registration_token` in the +> vault, which can only be obtained after Gitea is running and an admin account has +> been created. On a fresh provisioning run the role will fail if the token is +> absent. Either add a placeholder and redeploy with `--tags act_runner` after +> Gitea is configured (see step 3.4 of `runbook-configuration.md`), or skip the +> role on first run: `--skip-tags act_runner`. To apply a single role: