feat(vikunja): bring PIHA Vikunja under GitOps (postgres + Forgejo OIDC)

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 <noreply@anthropic.com>
This commit is contained in:
Oskar Kapala 2026-06-17 15:02:37 +02:00
parent dd64b9c878
commit f7e1391f30
9 changed files with 229 additions and 0 deletions

View file

@ -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

View file

@ -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

View file

@ -21,6 +21,7 @@ nodes:
- node-agent
- ha-diag-agent
- brain-watchdog
- vikunja # Task management (vikunja + postgres), public via npm
solaria:
roles:

View file

@ -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

View file

@ -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.

View file

@ -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"

View file

@ -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

23
services/vikunja/healthcheck.sh Executable file
View file

@ -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

View file

@ -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