From f7e1391f30151d24b1593da1e0d1068af5698efd Mon Sep 17 00:00:00 2001 From: Oskar Kapala Date: Wed, 17 Jun 2026 15:02:37 +0200 Subject: [PATCH] feat(vikunja): bring PIHA Vikunja under GitOps (postgres + Forgejo OIDC) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wraps the existing manually-run Vikunja instance on PIHA in the standard service layout. No deploy — definition only, for review. - services/vikunja/: docker-compose.yml (vikunja + postgres:16-alpine), service.yaml, README, healthcheck, config.yml (OIDC, secret-free), .env.example. Real .env stays gitignored. - Pins EXISTING named volumes (vikunja_vikunja_db, vikunja_vikunja_files) so DB + uploaded files survive cutover. - extra_hosts forgejo.okit.pl=192.168.31.5 (npm on PIHA) so OIDC discovery resolves over LAN instead of flaky public DNS. - OIDC client secret injected via env (VIKUNJA_AUTH_OPENID_PROVIDERS_ FORGEJO_CLIENTSECRET); config.yml keeps trailing-slash authurl/redirecturl. - Registers vikunja in hosts/piha/services.yaml + inventory/topology.yaml, plus hosts/piha/runtime/vikunja override (advisory mem_limits). Co-Authored-By: Claude Opus 4.8 --- .../vikunja/docker-compose.override.yml | 11 ++++ hosts/piha/services.yaml | 17 +++++ inventory/topology.yaml | 1 + services/vikunja/.env.example | 17 +++++ services/vikunja/README.md | 44 +++++++++++++ services/vikunja/config.yml | 23 +++++++ services/vikunja/docker-compose.yml | 62 +++++++++++++++++++ services/vikunja/healthcheck.sh | 23 +++++++ services/vikunja/service.yaml | 31 ++++++++++ 9 files changed, 229 insertions(+) create mode 100644 hosts/piha/runtime/vikunja/docker-compose.override.yml create mode 100644 services/vikunja/.env.example create mode 100644 services/vikunja/README.md create mode 100644 services/vikunja/config.yml create mode 100644 services/vikunja/docker-compose.yml create mode 100755 services/vikunja/healthcheck.sh create mode 100644 services/vikunja/service.yaml diff --git a/hosts/piha/runtime/vikunja/docker-compose.override.yml b/hosts/piha/runtime/vikunja/docker-compose.override.yml new file mode 100644 index 0000000..ce2ba82 --- /dev/null +++ b/hosts/piha/runtime/vikunja/docker-compose.override.yml @@ -0,0 +1,11 @@ +# PIHA-specific overrides for Vikunja. +# +# PIHA is not as RAM-constrained as the VPS, so these limits are advisory +# guard-rails (a leaking process gets restarted by Docker) rather than a hard +# budget. Adjust if Vikunja's working set grows. +services: + vikunja: + mem_limit: 768m + + db: + mem_limit: 256m diff --git a/hosts/piha/services.yaml b/hosts/piha/services.yaml index 5dcad97..cd29137 100644 --- a/hosts/piha/services.yaml +++ b/hosts/piha/services.yaml @@ -40,3 +40,20 @@ services: external: [control-plane] runtime: config_path: /opt/homelab/config/brain-watchdog + + vikunja: + role: task-management + deployment_model: docker-compose + exposure: private # public via npm reverse proxy at vikunja.okit.pl + offline_required: false + depends_on: + local: [] + external: [forgejo] # OIDC identity provider + ports: + - name: http + container_port: 3456 + protocol: tcp + runtime: + # secrets (.env) + OIDC config (config.yml) live alongside the compose file + config_path: services/vikunja + # data is in Docker named volumes: vikunja_vikunja_db, vikunja_vikunja_files diff --git a/inventory/topology.yaml b/inventory/topology.yaml index 0c89633..8a7f9f4 100644 --- a/inventory/topology.yaml +++ b/inventory/topology.yaml @@ -21,6 +21,7 @@ nodes: - node-agent - ha-diag-agent - brain-watchdog + - vikunja # Task management (vikunja + postgres), public via npm solaria: roles: diff --git a/services/vikunja/.env.example b/services/vikunja/.env.example new file mode 100644 index 0000000..729ab0d --- /dev/null +++ b/services/vikunja/.env.example @@ -0,0 +1,17 @@ +# Vikunja secrets — copy to .env and fill with the REAL live values. +# .env is gitignored; never commit it. +# +# These three values MUST match the running instance or you lose access / +# break login at cutover. Do not regenerate them. + +# Postgres superuser password for the vikunja DB. +POSTGRES_PASSWORD=change-me-very-strong + +# Must be identical to POSTGRES_PASSWORD — this is how Vikunja connects to it. +VIKUNJA_DATABASE_PASSWORD=change-me-very-strong + +# JWT signing secret. Changing it invalidates all existing sessions/tokens. +VIKUNJA_SERVICE_JWTSECRET=change-me-super-long-random-string + +# OIDC client secret for the Forgejo provider (config.yml stays secret-free). +VIKUNJA_AUTH_OPENID_PROVIDERS_FORGEJO_CLIENTSECRET=change-me-forgejo-oidc-client-secret diff --git a/services/vikunja/README.md b/services/vikunja/README.md new file mode 100644 index 0000000..fe9bc85 --- /dev/null +++ b/services/vikunja/README.md @@ -0,0 +1,44 @@ +# Vikunja + +Self-hosted task management. Runs on the **PIHA** node with a dedicated +PostgreSQL backend, exposed publicly at `https://vikunja.okit.pl` via the npm +reverse proxy on PIHA. Login is via Forgejo OIDC. + +## Stack + +| Container | Image | Purpose | +|-------------|------------------------|----------------------------------| +| `vikunja` | `vikunja/vikunja:latest` | App + API (port 3456) | +| `vikunja-db`| `postgres:16-alpine` | PostgreSQL (service alias `db`) | + +## Data (do not move) + +Persistent state lives in pre-existing Docker named volumes — these are pinned +by name so the live data survives cutover: + +- `vikunja_vikunja_db` → `/var/lib/postgresql/data` +- `vikunja_vikunja_files` → `/app/vikunja/files` + +## Configuration + +- `config.yml` — committed, **secret-free**. Holds OIDC provider metadata. +- `.env` — **gitignored**. Copy from `.env.example` and fill with the real + live values (must be identical to the running instance): + - `POSTGRES_PASSWORD` / `VIKUNJA_DATABASE_PASSWORD` (same value) + - `VIKUNJA_SERVICE_JWTSECRET` + - `VIKUNJA_AUTH_OPENID_PROVIDERS_FORGEJO_CLIENTSECRET` + +## Notes + +- `forgejo.okit.pl` is pinned via `extra_hosts` to `192.168.31.5` (npm on PIHA) + so OIDC discovery resolves over the LAN, avoiding flaky public DNS. +- Both `redirecturl` and the provider `authurl` MUST keep their trailing slash — + the `authurl` must match the OIDC issuer exactly or discovery fails. + +## Cutover checklist + +1. `git pull` on PIHA. +2. Create `services/vikunja/.env` from `.env.example` with the real values. +3. Confirm the named volumes exist: `docker volume ls | grep vikunja_vikunja`. +4. `docker compose -f services/vikunja/docker-compose.yml up -d`. +5. Verify: `./healthcheck.sh` and a test OIDC login. diff --git a/services/vikunja/config.yml b/services/vikunja/config.yml new file mode 100644 index 0000000..57a549d --- /dev/null +++ b/services/vikunja/config.yml @@ -0,0 +1,23 @@ +# Vikunja runtime config — committed to git (secret-free). +# +# Database, public URL, JWT secret and the OIDC client SECRET are supplied via +# environment variables (see docker-compose.yml + .env). Only non-secret OIDC +# provider metadata lives here. +# +# Env vars override matching keys, so anything secret stays out of this file. + +auth: + openid: + enabled: true + # MUST end with a trailing slash — matches Vikunja's redirect handling. + redirecturl: "https://vikunja.okit.pl/auth/openid/" + providers: + forgejo: + name: "Forgejo" + # authurl MUST have a trailing slash — it must match the OIDC issuer + # exactly or discovery fails. + authurl: "https://forgejo.okit.pl/" + clientid: "15ad6f99-b8ca-48dc-92b9-97907ddc4367" + # clientsecret is injected via + # VIKUNJA_AUTH_OPENID_PROVIDERS_FORGEJO_CLIENTSECRET (.env) — never here. + scope: "openid profile email" diff --git a/services/vikunja/docker-compose.yml b/services/vikunja/docker-compose.yml new file mode 100644 index 0000000..6fc146e --- /dev/null +++ b/services/vikunja/docker-compose.yml @@ -0,0 +1,62 @@ +services: + vikunja: + image: vikunja/vikunja:latest + container_name: vikunja + restart: unless-stopped + depends_on: + db: + condition: service_healthy + # forgejo.okit.pl is pinned to the npm reverse proxy on PIHA so OIDC + # discovery resolves over the LAN instead of flaky public DNS, which was + # causing OIDC discovery to fail. + extra_hosts: + - "forgejo.okit.pl:192.168.31.5" + env_file: + - .env + environment: + - VIKUNJA_DATABASE_TYPE=postgres + - VIKUNJA_DATABASE_HOST=db + - VIKUNJA_DATABASE_PORT=5432 + - VIKUNJA_DATABASE_USER=vikunja + - VIKUNJA_DATABASE_DATABASE=vikunja + - VIKUNJA_DATABASE_PASSWORD=${VIKUNJA_DATABASE_PASSWORD:?set in .env} + - VIKUNJA_SERVICE_PUBLICURL=https://vikunja.okit.pl + - VIKUNJA_SERVICE_JWTSECRET=${VIKUNJA_SERVICE_JWTSECRET:?set in .env} + # OIDC client secret injected from .env so config.yml stays secret-free. + - VIKUNJA_AUTH_OPENID_PROVIDERS_FORGEJO_CLIENTSECRET=${VIKUNJA_AUTH_OPENID_PROVIDERS_FORGEJO_CLIENTSECRET:?set in .env} + - TZ=Europe/Warsaw + volumes: + - vikunja_files:/app/vikunja/files + - ./config.yml:/etc/vikunja/config.yml:ro + ports: + - "3456:3456" + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:3456/api/v1/info"] + interval: 30s + timeout: 10s + retries: 5 + + db: + image: postgres:16-alpine + container_name: vikunja-db + restart: unless-stopped + environment: + - POSTGRES_USER=vikunja + - POSTGRES_DB=vikunja + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?set in .env} + - TZ=Europe/Warsaw + volumes: + - vikunja_db:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U vikunja -d vikunja"] + interval: 10s + timeout: 5s + retries: 5 + +# Pinned to the EXISTING named volumes so the live database and uploaded files +# survive cutover. Do not rename these — the data lives here. +volumes: + vikunja_db: + name: vikunja_vikunja_db + vikunja_files: + name: vikunja_vikunja_files diff --git a/services/vikunja/healthcheck.sh b/services/vikunja/healthcheck.sh new file mode 100755 index 0000000..81f66aa --- /dev/null +++ b/services/vikunja/healthcheck.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# Healthcheck for Vikunja (app + database) + +# Vikunja app container must be running +if ! docker ps --filter "name=vikunja" --filter "status=running" | grep -qw "vikunja"; then + echo "[FAIL] vikunja container is not running" + exit 1 +fi + +# Postgres container must be running +if ! docker ps --filter "name=vikunja-db" --filter "status=running" | grep -qw "vikunja-db"; then + echo "[FAIL] vikunja-db container is not running" + exit 1 +fi + +# API must respond +if ! curl -sf http://localhost:3456/api/v1/info > /dev/null; then + echo "[FAIL] Vikunja API is not responding on :3456" + exit 1 +fi + +echo "[OK] Vikunja is healthy" +exit 0 diff --git a/services/vikunja/service.yaml b/services/vikunja/service.yaml new file mode 100644 index 0000000..c78d06a --- /dev/null +++ b/services/vikunja/service.yaml @@ -0,0 +1,31 @@ +service: + name: vikunja + owner_node: piha + exposure: private # reachable on Tailscale; public via npm reverse proxy + dependencies: + - forgejo # OIDC identity provider (forgejo.okit.pl) + ports: + - container: 3456 + host: 3456 + protocol: tcp + healthcheck: + type: http + endpoint: http://localhost:3456/api/v1/info + interval: 30s + timeout: 10s + retries: 5 + restart_policy: unless-stopped + persistence: + # Data lives in pre-existing Docker named volumes — do NOT move/rename. + paths: + - vikunja_vikunja_db # postgres data -> /var/lib/postgresql/data + - vikunja_vikunja_files # uploads -> /app/vikunja/files + runtime: + config_files: + - config.yml # OIDC provider metadata (secret-free, in git) + - .env # secrets (gitignored, supplied per node) + env_vars: + - POSTGRES_PASSWORD + - VIKUNJA_DATABASE_PASSWORD + - VIKUNJA_SERVICE_JWTSECRET + - VIKUNJA_AUTH_OPENID_PROVIDERS_FORGEJO_CLIENTSECRET