- Shell 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| docs/superpowers | ||
| scripts | ||
| tests | ||
| .env.example | ||
| .gitignore | ||
| deploy.env | ||
| docker-compose.yml | ||
| README.md | ||
| renovate.json | ||
AdventureLog
AdventureLog at
https://adventure.waugze.com — private, invite-only trip and activity planning.
This service runs on the NAS (192.168.86.201), not on the server.
AdventureLog is GeoDjango and needs PostGIS, which the shared postgres18
instance does not have. Rather than maintain a custom shared image and restart
the database Zitadel and Umami depend on, the stack keeps its own PostGIS
container beside the app. Design rationale:
docs/superpowers/specs/2026-09-07-adventurelog-deployment-design.md.
Shape
Cloudflare (proxied CNAME → tunnel)
→ cloudflared → traefik:443 [server 192.168.86.203]
→ file-provider router `adventurelog` [traefik repo, traefik.yml]
→ http://192.168.86.201:8015 [LAN, plaintext]
→ adventurelog + adventurelog-db [NAS 192.168.86.201]
There is no Forgejo workflow here. UGOS provides no secret store for Docker projects, and opening SSH to a CI runner is a larger attack surface than this service warrants, so deploys are a deliberate manual step.
Deploy
INFISICAL_PROJECT_ID=<project-id> ./scripts/render.sh
Pulls secrets from the prod Infisical environment by default; set
INFISICAL_ENV to render against a different one.
That pulls the secrets, merges the non-secret deploy.env, and writes
build/docker-compose.yml with every value inlined. build/ is gitignored
and holds live secrets in plaintext. Do not commit it.
Before the first deploy, create the two bind-mount directories on the NAS
array (/volume1/docker/adventurelog/media and /volume1/docker/adventurelog/db)
— if they don't exist yet, Docker creates them itself, possibly at a path UGOS
resolves differently, which may not even be on the array.
Check the artifact carries no placeholder before you ship it — render.sh
rejects a missing or empty secret, but a secret whose value is a stand-in
renders as happily as a real one:
grep -E 'replace|changeme|dummy' build/docker-compose.yml \
&& echo "PLACEHOLDER PRESENT — fix the secret in Infisical before deploying" \
|| echo "no placeholders"
Copy that file into the UGOS Docker Project for adventurelog and apply it.
To change routing rather than the app, edit traefik.yml in the traefik
repo — that one still deploys through Forgejo on push to main.
Secrets
Infisical, dedicated project, prod environment, path /:
| Key | |
|---|---|
POSTGRES_PASSWORD |
shared by the app and db services |
SECRET_KEY |
Django signing key; openssl rand -hex 32 |
DJANGO_ADMIN_USERNAME |
first-boot superuser; inert on later boots |
DJANGO_ADMIN_PASSWORD |
|
DJANGO_ADMIN_EMAIL |
|
EMAIL_HOST_PASSWORD |
purelymail password for EMAIL_HOST_USER |
Non-secret host settings are committed in deploy.env: MEDIA_PATH, DB_PATH,
EMAIL_HOST_USER, DEFAULT_FROM_EMAIL.
Hazards
-
docker compose configrewrites relative bind paths against the machine doing the rendering.MEDIA_PATHandDB_PATHmust stay absolute NAS paths or the stack silently points at your workstation.scripts/render.shenforces this — it refuses to render if either one doesn't start with/, naming the offending key. -
Never add an
env_file:directive todocker-compose.yml.configinlinesenvironment:but leavesenv_fileas a path reference, and the NAS has no.envbeside the compose file. Upstream's compose file uses one; ours must not. -
The tunnel entry needs No TLS Verify. Cloudflare's public-hostname entry for
adventure.waugze.commust have it enabled, orcloudflaredvalidates the origin certificate against the literal nametraefik, Traefik serves its self-signed default, and the hostname 502s while everything else looks fine. The setting is per-entry and is not inherited. -
Cloudflare's free plan caps request bodies at 100 MB, which bounds photo uploads.
BODY_SIZE_LIMITis set to match so the app's own limit is not the confusing one. -
The image's nginx config is overridden, and that override must be re-checked on every upgrade. Django routes
/invitations/but the AIO image's nginx proxies only/(media|admin|accounts)to it, so every emailed account-invite link 404s on the SvelteKit frontend. Upstream bug, present in v0.13.0 and on main.docker-compose.ymlcarries a corrected copy inline as a Composeconfig(needs Compose >= 2.23; the NAS has 2.26.1), which keeps the rendered file the single artifact to copy over.On an image upgrade, diff the inline copy against
docker/shared/nginx-aio.confat the new tag and re-apply the one-word change — a stale copy silently reverts upstream's nginx fixes. If upstream fixes the routing, drop the override entirely.Inside that block every
$is written$$. Compose interpolates this file, so a bare$hostwould be eaten as an empty variable. The rendered artifact keeps the$$, and Compose resolves it to a literal$when the stack is applied on the NAS — verified against a real container, not assumed. -
UGOS's compose parser is stricter than Docker's. It rejected the rendered file with
Expected Numberbecausedocker compose configemitsmode: "0444"— a quoted string where the schema wants a number. Compose normalises and quotes numeric fields on output (memory limits becomememory: "3221225472", portspublished: "8015"), so a field Docker accepts can still fail in the UGOS UI. If a future compose change is rejected there, look for a quoted number before suspecting anything else, and prefer omitting a field whose default is already what you want. -
First boot is slow. Migrations plus the world/country data import take several minutes; the healthcheck allows a 300s start period. A container that looks unhealthy for the first few minutes is normal.
Invites and collaboration
Two different mechanisms, and the difference matters:
- Collection invites are the collaboration feature and need no privileges.
Any user shares a Collection with any other existing user; the recipient
accepts at
/invites. Both then edit that trip's locations, transportation, lodging, notes and checklists. - Account invites are how a new person gets onto the instance at all —
django-invitations, sent from/admin/invitations/invitation/add/. They work even thoughDISABLE_REGISTRATION=True, which is exactly the intended posture. They requireis_staffand working SMTP.
Granting invite rights without opening the admin
Django's admin index renders only the models a user holds permissions on, so a
narrowly-scoped group turns is_staff into invite-only access:
/admin/auth/group/add/→ nameInviters→ add exactlyinvitations | invitation | Can add invitationandinvitations | invitation | Can view invitation. Nothing else./admin/auth/user/→ the user → tickis_staff(notis_superuser) → add toInviters→ save.
Their /admin then contains the invite form and nothing else — no other
models, no delete actions. The operator's superuser stays the break-glass
account.
Backups
This stack is not covered by the postgres repo's pre-deploy pg_dumpall.
Both bind mounts live on the NAS array and inherit its snapshot schedule. For a
logical dump alongside that:
mkdir -p /volume1/docker/adventurelog/backups
docker exec adventurelog-db pg_dump -U adventure database \
| gzip > /volume1/docker/adventurelog/backups/adventurelog-$(date +%F).sql.gz
Media is plain files under MEDIA_PATH; it needs no dump, only the snapshot.
Tests
./tests/test-render.sh
Running the tests deletes build/docker-compose.yml. They render with dummy
secrets and clean up after themselves, which is right — leaving a dummy-valued
artifact where the real one belongs is how you deploy a broken stack. So render
after testing, never before, or you will cat a file that is no longer there.
Exercises scripts/render.sh against a stubbed Infisical CLI: a complete
export renders, inlines secrets, and is written at mode 600; a missing secret
fails and names itself; a secret present but set to an empty string also fails
and names itself; an empty export fails.