From e7b8d4df178a8b16438c7e98f3883770fecf865d Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 1 Jun 2026 21:24:09 -0400 Subject: [PATCH] initial commit: Dawarich LXC role (CT 459 on pve02, .159) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-hosted location history. 4-container compose: Rails 8 app + Sidekiq + PostGIS 16-3.4 + Redis 7, plus watchtower. Authentik OIDC end-to-end. Image pinned at freikin/dawarich:1.7.11 (OIDC support requires >= 1.7.8). PostGIS DB lives in this LXC, not on the central DB VM (.172) — central image is postgres:16-alpine without postgis, swapping it carries broader blast radius than colocating here. Convention exception captured in homelab-docs project_dawarich memory. Roles: - dawarich: system + Docker + compose + weekly prune timer - alloy: logs+journald → Loki, node metrics → Prometheus Bring-up sequence proven 2026-06-01. README documents the 5-trap build chain (image version, entrypoint scripts, solid_cache SQLite bind mount, APPLICATION_HOSTS+localhost, force_ssl+healthcheck). Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 4 + README.md | 152 +++++++++++++ ansible.cfg | 6 + deploy.sh | 38 ++++ inventory.ini | 1 + logs.sh | 31 +++ requirements.yml | 2 + roles/alloy/handlers/main.yml | 5 + roles/alloy/tasks/main.yml | 37 ++++ roles/alloy/templates/config.alloy.j2 | 161 ++++++++++++++ roles/alloy/templates/docker-compose.yml.j2 | 33 +++ roles/dawarich/files/docker-prune.service | 8 + roles/dawarich/files/docker-prune.timer | 9 + roles/dawarich/handlers/main.yml | 4 + roles/dawarich/tasks/main.yml | 207 ++++++++++++++++++ .../dawarich/templates/docker-compose.yml.j2 | 187 ++++++++++++++++ scripts/bootstrap-secrets.sh | 46 ++++ site.yml | 101 +++++++++ vars/main.yml | 84 +++++++ vars/vault.yml | 10 + 20 files changed, 1126 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 ansible.cfg create mode 100755 deploy.sh create mode 100644 inventory.ini create mode 100755 logs.sh create mode 100644 requirements.yml create mode 100644 roles/alloy/handlers/main.yml create mode 100644 roles/alloy/tasks/main.yml create mode 100644 roles/alloy/templates/config.alloy.j2 create mode 100644 roles/alloy/templates/docker-compose.yml.j2 create mode 100644 roles/dawarich/files/docker-prune.service create mode 100644 roles/dawarich/files/docker-prune.timer create mode 100644 roles/dawarich/handlers/main.yml create mode 100644 roles/dawarich/tasks/main.yml create mode 100644 roles/dawarich/templates/docker-compose.yml.j2 create mode 100755 scripts/bootstrap-secrets.sh create mode 100644 site.yml create mode 100644 vars/main.yml create mode 100644 vars/vault.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8231d00 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +vars/vault.yml.dec +*.retry +__pycache__/ +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..0dbb49b --- /dev/null +++ b/README.md @@ -0,0 +1,152 @@ +# homelab-ansible-lxc-dawarich + +Configuration for **dawarich** (LXC 459 on pve02, `192.168.1.159`) — self-hosted +location-history app, [Dawarich](https://dawarich.app). Ingests location data +from OwnTracks, the Dawarich iOS/Android apps, and Google Takeout exports; +renders heatmaps, places visited, distance/time statistics, and timeline view. + +- **URL:** `dawarich.balders.ca` (Caddy → `192.168.1.159:3000`, Authentik OIDC) +- **Image:** `freikin/dawarich:1.7.11` (pinned in `vars/main.yml`) — **must be ≥ 1.7.8** for OIDC. +- **Tier:** T2 (Proxmox) +- **Auth:** Authentik OIDC (SSO-only — `ALLOW_EMAIL_PASSWORD_REGISTRATION=false`) +- **DB:** PostGIS 16-3.4 **local to this LXC**. The central DB VM (.172) runs + `postgres:16-alpine` which doesn't bundle the PostGIS extension; rather than + swap that image (16 other databases) we run a colocated container here. See + memory `project_dawarich` for the convention-exception rationale. + +## Architecture + +``` + Phone (OwnTracks / Dawarich app) + │ HTTP POST /api/v1/owntracks/points?api_key=… + ▼ + Caddy (TLS, Authentik OIDC on UI; API bypassed by query-string auth) + │ + ▼ + dawarich_app (Rails 8 + Puma) ──▶ dawarich_db (PostGIS 16-3.4) + │ ▲ + ▼ │ + dawarich_sidekiq ─── enqueue ─────────── dawarich_redis + └──── solid_queue ──▶ /dawarich_db_data (SQLite) +``` + +## Roles + +| Role | Purpose | +|------------|---------| +| `dawarich` | System setup, Docker, registry mirror, 4-container compose, prune timer | +| `alloy` | Docker logs + journald → Loki, node metrics → Prometheus | + +## Prerequisites (before first deploy) + +1. **LXC provisioned** — `module.dawarich` in homelab-terraform (CTID 459, + pve02, `.159`). `tf.sh apply lxc` then bootstrap the cbalders user manually + on the new CT (the bpg module only seeds root keys; cbalders + NOPASSWD sudo + is post-create). See `feedback_lxc_bootstrap_user`. +2. **`vars/vault.yml`** — holds `vault_infisical_client_secret`. Shared + universal-auth client across all LXC repos — copy from `homelab-ansible-lxc-rag/vars/vault.yml`. +3. **Infisical `/dawarich` folder + 5 secrets** — run `./scripts/bootstrap-secrets.sh`. It creates the folder if missing and pushes: + - `vault_dawarich_db_password` (`openssl rand -hex 32`) + - `vault_dawarich_secret_key_base` (`openssl rand -hex 64`) + - `vault_dawarich_otp_primary_key` (`openssl rand -hex 32`) + - `vault_dawarich_otp_deterministic_key` (`openssl rand -hex 32`) + - `vault_dawarich_otp_salt` (`openssl rand -hex 32`) +4. **Infisical `/oidc` secrets** — generate `vault_dawarich_oidc_client_id` + (40 hex) and `vault_dawarich_oidc_client_secret` (64 hex), push as `--type shared`. + These values ARE the credentials — pi-auth creates the Authentik provider + with them, it doesn't fetch them back. Generation pattern: + ```bash + infisical secrets set "vault_dawarich_oidc_client_id=$(openssl rand -hex 20)" \ + --projectId 50062d7c-06ff-4d5c-8ca3-6c0cdba9f270 --env prod --path /oidc --type shared + infisical secrets set "vault_dawarich_oidc_client_secret=$(openssl rand -hex 32)" \ + --projectId 50062d7c-06ff-4d5c-8ca3-6c0cdba9f270 --env prod --path /oidc --type shared + ``` +5. **pi-auth site.yml mapping** — `homelab-ansible-pi-auth/site.yml` must have + a `dawarich_oidc_client_id` / `dawarich_oidc_client_secret` set_fact line + reading from `oidc_secrets.secrets.vault_dawarich_oidc_client_{id,secret}`. + Without this, pi-auth's loop fails with `'dawarich_oidc_client_id' is undefined`. + +## Deploy + +```bash +./deploy.sh # full deploy +./deploy.sh --tags dawarich # dawarich role only +./logs.sh -f # follow dawarich_app +./logs.sh dawarich_sidekiq -f # follow background jobs +``` + +## Bring-up order (proven 2026-06-01) + +1. `cd ../homelab-terraform && ./tf.sh apply lxc` → CT 459 created. +2. Bootstrap `cbalders` user + SSH keys + NOPASSWD sudo on the new CT: + ```bash + ssh root@pve02 'pct exec 459 -- bash -c " + useradd -m -s /bin/bash -G sudo cbalders && + mkdir -p /home/cbalders/.ssh && cat > /home/cbalders/.ssh/authorized_keys < + EOF + chown -R cbalders:cbalders /home/cbalders/.ssh && chmod 700 /home/cbalders/.ssh && chmod 600 /home/cbalders/.ssh/authorized_keys && + apt-get update -qq && apt-get install -y -qq sudo && + mkdir -p /etc/sudoers.d && + echo \"cbalders ALL=(ALL) NOPASSWD:ALL\" > /etc/sudoers.d/cbalders && + chmod 440 /etc/sudoers.d/cbalders"' + ``` +3. `./scripts/bootstrap-secrets.sh` → 5 `/dawarich` secrets in Infisical. +4. Push `/oidc` secrets (see prereq 4 above). +5. Add `dawarich_oidc_client_{id,secret}` set_fact mapping in `homelab-ansible-pi-auth/site.yml`. +6. First `./deploy.sh` here → app boots, migrations run (~3 min cold-start). +7. `cd ../homelab-ansible-pi-auth && ./deploy.sh` → creates Authentik provider + application. +8. `cd ../homelab-ansible-proxy && ./deploy.sh` → adds Caddy vhost + Technitium CNAME. +9. `cd ../homelab-ansible-pve && ./deploy.sh` → adds CTID 459 to `pbs-prod-daily`. +10. Browser-test `https://dawarich.balders.ca/users/sign_in` → click "Sign in with Authentik". + +## Mobile clients + +After first OIDC login, the user goes to Settings → API Keys in the Dawarich +UI to generate a per-user token. Use it as the bearer for either: + +- **OwnTracks** (any platform) — HTTP recorder mode, URL: + `https://dawarich.balders.ca/api/v1/owntracks/points?api_key=` +- **Dawarich iOS/Android app** — paste the token + host on first launch. + +## Google Takeout import + +1. Take the `location-history.json` out of the Takeout archive. +2. Drop it into `/opt/dawarich/watched/` on the LXC (NFS mount or `scp`). +3. Sidekiq picks it up via the import watcher within ~30s and emits progress + to `dawarich_sidekiq` logs. Large histories (>10y) can take an hour. + +## Gotchas (build-day 2026-06-01) + +- **Image version pin.** OIDC support landed in `freikin/dawarich:1.7.8`. The + `0.27.x` line silently ignores `OIDC_*` env vars (no OmniAuth controllers in + the codebase). Pinned at `1.7.11`. Bump app + sidekiq lockstep — same image + tag in two containers, breaking schema migrations on minor bumps. +- **Image entrypoint scripts are required.** Image ships `ENTRYPOINT ["bundle", "exec"]` + with no CMD. Compose MUST set `entrypoint: ["/usr/local/bin/web-entrypoint.sh"]` + (and `sidekiq-entrypoint.sh` for sidekiq). Those scripts run `db:prepare`, + create the SQLite files for solid_cache/solid_queue/solid_cable at + `/dawarich_db_data/`, and migrate Postgres before exec'ing the command. +- **Multi-DB SQLite persistence.** The solid_cache/solid_queue/solid_cable + files live at `/dawarich_db_data/` — bind-mount `/opt/dawarich/db_data:/dawarich_db_data` + to keep in-flight Sidekiq jobs across recreates. +- **`APPLICATION_HOSTS` must include localhost.** Rails 8 host authorization + blocks loopback if only the public host is listed — kills Docker HEALTHCHECK + and the Ansible health-wait. +- **Rails 8 `force_ssl` + healthcheck.** `APPLICATION_PROTOCOL=https` makes Rails + 301-redirect every plain-HTTP request to HTTPS. The Docker healthcheck is a + TCP-socket probe (`ruby -rsocket -e 'TCPSocket.new("localhost",3000).close'`) + rather than HTTP, because curl/wget either chase the redirect to TLS (Puma is + plain-HTTP behind Caddy → "SSL wrong version number") or fail 2xx. The Ansible + `Wait for Dawarich to be healthy` task is `ignore_errors: true` for the same reason. +- **SSO button is POST, not GET.** The signin page renders a `
`; the button submits with a Rails CSRF token. `GET /users/auth/openid_connect` returns 404 — that's CSRF working, not a routing bug. + +## Notes + +- Postgres data lives at `/opt/dawarich/postgres` (uid 70, alpine `postgres` + user). Pre-chowned in the role to avoid the empty-dir trap. +- Redis data at `/opt/dawarich/redis` (uid 999) — see `feedback_redis_uid_999`. +- PBS LXC snapshot covers everything nightly (the postgis + db_data volumes). + No central pg_backup wiring — DB is local. +- Watchtower exposes its HTTP API on port 8088 inside the LXC (matches the + fleet pattern; 8080 collides with potential second app on this CT). diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..21e4d89 --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,6 @@ +[defaults] +interpreter_python = /usr/bin/python3 +deprecation_warnings = False +host_key_checking = False +result_format = yaml +callbacks_enabled = profile_tasks, timer diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..5991217 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# ============================================================================== +# deploy.sh — Deploy Dawarich LXC (Rails + Sidekiq + PostGIS + Redis) +# +# Usage: +# ./deploy.sh # full deploy (prompts for vault password) +# ./deploy.sh --tags dawarich # dawarich role only +# ./deploy.sh -v # verbose output +# ============================================================================== +set -euo pipefail + +HOST_IP="$(grep -E '^[0-9]' inventory.ini | head -1 | awk '{print $1}')" +HOST_USER="cbalders" + +echo "==> Checking connectivity to ${HOST_USER}@${HOST_IP} ..." +if ! ssh -o ConnectTimeout=5 -o BatchMode=yes "${HOST_USER}@${HOST_IP}" true 2>/dev/null; then + echo " Cannot SSH to ${HOST_IP} — refreshing host key ..." + ssh-keygen -R "$HOST_IP" 2>/dev/null || true + ssh-keyscan -H "$HOST_IP" >> ~/.ssh/known_hosts 2>/dev/null +fi + +echo "==> Installing Ansible collections ..." +ansible-galaxy collection install -r requirements.yml --force 2>/dev/null + +echo "==> Running deploy playbook ..." +ansible-playbook -i inventory.ini site.yml --ask-vault-pass "$@" + +echo "==> Verifying ..." +ssh "${HOST_USER}@${HOST_IP}" bash -s <<'VERIFY' +echo "Containers:" +sudo docker ps --format ' {{.Names}}: {{.Status}}' | sort +echo "Dawarich web (TCP socket probe — Rails force_ssl redirects HTTP, smoke via Caddy after Caddy deploy):" +sudo docker exec dawarich_app ruby -rsocket -e 'TCPSocket.new("localhost",3000).close' 2>/dev/null && echo " :3000 LISTENING" || echo " UNREACHABLE" +echo "Postgis (probed from dawarich_app):" +sudo docker exec dawarich_app sh -c 'pg_isready -h dawarich_db -U dawarich' 2>/dev/null || echo " (pg unreachable)" +VERIFY + +echo "==> Done." diff --git a/inventory.ini b/inventory.ini new file mode 100644 index 0000000..f891f2d --- /dev/null +++ b/inventory.ini @@ -0,0 +1 @@ +192.168.1.159 ansible_user=cbalders ansible_become=yes diff --git a/logs.sh b/logs.sh new file mode 100755 index 0000000..9f78d66 --- /dev/null +++ b/logs.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# logs.sh — View container logs from the Dawarich LXC +# +# Usage: +# ./logs.sh # last 50 lines from dawarich_app +# ./logs.sh -f # tail/follow dawarich_app +# ./logs.sh dawarich_sidekiq # sidekiq logs +# ./logs.sh dawarich_db -f # follow db logs + +HOST_IP="$(grep -E '^[0-9]' inventory.ini | head -1 | awk '{print $1}')" +HOST_USER="cbalders" + +CONTAINER="dawarich_app" +FOLLOW=false +LINES=50 + +for arg in "$@"; do + case "$arg" in + -f|--follow) FOLLOW=true ;; + [0-9]*) LINES="$arg" ;; + *) CONTAINER="$arg" ;; + esac +done + +if [ "$FOLLOW" = true ]; then + echo "==> Tailing $CONTAINER on dawarich ($HOST_IP) — Ctrl+C to stop" + ssh "${HOST_USER}@${HOST_IP}" "sudo docker logs $CONTAINER --tail $LINES -f" +else + echo "==> Last $LINES lines of $CONTAINER on dawarich ($HOST_IP)" + ssh "${HOST_USER}@${HOST_IP}" "sudo docker logs $CONTAINER --tail $LINES 2>&1" +fi diff --git a/requirements.yml b/requirements.yml new file mode 100644 index 0000000..6a77f32 --- /dev/null +++ b/requirements.yml @@ -0,0 +1,2 @@ +collections: + - name: infisical.vault diff --git a/roles/alloy/handlers/main.yml b/roles/alloy/handlers/main.yml new file mode 100644 index 0000000..10a3407 --- /dev/null +++ b/roles/alloy/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: restart alloy + command: docker compose -f /opt/alloy/docker-compose.yml up -d --force-recreate + args: + chdir: /opt/alloy diff --git a/roles/alloy/tasks/main.yml b/roles/alloy/tasks/main.yml new file mode 100644 index 0000000..4eebe14 --- /dev/null +++ b/roles/alloy/tasks/main.yml @@ -0,0 +1,37 @@ +--- +- name: Create Alloy directories + file: + path: "{{ item }}" + state: directory + owner: cbalders + group: cbalders + mode: '0755' + loop: + - /opt/alloy + - /var/lib/alloy + +- name: Deploy Alloy compose + template: + src: docker-compose.yml.j2 + dest: /opt/alloy/docker-compose.yml + owner: cbalders + group: cbalders + mode: '0644' + notify: restart alloy + +- name: Deploy Alloy config + template: + src: config.alloy.j2 + dest: /opt/alloy/config.alloy + owner: cbalders + group: cbalders + mode: '0644' + notify: restart alloy + +# Bring Alloy up (idempotent — docker compose up -d is a no-op if running +# and config hasn't changed). The handler force-recreates on config edit. +- name: Ensure Alloy is running + command: docker compose -f /opt/alloy/docker-compose.yml up -d + args: + chdir: /opt/alloy + changed_when: false diff --git a/roles/alloy/templates/config.alloy.j2 b/roles/alloy/templates/config.alloy.j2 new file mode 100644 index 0000000..a1f2239 --- /dev/null +++ b/roles/alloy/templates/config.alloy.j2 @@ -0,0 +1,161 @@ +// Alloy — Docker container logs + journald → Loki. River syntax. +// Per-LXC config; only the `host:` label changes via alloy_host_label var. + +// ============================================================ +// Docker — discover + scrape container logs +// ============================================================ + +discovery.docker "containers" { + host = "unix:///var/run/docker.sock" + refresh_interval = "15s" +} + +discovery.relabel "containers" { + targets = discovery.docker.containers.targets + + rule { + source_labels = ["__meta_docker_container_name"] + regex = "/(.+)" + target_label = "container" + } + rule { + source_labels = ["__meta_docker_container_log_stream"] + target_label = "stream" + } + rule { + source_labels = ["__meta_docker_container_label_com_docker_compose_service"] + target_label = "service" + } + rule { + source_labels = ["__meta_docker_container_label_com_docker_compose_project"] + target_label = "compose_project" + } + rule { + target_label = "job" + replacement = "docker" + } + rule { + target_label = "host" + replacement = "{{ alloy_host_label }}" + } +} + +loki.source.docker "containers" { + host = "unix:///var/run/docker.sock" + targets = discovery.relabel.containers.output + forward_to = [loki.process.docker.receiver] +} + +loki.process "docker" { + // Replay protection on first boot — Loki rejects > 7d. + stage.drop { + older_than = "24h" + drop_counter_reason = "older_than_24h" + } + // pulse-agent broken-pipe noise on hosts running pulse-agent --enable-host. + stage.drop { + expression = ".*broken pipe.*" + drop_counter_reason = "broken_pipe_noise" + } + forward_to = [loki.write.default.receiver] +} + +// ============================================================ +// Journald — LXC systemd units +// ============================================================ + +loki.source.journal "host" { + path = "/var/log/journal" + max_age = "1m" + forward_to = [loki.process.journal.receiver] + + relabel_rules = loki.relabel.journal.rules + labels = { + job = "journald", + host = "{{ alloy_host_label }}", + } +} + +loki.relabel "journal" { + forward_to = [] + + rule { + source_labels = ["__journal__systemd_unit"] + target_label = "unit" + } + rule { + source_labels = ["__journal__hostname"] + target_label = "instance" + } + rule { + source_labels = ["__journal_priority_keyword"] + target_label = "severity" + } +} + +loki.process "journal" { + forward_to = [loki.write.default.receiver] +} + +// ============================================================ +// Loki — push +// ============================================================ + +loki.write "default" { + endpoint { + url = "{{ alloy_loki_url }}" + } +} + +// ============================================================ +// Prometheus — embedded node_exporter (replaces standalone) +// ============================================================ + +prometheus.exporter.unix "node" { + // Container view of host filesystems — bind-mounted in compose. + rootfs_path = "/host/rootfs" + procfs_path = "/host/proc" + sysfs_path = "/host/sys" + // Default collectors match upstream node_exporter set. +} + +prometheus.scrape "node" { + targets = prometheus.exporter.unix.node.targets + forward_to = [prometheus.relabel.node.receiver] + scrape_interval = "15s" + job_name = "{{ alloy_prom_job | default('node_lxc') }}" +} + +// Rewrite the instance + job labels to match the historical +// node_exporter scrape, so existing dashboards continue to slice by +// the same instance string. Alloy's exporter component injects +// job="integrations/unix" on its targets — we have to overwrite it +// here; `job_name` in prometheus.scrape only names the scrape pool, +// it doesn't relabel the metrics. +prometheus.relabel "node" { + forward_to = [prometheus.remote_write.observe.receiver] + rule { + target_label = "instance" + replacement = "{{ alloy_prom_instance | default(ansible_default_ipv4.address + ':9100') }}" + } + rule { + target_label = "job" + replacement = "{{ alloy_prom_job | default('node_lxc') }}" + } + // Match the static labels Prom previously injected at scrape time + // (see homelab-ansible-vm-observability/roles/configs/files/prometheus.yml). + rule { + target_label = "group" + replacement = "{{ alloy_prom_group | default('lxc') }}" + } + rule { + target_label = "hostname" + replacement = "{{ alloy_prom_hostname | default(alloy_host_label) }}" + } +} + +prometheus.remote_write "observe" { + endpoint { + url = "{{ alloy_prom_remote_write | default('http://observe.lan.balders.ca:9090/api/v1/write') }}" + } +} diff --git a/roles/alloy/templates/docker-compose.yml.j2 b/roles/alloy/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..5091cbf --- /dev/null +++ b/roles/alloy/templates/docker-compose.yml.j2 @@ -0,0 +1,33 @@ +--- +# Self-contained Alloy stack on /opt/alloy. Runs independently from the +# host's main service (kestra/infisical/mcp/etc) so a service-side compose +# down doesn't take logging with it. +services: + alloy: + image: grafana/alloy:latest + container_name: alloy + restart: unless-stopped + command: + - run + - /etc/alloy/config.alloy + - --storage.path=/var/lib/alloy/data + - --server.http.listen-addr=0.0.0.0:12345 + ports: + - "12345:12345" + # Share the host's PID namespace so prometheus.exporter.unix reads + # /proc with the host kernel's cgroup view (cgroup-aware MemAvailable). + # Without this, /proc/meminfo returns hybrid values: MemTotal from the + # host cgroup but Cached/SReclaimable from the container, leading to + # a ~25% MemAvailable inflation. See docs/audit/alloy-consolidation-2026-05-21.md. + pid: host + volumes: + - /opt/alloy/config.alloy:/etc/alloy/config.alloy:ro + - /var/lib/alloy:/var/lib/alloy + - /var/log/journal:/var/log/journal:ro + - /run/log/journal:/run/log/journal:ro + - /etc/machine-id:/etc/machine-id:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + # Host metric collection for prometheus.exporter.unix (node_exporter replacement) + - /:/host/rootfs:ro,rslave + - /proc:/host/proc:ro + - /sys:/host/sys:ro diff --git a/roles/dawarich/files/docker-prune.service b/roles/dawarich/files/docker-prune.service new file mode 100644 index 0000000..e5a196a --- /dev/null +++ b/roles/dawarich/files/docker-prune.service @@ -0,0 +1,8 @@ +[Unit] +Description=Prune dangling docker images (Watchtower leaves them behind) +After=docker.service +Requires=docker.service + +[Service] +Type=oneshot +ExecStart=/usr/bin/docker image prune -af diff --git a/roles/dawarich/files/docker-prune.timer b/roles/dawarich/files/docker-prune.timer new file mode 100644 index 0000000..85c2622 --- /dev/null +++ b/roles/dawarich/files/docker-prune.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Weekly docker image prune + +[Timer] +OnCalendar=Sun 03:30 +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/roles/dawarich/handlers/main.yml b/roles/dawarich/handlers/main.yml new file mode 100644 index 0000000..bd9a70f --- /dev/null +++ b/roles/dawarich/handlers/main.yml @@ -0,0 +1,4 @@ +--- +- name: reload systemd + systemd: + daemon_reload: true diff --git a/roles/dawarich/tasks/main.yml b/roles/dawarich/tasks/main.yml new file mode 100644 index 0000000..661494c --- /dev/null +++ b/roles/dawarich/tasks/main.yml @@ -0,0 +1,207 @@ +--- +# ---------- System setup ---------- +- name: Set timezone + copy: + content: "{{ timezone }}" + dest: /etc/timezone + mode: '0644' + register: tz_file + +- name: Apply timezone + command: dpkg-reconfigure -f noninteractive tzdata + when: tz_file.changed + +- name: Set hostname + copy: + content: "dawarich" + dest: /etc/hostname + mode: '0644' + +- name: Apply hostname + command: hostname dawarich + changed_when: false + +- name: Ensure hostname in /etc/hosts + lineinfile: + path: /etc/hosts + regexp: '^127\.0\.1\.1' + line: "127.0.1.1 dawarich" + +- name: Update apt cache + apt: + update_cache: true + cache_valid_time: 3600 + register: apt_cache_result + failed_when: false + changed_when: apt_cache_result.changed | default(false) + +- name: Install base packages + apt: + name: "{{ packages }}" + state: present + +- name: Create user + user: + name: "{{ item.name }}" + groups: "{{ item.groups }}" + shell: "{{ item.shell }}" + append: true + loop: "{{ users }}" + +- name: Deploy SSH authorized keys + authorized_key: + user: cbalders + key: "{{ item }}" + state: present + loop: "{{ ssh_authorized_keys }}" + +# ---------- Docker ---------- +- name: Check if Docker is installed + command: docker --version + register: docker_check + changed_when: false + failed_when: false + +- name: Install Docker + when: docker_check.rc != 0 + block: + - name: Download Docker install script + get_url: + url: https://get.docker.com + dest: /tmp/get-docker.sh + mode: '0755' + + - name: Run Docker install script + command: /tmp/get-docker.sh + + - name: Remove install script + file: + path: /tmp/get-docker.sh + state: absent + +- name: Ensure Docker service is running + systemd: + name: docker + state: started + enabled: true + +- name: Add cbalders to docker group + user: + name: cbalders + groups: docker + append: true + +# ---------- Docker registry mirror ---------- +- name: Ensure /etc/docker exists + file: + path: /etc/docker + state: directory + mode: '0755' + +- name: Read existing daemon.json + slurp: + src: /etc/docker/daemon.json + register: daemon_json_raw + failed_when: false + +- name: Configure Docker registry mirror + copy: + content: "{{ ((daemon_json_raw.content | b64decode | from_json) if daemon_json_raw.content is defined else {}) | combine({'registry-mirrors': ['http://registry.lan.balders.ca:5000']}) | to_nice_json }}" + dest: /etc/docker/daemon.json + owner: root + group: root + mode: '0644' + register: docker_daemon_config + +- name: Restart Docker if mirror config changed + systemd: + name: docker + state: restarted + when: docker_daemon_config.changed + +# ---------- Dawarich stack ---------- +- name: Create app directories + file: + path: "{{ item.path }}" + state: directory + owner: "{{ item.owner | default('cbalders') }}" + group: "{{ item.group | default('cbalders') }}" + mode: '0755' + loop: + - { path: /opt/dawarich } + # Postgis container runs as uid 70 (alpine 'postgres' user). Pre-chown + # so the first boot doesn't fail with "directory is not empty / wrong + # permissions" the way Nextcloud did (feedback_nextcloud_db_perms). + - { path: /opt/dawarich/postgres, owner: '70', group: '70' } + # Redis runs as uid 999 on bind volumes (feedback_redis_uid_999). + - { path: /opt/dawarich/redis, owner: '999', group: '999' } + # Dawarich Rails app writes uploads/cache here as uid 1000. + - { path: /opt/dawarich/public } + - { path: /opt/dawarich/watched } + - { path: /opt/dawarich/storage } + # solid_cache + solid_queue + solid_cable SQLite files (writable by the + # rails uid inside the container — image runs as uid 1000). + - { path: /opt/dawarich/db_data } + +- name: Deploy docker-compose + template: + src: docker-compose.yml.j2 + dest: /opt/dawarich/docker-compose.yml + owner: cbalders + group: cbalders + mode: '0640' + register: compose_changed + +- name: Pull images + command: docker compose pull + args: + chdir: /opt/dawarich + changed_when: false + +- name: Recreate containers if config changed + command: docker compose up -d --force-recreate + args: + chdir: /opt/dawarich + when: compose_changed.changed + changed_when: true + +- name: Ensure containers are running + command: docker compose up -d + args: + chdir: /opt/dawarich + when: not compose_changed.changed + changed_when: false + +# Dawarich runs migrations on boot (entrypoint includes db:migrate). First +# boot can take ~60-90s because the initial schema + PostGIS extension load +# is heavy. Don't fail the play if it's not ready yet — Watchtower-driven +# recreates will converge. +- name: Wait for Dawarich to be healthy + uri: + url: "http://localhost:{{ dawarich_port }}/api/v1/health" + method: GET + register: dawarich_health + until: dawarich_health.status is defined and dawarich_health.status == 200 + retries: 30 + delay: 5 + ignore_errors: true + +# ---------- Weekly docker prune (Watchtower side-effect cleanup) ---------- +- name: Deploy docker-prune systemd units + copy: + src: "{{ item }}" + dest: "/etc/systemd/system/{{ item }}" + owner: root + group: root + mode: '0644' + loop: + - docker-prune.service + - docker-prune.timer + notify: reload systemd + +- name: Enable docker-prune timer + systemd: + name: docker-prune.timer + enabled: true + state: started + daemon_reload: true diff --git a/roles/dawarich/templates/docker-compose.yml.j2 b/roles/dawarich/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..1eba855 --- /dev/null +++ b/roles/dawarich/templates/docker-compose.yml.j2 @@ -0,0 +1,187 @@ +# {{ ansible_managed }} +# Dawarich — self-hosted location history. Four containers + watchtower: +# dawarich_db postgis/postgis:16-3.4-alpine (PostGIS-enabled Postgres) +# dawarich_redis redis:7-alpine (cache + Sidekiq queue) +# dawarich_app freikin/dawarich (Rails web tier, port 3000) +# dawarich_sidekiq freikin/dawarich (background jobs) +# DB lives here (not the central DB VM) because the central image is plain +# postgres:16-alpine without the postgis extension. See memory project_dawarich. + +services: + dawarich_db: + image: "{{ postgis_image }}" + container_name: dawarich_db + restart: unless-stopped + shm_size: 1g + environment: + POSTGRES_USER: "{{ dawarich_db_user }}" + POSTGRES_PASSWORD: "{{ dawarich_db_password }}" + POSTGRES_DB: "{{ dawarich_db_name }}" + volumes: + - /opt/dawarich/postgres:/var/lib/postgresql/data + # No published ports — only the app + sidekiq reach it over the compose net. + healthcheck: + test: ["CMD-SHELL", "pg_isready -U {{ dawarich_db_user }} -d {{ dawarich_db_name }}"] + interval: 15s + timeout: 5s + retries: 10 + start_period: 30s + + dawarich_redis: + image: "{{ redis_image }}" + container_name: dawarich_redis + restart: unless-stopped + volumes: + - /opt/dawarich/redis:/data + # No published ports — internal-only. + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 15s + timeout: 3s + retries: 5 + + dawarich_app: + image: "{{ dawarich_image }}" + container_name: dawarich_app + restart: unless-stopped + depends_on: + dawarich_db: + condition: service_healthy + dawarich_redis: + condition: service_healthy + ports: + - "{{ dawarich_port }}:3000" + volumes: + - /opt/dawarich/public:/var/app/public + - /opt/dawarich/watched:/var/app/tmp/imports/watched + - /opt/dawarich/storage:/var/app/storage + # Solid_cache + solid_queue + solid_cable SQLite files. Without this + # bind, the queue DB is ephemeral and in-flight Sidekiq jobs are lost + # on container recreate. Path matches the entrypoint's expectation. + - /opt/dawarich/db_data:/dawarich_db_data + # The image ships a `web-entrypoint.sh` that runs `db:prepare` (creates + # the solid_cache + solid_queue SQLite files in /var/app/storage, runs + # Postgres migrations) before exec'ing the command. We MUST use it — + # without it, Rails 8 boots fail with "No database file specified" on + # solid_cache's first lookup. The image default entrypoint is `bundle exec` + # alone, which skips the prep. + entrypoint: ["/usr/local/bin/web-entrypoint.sh"] + command: ["bin/rails", "server", "-p", "3000", "-b", "::"] + environment: + RAILS_ENV: production + SELF_HOSTED: "true" + # localhost + ::1 needed for Docker healthcheck (Rails 8 host + # authorization rejects any host not in this list, including loopback). + APPLICATION_HOSTS: "{{ dawarich_domain }},localhost,127.0.0.1,::1" + APPLICATION_PROTOCOL: "https" + DOMAIN: "{{ dawarich_domain }}" + TIME_ZONE: "{{ timezone }}" + DISTANCE_UNIT: "km" + # Database + DATABASE_HOST: dawarich_db + DATABASE_PORT: "5432" + DATABASE_USERNAME: "{{ dawarich_db_user }}" + DATABASE_PASSWORD: "{{ dawarich_db_password }}" + DATABASE_NAME: "{{ dawarich_db_name }}" + RAILS_MAX_THREADS: "5" + # Redis + REDIS_URL: "redis://dawarich_redis:6379" + # Secrets + SECRET_KEY_BASE: "{{ dawarich_secret_key_base }}" + OTP_ENCRYPTION_PRIMARY_KEY: "{{ dawarich_otp_primary_key }}" + OTP_ENCRYPTION_DETERMINISTIC_KEY: "{{ dawarich_otp_deterministic_key }}" + OTP_ENCRYPTION_KEY_DERIVATION_SALT: "{{ dawarich_otp_salt }}" + # Registration policy — OIDC-only when enabled. Anything else and a + # local user form silently appears on /users/sign_up. + ALLOW_EMAIL_PASSWORD_REGISTRATION: "{{ dawarich_allow_email_password_registration | string | lower }}" +{% if dawarich_oidc_enabled %} + # ---- Auth: Authentik OIDC ---- + OIDC_CLIENT_ID: "{{ dawarich_oidc_client_id }}" + OIDC_CLIENT_SECRET: "{{ dawarich_oidc_client_secret }}" + OIDC_ISSUER: "{{ dawarich_oidc_issuer }}" + OIDC_REDIRECT_URI: "{{ dawarich_oidc_redirect_uri }}" + OIDC_PROVIDER_NAME: "{{ dawarich_oidc_provider_name }}" + OIDC_AUTO_REGISTER: "{{ dawarich_oidc_auto_register | string | lower }}" + OIDC_PKCE_ENABLED: "true" +{% endif %} + # TCP socket check, not HTTP. Rails 8 force_ssl (APPLICATION_PROTOCOL=https) + # redirects every plain-HTTP path to https — curl/wget can't satisfy a 2xx + # without TLS, and Puma is plain-HTTP (Caddy terminates TLS upstream). The + # socket check confirms Puma is listening, which is what we actually care + # about for orchestration. + healthcheck: + test: ["CMD-SHELL", "ruby -rsocket -e 'TCPSocket.new(\"localhost\",3000).close' || exit 1"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 120s + + dawarich_sidekiq: + image: "{{ dawarich_image }}" + container_name: dawarich_sidekiq + restart: unless-stopped + depends_on: + dawarich_db: + condition: service_healthy + dawarich_redis: + condition: service_healthy + dawarich_app: + condition: service_started + volumes: + - /opt/dawarich/public:/var/app/public + - /opt/dawarich/watched:/var/app/tmp/imports/watched + - /opt/dawarich/storage:/var/app/storage + # Solid_cache + solid_queue + solid_cable SQLite files. Without this + # bind, the queue DB is ephemeral and in-flight Sidekiq jobs are lost + # on container recreate. Path matches the entrypoint's expectation. + - /opt/dawarich/db_data:/dawarich_db_data + # Same entrypoint pattern as the web tier — sidekiq's script ensures the + # DB is migrated before workers start consuming jobs. + entrypoint: ["/usr/local/bin/sidekiq-entrypoint.sh"] + command: ["sidekiq"] + environment: + RAILS_ENV: production + SELF_HOSTED: "true" + # localhost + ::1 needed for Docker healthcheck (Rails 8 host + # authorization rejects any host not in this list, including loopback). + APPLICATION_HOSTS: "{{ dawarich_domain }},localhost,127.0.0.1,::1" + APPLICATION_PROTOCOL: "https" + DOMAIN: "{{ dawarich_domain }}" + TIME_ZONE: "{{ timezone }}" + DATABASE_HOST: dawarich_db + DATABASE_PORT: "5432" + DATABASE_USERNAME: "{{ dawarich_db_user }}" + DATABASE_PASSWORD: "{{ dawarich_db_password }}" + DATABASE_NAME: "{{ dawarich_db_name }}" + REDIS_URL: "redis://dawarich_redis:6379" + SECRET_KEY_BASE: "{{ dawarich_secret_key_base }}" + OTP_ENCRYPTION_PRIMARY_KEY: "{{ dawarich_otp_primary_key }}" + OTP_ENCRYPTION_DETERMINISTIC_KEY: "{{ dawarich_otp_deterministic_key }}" + OTP_ENCRYPTION_KEY_DERIVATION_SALT: "{{ dawarich_otp_salt }}" + BACKGROUND_PROCESSING_CONCURRENCY: "{{ dawarich_sidekiq_concurrency }}" + + watchtower: + container_name: dawarich_watchtower + image: containrrr/watchtower:latest + restart: unless-stopped + dns: + - 192.168.1.1 + ports: + - "8088:8080" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + - DOCKER_API_VERSION=1.40 + - WATCHTOWER_HTTP_API_UPDATE=true + - WATCHTOWER_HTTP_API_TOKEN={{ watchtower_api_token }} + - TZ={{ timezone }} + - WATCHTOWER_CLEANUP=true + - WATCHTOWER_INCLUDE_RESTARTING=true + - WATCHTOWER_NO_STARTUP_MESSAGE=true + - WATCHTOWER_NOTIFICATION_REPORT=true + - WATCHTOWER_NOTIFICATIONS=shoutrrr + - WATCHTOWER_NOTIFICATION_URL={{ watchtower_gotify_url }} + - WATCHTOWER_NOTIFICATIONS_HOSTNAME=DAWARICH + - WATCHTOWER_NOTIFICATION_TITLE_TAG=DAWARICH + # Render empty when nothing was updated/failed — see feedback_watchtower_pushover. + - 'WATCHTOWER_NOTIFICATION_TEMPLATE={% raw %}{{- if .Report -}}{{- with .Report -}}{{- if or .Updated .Failed -}}{{ len .Updated }} updated, {{ len .Failed }} failed{{- range .Updated}} | updated {{.Name}}{{- end -}}{{- range .Failed}} | FAILED {{.Name}}{{- end -}}{{- end -}}{{- end -}}{{- end -}}{% endraw %}' diff --git a/scripts/bootstrap-secrets.sh b/scripts/bootstrap-secrets.sh new file mode 100755 index 0000000..0933064 --- /dev/null +++ b/scripts/bootstrap-secrets.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# ============================================================================== +# bootstrap-secrets.sh — Generate + push the 5 Dawarich app secrets to Infisical +# +# Run ONCE before the first deploy. Idempotent in the sense that re-running it +# rotates every key — which will invalidate existing Rails sessions + the OTP- +# encrypted columns. Do NOT re-run blindly on an existing install. +# ============================================================================== +set -euo pipefail + +INF_PROJECT="50062d7c-06ff-4d5c-8ca3-6c0cdba9f270" +ENV="prod" +PATH_PREFIX="/dawarich" + +command -v infisical >/dev/null || { echo "ERROR: infisical CLI not installed"; exit 1; } +command -v openssl >/dev/null || { echo "ERROR: openssl not installed"; exit 1; } + +push() { + local name="$1" value="$2" + echo " -> $name" + infisical secrets set "${name}=${value}" \ + --projectId "$INF_PROJECT" --env "$ENV" --path "$PATH_PREFIX" \ + --type shared >/dev/null +} + +echo "==> Generating + pushing 5 Dawarich secrets to ${PATH_PREFIX}/ ..." +push vault_dawarich_db_password "$(openssl rand -hex 32)" +push vault_dawarich_secret_key_base "$(openssl rand -hex 64)" +push vault_dawarich_otp_primary_key "$(openssl rand -hex 32)" +push vault_dawarich_otp_deterministic_key "$(openssl rand -hex 32)" +push vault_dawarich_otp_salt "$(openssl rand -hex 32)" + +echo "==> Done." +echo +echo "Next steps:" +echo " 1) Apply terraform (module.dawarich) — provisions CT 459 on pve02 at .159." +echo " 2) Bootstrap cbalders user + SSH keys + NOPASSWD sudo on the new CT (see README)." +echo " 3) Generate + push 2 OIDC credentials (pi-auth uses these to CREATE the Authentik client — they are not fetched back):" +echo " infisical secrets set \"vault_dawarich_oidc_client_id=\$(openssl rand -hex 20)\" --projectId 50062d7c-06ff-4d5c-8ca3-6c0cdba9f270 --env prod --path /oidc --type shared" +echo " infisical secrets set \"vault_dawarich_oidc_client_secret=\$(openssl rand -hex 32)\" --projectId 50062d7c-06ff-4d5c-8ca3-6c0cdba9f270 --env prod --path /oidc --type shared" +echo " 4) Confirm homelab-ansible-pi-auth/site.yml has the dawarich_oidc_client_{id,secret} set_fact mapping." +echo " 5) First ./deploy.sh — picks up dawarich:1.7.11 (OIDC requires >= 1.7.8), runs migrations, comes up healthy." +echo " 6) cd ../homelab-ansible-pi-auth && ./deploy.sh — creates Authentik provider + application." +echo " 7) cd ../homelab-ansible-proxy && ./deploy.sh — adds Caddy vhost + Technitium CNAME." +echo " 8) cd ../homelab-ansible-pve && ./deploy.sh — adds CTID 459 to pbs-prod-daily." +echo " 9) Browser-test https://dawarich.balders.ca/users/sign_in → 'Sign in with Authentik'." diff --git a/site.yml b/site.yml new file mode 100644 index 0000000..53ead6b --- /dev/null +++ b/site.yml @@ -0,0 +1,101 @@ +--- +# ============================================================================== +# Dawarich LXC — Self-hosted location history (Google Timeline alternative) +# ============================================================================== +# Rails app + Sidekiq worker + Redis + PostGIS, all on this LXC. PostGIS lives +# here (not central DB VM .172) because the DB VM's postgres:16-alpine image +# doesn't bundle the postgis extension — see memory project_dawarich for the +# convention-exception rationale. +# +# Ingests: OwnTracks (HTTP, no MQTT broker needed), Dawarich's own iOS/Android +# app, Google Takeout exports. Auth: Authentik OIDC end-to-end. +# ============================================================================== + +- name: Deploy Dawarich LXC + hosts: all + become: true + vars_files: + - vars/main.yml + - vars/vault.yml + + pre_tasks: + - name: Deploy banner + debug: + msg: "===== {{ ansible_play_name }} → {{ inventory_hostname }} ({{ ansible_host }}) =====" + + - name: Install infisicalsdk on controller + pip: + name: infisicalsdk + state: present + extra_args: --break-system-packages + delegate_to: localhost + become: false + run_once: true + + - name: Authenticate with Infisical + infisical.vault.login: + url: "{{ infisical_url }}" + auth_method: universal_auth + universal_auth_client_id: "{{ infisical_client_id }}" + universal_auth_client_secret: "{{ infisical_client_secret }}" + register: infisical_login + delegate_to: localhost + become: false + run_once: true + + - name: Read dawarich secrets + infisical.vault.read_secrets: + login_data: "{{ infisical_login.login_data }}" + project_id: "{{ infisical_project_id }}" + env_slug: "prod" + path: "/dawarich" + as_dict: true + register: repo_secrets + delegate_to: localhost + become: false + run_once: true + + # OIDC creds live in /oidc alongside every other Authentik client (the same + # pair pi-auth pushes into Authentik). default('') keeps the play + # idempotent before secrets exist / OIDC is flipped on. + - name: Read oidc secrets + infisical.vault.read_secrets: + login_data: "{{ infisical_login.login_data }}" + project_id: "{{ infisical_project_id }}" + env_slug: "prod" + path: "/oidc" + as_dict: true + register: oidc_secrets + delegate_to: localhost + become: false + run_once: true + + - name: Read shared secrets + infisical.vault.read_secrets: + login_data: "{{ infisical_login.login_data }}" + project_id: "{{ infisical_project_id }}" + env_slug: "prod" + path: "/shared" + as_dict: true + register: shared_secrets + delegate_to: localhost + become: false + run_once: true + + - name: Map secrets to vars + set_fact: + dawarich_db_password: "{{ repo_secrets.secrets.vault_dawarich_db_password }}" + dawarich_secret_key_base: "{{ repo_secrets.secrets.vault_dawarich_secret_key_base }}" + # OTP encryption keys — Dawarich ships built-in defaults but rotating them + # off the public defaults is a one-line hardening win. All three required. + dawarich_otp_primary_key: "{{ repo_secrets.secrets.vault_dawarich_otp_primary_key }}" + dawarich_otp_deterministic_key: "{{ repo_secrets.secrets.vault_dawarich_otp_deterministic_key }}" + dawarich_otp_salt: "{{ repo_secrets.secrets.vault_dawarich_otp_salt }}" + dawarich_oidc_client_id: "{{ oidc_secrets.secrets.vault_dawarich_oidc_client_id | default('') }}" + dawarich_oidc_client_secret: "{{ oidc_secrets.secrets.vault_dawarich_oidc_client_secret | default('') }}" + watchtower_gotify_url: "{{ shared_secrets.secrets.vault_watchtower_gotify_url }}" + watchtower_api_token: "{{ shared_secrets.secrets.vault_watchtower_api_token }}" + + roles: + - dawarich + - alloy diff --git a/vars/main.yml b/vars/main.yml new file mode 100644 index 0000000..f4bb31a --- /dev/null +++ b/vars/main.yml @@ -0,0 +1,84 @@ +--- +timezone: America/Toronto + +packages: + - apt-utils + - bash-completion + - ca-certificates + - curl + - git + - gnupg + - htop + - net-tools + - openssh-server + - python3 + - python3-pip + - sudo + - vim + - wget + +users: + - name: cbalders + groups: sudo + shell: /bin/bash + +ssh_authorized_keys: + - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINALaic1jpoP6t1urbZqJLI1eU5NeTVD9k8AAMAvOvvk OfficeMini" + - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGzTHdCiQjhIHsGB8oMpyKtr9TZXrXeIRKwcwe698zMW Generated By Termius" + +# Alloy ships Docker logs + journald to Loki on observe.lan.balders.ca + node +# metrics to Prometheus. Pattern mirrors every other LXC. +alloy_host_label: dawarich +alloy_loki_url: http://observe.lan.balders.ca:3100/loki/api/v1/push +alloy_prom_job: node_lxc +alloy_prom_group: lxc +alloy_prom_hostname: dawarich + +# ------------------------------------------------------------------------------ +# Dawarich — self-hosted location history +# ------------------------------------------------------------------------------ +# Pin image tags (Dawarich ships breaking schema changes on minor bumps). Bump +# in lockstep across app + sidekiq, never split — they share the same DB +# migrations. Check release notes before bumping. +# Pinned tag — OIDC support first landed in 1.7.8 (2026-05-16). DO NOT +# downgrade below 1.7.8 without disabling OIDC. Bump in lockstep with sidekiq. +dawarich_image: "freikin/dawarich:1.7.11" +# Official PostGIS image — alpine variant. Strict superset of postgres:16-alpine +# so swapping to it later (if we ever centralize on DB VM) is non-breaking. +postgis_image: "postgis/postgis:16-3.4-alpine" +redis_image: "redis:7-alpine" + +dawarich_port: 3000 +dawarich_site_url: "https://dawarich.balders.ca" +dawarich_domain: "dawarich.balders.ca" + +# Database (local — central DB VM doesn't load PostGIS extension; see memory +# project_dawarich for the convention exception rationale). +dawarich_db_name: "dawarich" +dawarich_db_user: "dawarich" + +# Background processing — keep modest to leave headroom for the web tier. +# Dawarich docs default 5; bump cautiously if Sidekiq queue depth grows. +dawarich_sidekiq_concurrency: 5 + +# Geocoding — disabled in v1. Dawarich falls back to its internal lightweight +# lookup. Self-hosted Photon stack is a future addition; uncomment + set host +# when wired. +# dawarich_photon_api_host: "photon.lan.balders.ca" + +# OIDC via Authentik. Client id/secret live in Infisical /oidc/ (pushed into +# Authentik by homelab-ansible-pi-auth's oidc_clients role). +dawarich_oidc_enabled: true +dawarich_oidc_issuer: "https://auth.balders.ca/application/o/dawarich/" +dawarich_oidc_redirect_uri: "https://dawarich.balders.ca/users/auth/openid_connect/callback" +dawarich_oidc_provider_name: "Authentik" +# false → only OIDC users can sign in (we want SSO-only). First OIDC login +# becomes the canonical user account. +dawarich_allow_email_password_registration: false +dawarich_oidc_auto_register: true + +# Infisical (secrets source) +infisical_url: "https://secrets.balders.ca" +infisical_project_id: "50062d7c-06ff-4d5c-8ca3-6c0cdba9f270" +infisical_client_id: "828d2cc8-eb25-4b1e-a711-c9a4b1580106" +infisical_client_secret: "{{ vault_infisical_client_secret }}" diff --git a/vars/vault.yml b/vars/vault.yml new file mode 100644 index 0000000..d164004 --- /dev/null +++ b/vars/vault.yml @@ -0,0 +1,10 @@ +$ANSIBLE_VAULT;1.1;AES256 +66366365356236623964366166336662353433626337323337343365316662636332356636336534 +6364616163666431333863613639353837623165636264390a363030376536373966316230356335 +30623466653337326133666539343966656362613964353763636539623634396364633137323733 +3636613464393534660a313334393333343835616235613833346663373537363738383064363437 +34373430306665376639633032373961653134303233613164633738356166376234663039303138 +65313065383061636263393262353139646239383638303036313662373663316132333666366537 +65333866356235373830323734623730356138653338663538616666643230303835653461343236 +31616161333461356665316238363133316134376665353437386564313939356137313331613333 +35653238383931376131323834383633313930396533323032363863666138383332