Files
oikos/plans/2026-06-25-yuvomi-deployment.md

13 KiB

Yuvomi deployment — house.hubris.network

Deploy Yuvomi (previously Oikos) — a self-hosted family planner with 14 modules (calendar, tasks, meals, groceries, budget, documents, notes, etc). Single Docker container (Express.js + SQLCipher SQLite), 256 MB RAM min.

Target hostname: house.hubris.network — publicly reachable via VPS traefik, LAN reachable via Caddy.

Integrations:

  • Authentik SSO (OIDC)
  • Google Calendar (tokens exist on trmnl LXC 128)
  • Paperless (Yuvomi's Documents module / clarification needed — see Phase 4)

Phase 0 — Clarifications needed

0.1 Paperless connection

Yuvomi's "Documents" module stores documents inside its encrypted SQLite DB or optionally on WebDAV. There is no direct Paperless-ngx API connector in Yuvomi. Options:

a) Keep as-is — Yuvomi's docs are separate from Paperless, no integration b) WebDAV bridge — Mount Paperless's consumption dir as WebDAV, point Yuvomi doc storage there (Yuvomi stores newly uploaded docs directly in the Paperless consume folder) c) Custom module — Write a Yuvomi module that fetches from Paperless API

Decision needed before Phase 3 config.

Decision: WebDAV bridge (Phase 6.2).

0.2 Deployment target

Two options:

Option Pros Cons
apps LXC (105) — Docker already there, 4GB RAM, 2 cores Zero provisioning, existing compose pattern Shared with artifacto, MCP, secrets-issuance; Portainer-managed stacks can be tricky
New LXC (~129) — dedicated, clean Isolated, no side-effects Need to create, install Docker, wire into everything

Decision: New LXC (129).


Phase 1 — Provision new LXC (129) for Yuvomi

1.1 Create the LXC on hubris

ssh root@192.168.8.77 << 'EOF'
# Check available templates
pveam list local | grep debian

# Create unprivileged Debian 13 LXC (follows trmnl's unpriv pattern)
pct create 129 local:vztmpl/debian-13-standard_13.7-1_amd64.tar.zst \
  --hostname house \
  --description "Yuvomi family planner — house.hubris.network" \
  --cores 1 \
  --memory 1024 \
  --swap 512 \
  --rootfs local:8 \
  --net0 name=eth0,bridge=vmbr0,ip=dhcp,type=veth \
  --unprivileged 1 \
  --features nesting=1 \
  --onboot 1 \
  --start 1
EOF

Resources: 1 core / 1 GiB RAM / 8 GiB rootfs (generous for a single Express.js container; can downsize later).

1.2 Set static IP and install Docker

After the LXC boots, find its DHCP lease, then set a static IP:

# Find actual IP
ssh root@192.168.8.77 'lxc-attach 129 -- ip addr show eth0 | grep inet'

# Reserve 192.168.8.212 (or whatever is free) via Technitium DHCP,
# or set static IP in PVE config:
ssh root@192.168.8.77 'pct set 129 --net0 name=eth0,bridge=vmbr0,ip=192.168.8.212/24,gw=192.168.8.1,type=veth'
ssh root@192.168.8.77 'lxc-attach 129 -- reboot'

1.3 Install Docker inside the LXC

ssh root@192.168.8.77 << 'DOCKER'
lxc-attach 129 -- bash -c '
  apt-get update
  apt-get install -y ca-certificates curl
  install -m 0755 -d /etc/apt/keyrings
  curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
  chmod a+r /etc/apt/keyrings/docker.asc
  echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian $(. /etc/os-release && echo \"$VERSION_CODENAME\") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
  apt-get update
  apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
  systemctl enable --now docker
  docker --version
  docker compose version
'
DOCKER

1.4 Download Yuvomi and start

ssh root@192.168.8.77 'lxc-attach 129 -- bash -c "
mkdir -p /opt/yuvomi /opt/yuvomi/data /opt/yuvomi/backups /opt/yuvomi/modules
cd /opt/yuvomi
curl -O https://raw.githubusercontent.com/ulsklyc/yuvomi/main/docker-compose.yml
curl -O https://raw.githubusercontent.com/ulsklyc/yuvomi/main/.env.example
cp .env.example .env
"'

1.5 Generate keys and configure .env

ssh root@192.168.8.77 'lxc-attach 129 -- bash -c "
SESSION_SECRET=\$(openssl rand -hex 32)
DB_KEY=\$(openssl rand -hex 32)
cd /opt/yuvomi
sed -i \"s/SESSION_SECRET=.*/SESSION_SECRET=\$SESSION_SECRET/\" .env
sed -i \"s/DB_ENCRYPTION_KEY=.*/DB_ENCRYPTION_KEY=\$DB_KEY/\" .env
sed -i \"s/OIKOS_HTTP_PORT=3000/OIKOS_HTTP_PORT=3000/\" .env
sed -i \"s/# TZ=.*/TZ=Europe\\/Berlin/\" .env
echo \"SESSION_SECURE=true\" >> .env
echo \"TRUST_PROXY=1\" >> .env
echo \"BASE_URL=https://house.hubris.network\" >> .env
"'

1.6 Start Yuvomi

ssh root@192.168.8.77 'lxc-attach 129 -- bash -c "cd /opt/yuvomi && docker compose up -d"'

1.7 Verify

ssh root@192.168.8.77 'lxc-attach 129 -- curl -s http://127.0.0.1:3000/health'
# Expected: 200 OK

---

## Phase 2

### 2.1 Caddy — add `house.hubris.network`

Edit `/etc/caddy/Caddyfile` on LXC 121 (via `dtoro/caddy-conf` repo):

house.hubris.network { tls { dns ionos } reverse_proxy 192.168.8.212:3000 }


- Commit to `dtoro/caddy-conf` → auto-deploy via webhook
- If not yet deployed, push manually: `cd /etc/caddy && git add Caddyfile && git commit -m 'add house.hubris.network → yuvomi' && git push`

### 2.2 Verify LAN access

curl -sI https://house.hubris.network/

Expected: 200 or 302 (redirect to /login or the setup wizard)


### 2.3 DNS — add Technitium record

Add A record `house.hubris.network → 192.168.8.175` (Caddy) on DNS LXC (107).

If using the DNS web UI: http://192.168.8.2/ → Zones → hubris.network → Add A record.

### 2.4 DNS mesh sync

If mesh DNS (Netbird managed zone) is in use, add the same record there or
verify dns-sync picks it up.

---

## Phase 3 — Public exposure (VPS traefik)

### 3.1 Add cert sync entry

On hubris (PVE host), edit `/usr/local/bin/hubris-public-cert-sync.sh`, add:

```bash
[house.hubris.network]="house.fullchain.crt house.privkey.key"

Run once:

systemctl start hubris-public-cert-sync.service

Verify certs landed on VPS:

ssh root@100.122.165.149 "ls -la /var/lib/docker/volumes/opt_netbird_traefik_letsencrypt/_data/house.*"

3.2 Add traefik router

On the VPS, edit /opt/traefik-dynamic.yaml:

http:
  routers:
    house-public:
      rule: 'Host(`house.hubris.network`)'
      entryPoints:
        - websecure
      priority: 10
      tls: {}
      middlewares:
        - house-ratelimit
      service: house-public

  middlewares:
    house-ratelimit:
      rateLimit:
        average: 30
        period: 1s
        burst: 60

  services:
    house-public:
      loadBalancer:
        servers:
          - url: 'http://192.168.8.212:3000'

tls:
  certificates:
    - certFile: /letsencrypt/house.fullchain.crt
      keyFile: /letsencrypt/house.privkey.key

Restart traefik:

docker restart netbird-traefik

3.3 Verify public access

From outside the homelab LAN (or with --resolve):

curl -sI --resolve house.hubris.network:443:82.165.190.79 https://house.hubris.network/
# Expected: 200 or 302

echo | openssl s_client -connect 82.165.190.79:443 -servername house.hubris.network 2>&1 | openssl x509 -noout -subject
# Expected: CN=house.hubris.network (not TRAEFIK DEFAULT CERT)

Phase 4 — Authentik SSO (OIDC)

4.1 Create OIDC provider in Authentik

Via VPS admin UI (https://auth.hubris.network/if/admin/):

  • Applications → Providers → Create → OAuth2/OpenID Provider
  • Name: yuvomi
  • Client ID: auto-generated
  • Client Secret: auto-generated (save this)
  • Redirect URIs: https://house.hubris.network/oauth2/callback
  • Signing Key: auto-generated
  • Subject Mode: Based on User ID (or Based on Username — pick what Yuvomi expects)

4.2 Create application in Authentik

  • Applications → Applications → Create
  • Name: Yuvomi
  • Slug: yuvomi
  • Provider: select the one created above
  • Launch URL: https://house.hubris.network

4.3 Set env vars in Yuvomi .env

On apps LXC (105), edit /opt/yuvomi/.env:

OIDC_ISSUER=https://auth.hubris.network/application/o/yuvomi/
OIDC_CLIENT_ID=<from Authentik>
OIDC_CLIENT_SECRET=<from Authentik>
# OIDC_TRUST_EMAIL_WITHOUT_VERIFIED_CLAIM=true   # if Authentik doesn't send email_verified

4.4 Restart Yuvomi

pct exec 105 -- bash -c 'cd /opt/yuvomi && docker compose restart'

4.5 Verify SSO flow

Open https://house.hubris.network/ — should redirect to Authentik login, then back to Yuvomi.


Phase 5 — Google Calendar

5.1 Extract tokens from trmnl LXC

On trmnl (LXC 128), the env file at /etc/trmnl-plugins/env contains:

GOOGLE_CLIENT_ID=119823214387-32f20ed3imesiv7uh5si7p3rou9fros4.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=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
GOOGLE_REFRESH_TOKEN=1//03CS0rkuf7XVQCgYIARAAGAMSNwF-L9Irr5_b4kLhkx-dtf9EGQ1eJ1OnvxkaV_P1_4TDPwUN2lFb7nsbrZklN7qbjpkHpQyYlBY

5.2 Add Google Account redirect URI

In the Google Cloud Console (OAuth 2.0 Client IDs), add:

https://house.hubris.network/auth/google/callback

to the authorized redirect URIs for the existing client ID.

5.3 Set env vars in Yuvomi .env

GOOGLE_CLIENT_ID=119823214387-32f20ed3imesiv7uh5si7p3rou9fros4.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=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

Note: Yuvomi's Google Calendar integration uses the OAuth flow to get its own refresh token — it doesn't reuse the trmnl refresh token. The first-time setup in Yuvomi Settings → Calendar → Google Calendar will prompt for authorization.

5.4 Restart and verify

pct exec 105 -- bash -c 'cd /opt/yuvomi && docker compose restart'

Then in Yuvomi UI: Settings → Calendar → Connect Google Calendar → authorize.


Phase 6 — Paperless integration (decide approach first)

6.1 If using as standalone documents module (no Paperless bridge)

No action needed. Yuvomi's Documents module works out of the box — docs stored in encrypted SQLite.

6.2 If using WebDAV bridge to Paperless consumption

  • Paperless consumes documents from /mnt/library/documents/consume/
  • Point Yuvomi's WebDAV document storage at a WebDAV server serving that dir
  • Options: run a lightweight WebDAV container on paperless LXC (103), or use Nextcloud's WebDAV if documents are already in /mnt/library

Set env vars:

DOCUMENT_STORAGE_WEBDAV_ENABLED=true
DOCUMENT_STORAGE_WEBDAV_URL=http://192.168.8.130:8000/...  # or WebDAV server
DOCUMENT_STORAGE_WEBDAV_USERNAME=...
DOCUMENT_STORAGE_WEBDAV_PASSWORD=...
DOCUMENT_STORAGE_WEBDAV_ALLOW_PRIVATE_NETWORK=true

6.3 If building a custom module

Write a Yuvomi module (client-side JS + module.json) that reads from Paperless API at https://paperless.hubris.network/api/ using a Paperless API token. See modules/MODULES.md in the Yuvomi repo for the module format.


Phase 7 — Backup & maintenance

7.1 Data persistence

Yuvomi stores everything in a single SQLCipher-encrypted SQLite file at /opt/yuvomi/data/oikos.db. This is the only file needed for backup.

7.2 Add to homelab context

  • Create /opt/homelab-context/containers/129-yuvomi.md (or .../house.md)
  • Update inventory.yaml if using a new LXC
  • Add changelog entries to caddy (121) and ingress docs
  • Update plans/index.md → mark this plan Done

7.3 Schedule backup

Add a cron (or existing backup system) for /opt/yuvomi/data/ if not already covered by the host-level backup scheme.


Summary of steps

Phase What Who/Where
0 Clarify Paperless approach + deployment target dtoro
1 Docker Compose on apps LXC, start container Hermes
2 Caddy block + DNS record for house.hubris.network Hermes
3 VPS traefik router + cert sync for public exposure Hermes
4 Authentik OIDC provider + env vars Hermes (needs admin UI)
5 Google Calendar tokens + redirect URI Hermes + dtoro (Google Cloud Console)
6 Paperless integration (depends on Phase 0 decision) Hermes
7 Documentation, backup, inventory updates Hermes

Duration estimate

Phase Time Notes
Phase 1 ~15 min Download, config, startup
Phase 2 ~10 min Caddy + DNS
Phase 3 ~15 min VPS traefik + cert sync
Phase 4 ~20 min Authentik provider setup + env
Phase 5 ~10 min + Google UI Redirect URI takes 1 min in console
Phase 6 TBD Depends on chosen approach
Phase 7 ~10 min Doc + inventory updates
Total ~1.5h + Phase 6

Rollback

If anything goes wrong:

# Stop and remove container
pct exec 105 -- bash -c 'cd /opt/yuvomi && docker compose down'

# Remove Caddy block, commit, push — auto-deploys
# Remove VPS traefik router, restart netbird-traefik
# Remove cert sync entry
# Remove DNS record