diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 7dc1a0c..9342743 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -63,5 +63,55 @@ jobs: build-args: | VERSION=${{ env.VERSION }} - # The deploy job (self-hosted runner, on-host docker-compose) comes with the - # deploy/ stack (Caddyfile + docker-compose.yml), the second Milestone-F PR. + # Ship the freshly pushed image to the on-host compose stack. The runner lives ON the + # deploy host (outbound-only long-poll to GitHub; the host is VPN-internal, so no inbound + # is possible). It syncs only the NON-secret infra (compose file + Caddyfile) from the + # repo; secrets (.env, .glabs-web.yaml) and the caddy-data volume (TLS certs) are never + # touched. See deploy/README.md "Automatischer Deploy". + deploy: + needs: build-and-push-image + # Gate: only runs once the self-hosted runner on the deploy host exists — set the repo + # variable AUTO_DEPLOY=true then. Until then the image is still built & pushed above; + # this job is simply skipped (no stuck queue waiting for a missing runner). + if: ${{ vars.AUTO_DEPLOY == 'true' }} + runs-on: [self-hosted, glabs-deploy] + env: + # The v-prefixed release tag (from whichever trigger fired). docker/metadata-action + # tags the image with this raw ref (v3.38.0), so GLABS_WEB_TAG must carry the v too. + VERSION: ${{ github.event.release.tag_name || inputs.tag }} + # On-host deploy dir; override via repo variable DEPLOY_DIR if different. + DEPLOY_DIR: ${{ vars.DEPLOY_DIR || '/home/glabs/glabs/deploy' }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + # Sync the deploy/ files as they were at the released tag. + ref: ${{ github.event.release.tag_name || inputs.tag }} + + - name: Sync infra + deploy pinned image + run: | + set -euo pipefail + # Sync non-secret infra into the on-host deploy dir. Secrets (.env, + # .glabs-web.yaml) and the caddy-data volume (certs) stay put. + cp deploy/docker-compose.yml "$DEPLOY_DIR/" + CADDY_CHANGED=0 + if ! diff deploy/Caddyfile "$DEPLOY_DIR/Caddyfile" >/dev/null 2>&1; then + cp deploy/Caddyfile "$DEPLOY_DIR/" + CADDY_CHANGED=1 + fi + + cd "$DEPLOY_DIR" + # Pin the backend tag so the host reflects the running version + # (manual rollback = edit .env + `docker compose up -d glabs-web`). + sed -i "s/^GLABS_WEB_TAG=.*/GLABS_WEB_TAG=$VERSION/" .env + + docker compose pull glabs-web + # Reconcile the stack: only glabs-web (new tag) + any changed service is recreated. + # The gh-runner services are under `profiles: [runner]`, so this plain `up -d` + # ignores them — the runner never restarts itself mid-deploy. + docker compose up -d + # Caddy re-reads its Caddyfile only on container (re)start. + if [ "$CADDY_CHANGED" = "1" ]; then + docker compose up -d --force-recreate caddy + fi + docker image prune -f diff --git a/deploy/.env.example b/deploy/.env.example index 3e792f3..9c6f2ce 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -41,3 +41,13 @@ OAUTH2_PROXY_CLIENT_ID=your-client-id OAUTH2_PROXY_CLIENT_SECRET=your-client-secret # Cookie encryption secret (must be 16/24/32 bytes — `openssl rand -base64 24` gives 32 chars). OAUTH2_PROXY_COOKIE_SECRET=change-me-random-32-bytes-base64 + +# --- Auto-deploy: self-hosted GitHub Actions runners (profile "runner") --- +# Only needed if you run the gh-runner services (docker compose --profile runner up -d). +# On-host deploy dir — MUST match the workflow's DEPLOY_DIR repo variable and is where the +# runner containers bind-mount .env / docker-compose.yml / Caddyfile. +DEPLOY_DIR=/home/glabs/glabs/deploy +# GitHub PAT the runner containers use to self-renew their registration tokens. +# Classic PAT with scopes `repo` + `workflow` (covers obcode/glabs AND obcode/glabs.gui), +# or fine-grained with Repository permission "Administration: Read and write". +GH_RUNNER_PAT=ghp_change-me diff --git a/deploy/README.md b/deploy/README.md index fee1fa7..58c0172 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -100,6 +100,41 @@ ssh -L 27017:127.0.0.1:27017 @ # then: mongodb://:@localhost:27017/?authSource=admin ``` +## Automatischer Deploy (self-hosted runners) + +CI builds and pushes the images on GitHub-hosted runners (`docker.yml`). The deploy host is +VPN-internal, so GitHub can't push into it — instead a **self-hosted runner on the host** polls +GitHub outbound and, on a release, runs `docker compose` locally. Each repo's `docker.yml` has +a gated `deploy` job (`runs-on: [self-hosted, glabs-deploy]`): + +- **glabs** (backend) — syncs `docker-compose.yml` + `Caddyfile` into `$DEPLOY_DIR` (secrets and + the `caddy-data` volume untouched), pins `GLABS_WEB_TAG` in `.env`, `docker compose pull + glabs-web` + `up -d`, and force-recreates Caddy if the Caddyfile changed. +- **glabs.gui** — pins `GUI_TAG` and rolls only the `gui` service. + +Because `obcode` is a personal account (no org), runners are **repo-scoped**: one per repo, +sharing the label `glabs-deploy`. Both run as containers in this compose under the `runner` +profile. + +### One-time host setup + +1. Set `DEPLOY_DIR` and `GH_RUNNER_PAT` in `.env` (see `.env.example`). `GH_RUNNER_PAT` is a + classic PAT with `repo` + `workflow` (covers both repos). +2. Start the runners (the `runner` profile keeps them out of the default stack, so a deploy's + `docker compose up -d` never restarts them mid-job): + ```sh + docker compose --profile runner up -d gh-runner gh-runner-gui + ``` +3. Confirm both appear under **Settings → Actions → Runners** in each repo (idle, label + `glabs-deploy`). +4. Flip the gate: set the repo variable **`AUTO_DEPLOY=true`** in **both** `obcode/glabs` and + `obcode/glabs.gui` (Settings → Secrets and variables → Actions → Variables). Optionally set + `DEPLOY_DIR` there too if it differs from the default. Until `AUTO_DEPLOY=true`, images are + still built and pushed — only the deploy step is skipped. + +From then on, every release rolls itself out. Manual rollback stays available: set an older +`GLABS_WEB_TAG`/`GUI_TAG` in `.env` and `docker compose up -d glabs-web` (or `gui`). + ## Notes - **Rollback:** set `GLABS_WEB_TAG` / `GUI_TAG` in `.env` to an older release tag and diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index c20b864..ab6b173 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -132,6 +132,64 @@ services: - caddy-data:/data # certificates + ACME account/EAB registration — MUST persist - caddy-config:/config + # --- Self-hosted GitHub Actions runners for auto-deploy (profile "runner") --- + # The deploy host is VPN-internal — GitHub cannot push in. These runners poll GitHub + # OUTBOUND and, on a release, run `docker compose` locally (see README "Automatischer + # Deploy"). obcode is a personal account (no org), so runners are REPO-scoped: one per + # repo, sharing the label `glabs-deploy`. + # + # `profiles: [runner]` keeps them OUT of the default stack: a bare `docker compose up -d` + # (what the deploy job runs) ignores profiled services entirely, so a runner never + # restarts itself mid-deploy and aborts its own job. Start them once, separately: + # docker compose --profile runner up -d gh-runner gh-runner-gui + gh-runner: + image: myoung34/github-runner:latest + container_name: glabs-gh-runner + restart: unless-stopped + profiles: ["runner"] + environment: + REPO_URL: https://github.com/obcode/glabs + RUNNER_NAME: glabs-deploy + RUNNER_SCOPE: repo + LABELS: glabs-deploy + # PAT (classic: repo + workflow, or fine-grained: Administration RW) so the container + # renews registration tokens itself and survives restarts without a token swap. + ACCESS_TOKEN: ${GH_RUNNER_PAT:-} + RUNNER_WORKDIR: /tmp/runner/work + security_opt: + - label:disable + volumes: + # Host Docker socket → `docker compose` in the deploy job talks to the HOST daemon. + - /var/run/docker.sock:/var/run/docker.sock + # The on-host deploy dir at the SAME path inside the container, so `cp`/`sed` in the + # job and the compose bind-mounts resolve to identical host paths (the socket resolves + # mounts against the host). Keep DEPLOY_DIR in sync with the workflow's DEPLOY_DIR var. + - ${DEPLOY_DIR:-/home/glabs/glabs/deploy}:${DEPLOY_DIR:-/home/glabs/glabs/deploy} + - /tmp/runner:/tmp/runner + + # Second runner for the glabs.gui repo (personal account → no org-wide sharing, each repo + # needs its own repo-scoped runner). Same stack, same `runner` profile, same PAT (classic + # repo+workflow covers both repos), same label — only REPO_URL + RUNNER_NAME differ. The + # GUI deploy job only touches the `gui` service. + gh-runner-gui: + image: myoung34/github-runner:latest + container_name: glabs-gh-runner-gui + restart: unless-stopped + profiles: ["runner"] + environment: + REPO_URL: https://github.com/obcode/glabs.gui + RUNNER_NAME: glabs-deploy-gui + RUNNER_SCOPE: repo + LABELS: glabs-deploy + ACCESS_TOKEN: ${GH_RUNNER_PAT:-} + RUNNER_WORKDIR: /tmp/runner-gui/work + security_opt: + - label:disable + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ${DEPLOY_DIR:-/home/glabs/glabs/deploy}:${DEPLOY_DIR:-/home/glabs/glabs/deploy} + - /tmp/runner-gui:/tmp/runner-gui + volumes: mongo-data: caddy-data: