Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

146 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

bluefox.cafe Foundry stack

Hosting D&D Foundry instances behind Discord OAuth, on Hetzner CPX33 (atlas).

Layout

  • docker-compose.yml - Docker Compose stack
  • overlays/ - per-environment port overlays (prod.yml, etc.)
  • caddy/Caddyfile - reverse proxy + auth config
  • data/ - bind-mounted state for every service (gitignored)
  • .env - secrets (NOT committed)
  • .env.example - template

Deploy

cp .env.example .env  # fill in values, including COMPOSE_FILE for your environment
chmod 600 .env
docker compose pull
docker compose up -d
docker compose logs -f caddy   # watch ACME issue real certs

Environment overlays

Ports are not published in the base docker-compose.yml. Each worktree's .env sets COMPOSE_FILE to merge the appropriate overlay:

Worktree COMPOSE_FILE value
prod docker-compose.yml:overlays/prod.yml
staging docker-compose.yml:overlays/staging.yml
dev docker-compose.yml

overlays/prod.yml publishes ports 80, 443, and 443/udp on the host. Create equivalent overlay files for staging/dev as needed (e.g. 8080/8443).

Theme testing

overlays/dev.yml adds a standalone caddy-dev service — plain caddy:alpine, no TLS, no auth, no connection to the prod stack. It serves all static pages on 127.0.0.1:8081 from the same bind-mounted caddy/ directories, so edits to HTML, CSS, and JS are reflected on the next browser refresh with no rebuild.

Note: Port 8080 is taken by CrowdSec's LAPI. The dev overlay uses 8081.

# Start (--no-deps skips all other services in the compose file)
docker compose -f docker-compose.yml -f overlays/dev.yml up caddy-dev --no-deps

# Working from a laptop? SSH tunnel first:
ssh -L 8081:localhost:8081 your-server
# then open http://localhost:8081

Available at http://localhost:8081:

Path Page
/ Homepage
/dnd DnD landing page
/shared/theme.css, /shared/theme.js Shared theme assets
/preview/beastworld/, /preview/demiplane/, /preview/dnd/, /preview/files/ Bot link-preview cards
/preview/403, /preview/404, /preview/500/preview/503 Error pages (no admin gate in dev)
/forbidden 403 page directly

Auth gates are stripped — all pages render immediately. OAuth links still point to auth.bluefox.cafe, but following them is fine; you just won't land back on localhost afterward.

Preview pages (/preview/beastworld/ etc.) still load theme.css/theme.js from https://bluefox.cafe/shared/ because in prod they're served from subdomains that resolve /shared/ against their own domain. To test theme changes, use the homepage or the DnD landing page instead.

Adding a new game

Each game is gated by its own Discord guild. Adding one means wiring up a new guild → role → policy chain.

  1. Env vars (.env + .env.example):
    • DISCORD_GUILD_GAMENAME (the guild ID gating this game)
    • FOUNDRY_*_GAMENAME if using per-game DM credentials
  2. Compose: duplicate a foundry-* block in docker-compose.yml (the ./data/foundry-gamename bind mount is created on first start). Add DISCORD_GUILD_GAMENAME to the caddy: environment: block too.
  3. Caddyfile - three additions:
    • Add the new guild ID to user_group_filters in the oauth identity provider discord block.
    • Add a transform pair (game role + authp/member) keyed off the new DISCORD_GUILD_GAMENAME.
    • Add an authorization policy gamename_policy (mirror of demiplane_policy).
    • Add a (gated_gamename) snippet and a gamename.bluefox.cafe { ... } site block importing it.
  4. DnD page: add a card for the new game in caddy/dnd/index.html.
  5. Uptime Kuma: add a monitor pointing at http://foundry-gamename:30000 (internal docker hostname - see operations note below).
  6. docker compose up -d.

Backups

Nightly restic snapshot at 09:00 UTC - see backup/README.md for full setup instructions. The Foundry games, filebrowser, crowdsec and Uptime Kuma are paused for the duration; Caddy and Dozzle stay up. A shell trap guarantees containers resume even if restic errors.

Quick reference:

  • Status: systemctl status backup-fox-cafe
  • Logs: journalctl -u backup-fox-cafe -n 50
  • List snapshots: source /etc/restic/fox-cafe.env && restic snapshots

Operations

  • Logs: docker compose logs -f <service>, or web UI at logs.bluefox.cafe. Container json-file logs are capped at 3×10MB per service via the x-logging anchor in compose - adjust there if you need more retention.
  • Health: kuma.bluefox.cafe
  • Container updates: all images are pinned. Bump tags manually after reading release notes (especially Caddy/authcrunch and Foundry).
  • Manual update: docker compose pull && docker compose up -d

Rotating the JWT signing key

JWT_SHARED_KEY signs the session cookies issued by the auth portal. Rotate it if you suspect leakage, after a contributor with .env access leaves, or periodically as hygiene.

caddy-security only loads one key at a time - there is no graceful overlap. Rotation invalidates every existing session; all users redirect to Discord once and re-auth. No data is lost.

# 1. Generate a new key
openssl rand -hex 32

# 2. Replace JWT_SHARED_KEY in .env
# 3. Restart caddy to pick it up
docker compose up -d caddy

Foundry game sessions (the in-app websocket) survive the cookie invalidation as long as the browser tab stays open; only the next navigation hits the auth gate.

CrowdSec

A CrowdSec agent runs in the stack, parses Caddy's JSON access log (caddy_logs named volume), and exposes LAPI on 127.0.0.1:8080. A host-side crowdsec-firewall-bouncer-nftables systemd service polls LAPI every 10s and maintains table ip crowdsec / table ip6 crowdsec6 in nftables - drops happen before Docker's DNAT, so banned IPs never reach Caddy.

The instance is enrolled in the CrowdSec console (https://app.crowdsec.net) for the community blocklist push and a remote dashboard. State lives at ./data/crowdsec/{data,config} and is picked up by the nightly backup.

# What's currently banned, and why
docker compose exec crowdsec cscli decisions list

# Recent scenarios that fired
docker compose exec crowdsec cscli alerts list

# Unban an IP (e.g. yourself after a false positive)
docker compose exec crowdsec cscli decisions delete --ip <ip>

# Confirm the host bouncer is connected and pulling
docker compose exec crowdsec cscli bouncers list
sudo systemctl status crowdsec-firewall-bouncer

# Console + CAPI status (enrolment, signal sharing, blocklist subscriptions)
docker compose exec crowdsec cscli console status
docker compose exec crowdsec cscli capi status

# Inspect the kernel-level blocklist
sudo nft list table ip crowdsec

If the bouncer dies, existing nftables rules stay in place (no protection lost) but no new decisions get enforced until it restarts: sudo systemctl restart crowdsec-firewall-bouncer.

Uptime Kuma monitors

When configuring monitors in Kuma, probe the internal docker hostnames, not the public URLs. The public URLs sit behind the Discord auth gate, so a public-URL monitor would always see a redirect to auth.bluefox.cafe and read as "down".

Use these targets instead (they resolve over the web bridge network):

  • Foundry games: http://foundry-<gamename>:30000
  • Dozzle: http://dozzle:8080
  • Caddy itself: http://caddy:80 (or just monitor the public auth portal, which is the one URL that's not gated)

URLs

Auth model

One Discord login produces one JWT cookie at the apex (.bluefox.cafe) that covers every subdomain. What the cookie unlocks depends on which roles the user has - the cookie is the same, the authorization differs per site.

Roles assigned by the Caddyfile transforms:

  • authp/guild_demiplane - set if the user is in the Demiplane Discord guild (currently gates both Demiplane and Beastworld)
  • authp/member - set if the user is in any gating guild
  • authp/admin - set if the user's Discord ID matches DISCORD_ADMIN_USER_ID
  • authp/user - set on every Discord login (compatibility for authcrunch internals; not used for service authorization)

Scaffolding for a second guild (DISCORD_GUILD_EXAMPLE_2authp/guild_example_2) is present in the Caddyfile but commented out - activate the transform, user_group_filters entry, and policy together when adding a second guild.

Policies (one per access tier):

  • dnd_policy - authp/member or authp/admin
  • demiplane_policy - authp/guild_demiplane or authp/admin (gates demiplane + beastworld)
  • admin_policy - authp/admin only

About

Self-hosted Foundry VTT stack for bluefox.cafe, gated by Discord OAuth and fronted by Caddy.

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages