Files
ulicraft-server-v1/plan/00-overview.md
Oier Bravo Urtasun cd79b0e540 docs(files): document filestash across plan/README/deploy skill
Fold files./filestash into the docs that carry the service list, the vhost
map and the deploy procedure — README stack table, 00-overview, 02-caddy,
12-build-order, 14-deploy, and the deploy skill.

Two real gaps the filestash deploy exposed, now guardrails in the skill:
- update-all.sh never touches host nginx, so a NEW SUBDOMAIN silently 404s
  after deploy. It needs issue-letsencrypt.sh + render-nginx.sh --install.
- a new service with ${FOO:?} guards fails the WHOLE compose file, so a
  missing .env var means `up` starts nothing — and --hard has already run
  `down`. Check the host's .env BEFORE tearing the stack down.

Also: filestash/config.json is a single-file bind mount re-rendered every
run, so a rolling update-all leaves it on a stale inode (same trap as the
caddy conf) — force-recreate after any FILES_* change.

Correct the cochi divergence section: `git diff HEAD` on the host is now
empty (verified this deploy — the ff-pull succeeded). The documented
docker-compose.yml/package.json divergence was converged; only untracked
dnsmasq/, caddy-root-ca.crt and a stale pack/ remain. The stash-pull-pop
procedure it prescribed is no longer needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 18:23:18 +02:00

161 lines
7.4 KiB
Markdown

# Ulicraft Server — Plan Overview
Self-hosted modded Minecraft stack — a single Docker Compose file holds the
entire thing, fronted by the host's own nginx (Let's Encrypt TLS) and a caddy
internal router. Built for a LAN party but runs as a persistent homelab service.
Assumes the internet is present.
## Single source of configuration
Everything is driven by one env var in `.env`:
```
BASE_DOMAIN=ulicraft.net # all services are subdomains of this
```
`render-config.sh` substitutes `${BASE_DOMAIN}` into the templates (plus the
derived `REGISTRATION_*` and `FILES_*` values — see `20-files.md`).
DNS is **not** this repo's concern: point `${BASE_DOMAIN}` and all subdomains
(`auth.` `avatar.` `status.` `files.` `distribution.` `www.`) at the host running
nginx; that is configured outside this repo.
## Subdomain / ingress map
```
ulicraft.net → nginx → caddy → landing page (/srv/www) + /launcher/
auth.ulicraft.net → nginx → caddy → drasl:25585 (auth web UI + Yggdrasil API)
avatar.ulicraft.net → nginx → caddy → nmsr:8080 (skin/avatar renderer)
status.ulicraft.net → nginx → caddy → uptime-kuma:3001 (status page)
files.ulicraft.net → nginx → caddy → filestash:8334 (player file share, shared login)
distribution.ulicraft.net → nginx → caddy → /srv/distribution (static, another repo)
mc.ulicraft.net:25565 → minecraft (raw MC protocol, not HTTP)
```
The host's own nginx is the only public ingress: it terminates TLS with Let's
Encrypt certs and reverse-proxies every vhost to caddy by Host header. caddy is
published on a **localhost-only** port (`CADDY_HTTP_BIND=127.0.0.1`,
`CADDY_HTTP_PORT=8880`) and acts as the internal router. drasl, nmsr and
uptime-kuma have no host ports — they are reached only through caddy.
### Internal resolution trick
Containers use the Docker resolver. caddy gets network aliases so the stack
resolves its own subdomains internally:
```yaml
caddy:
networks:
mcnet:
aliases: [ "auth.${BASE_DOMAIN}" ]
```
Result: `http://auth.ulicraft.net/authlib-injector` resolves identically inside
the stack (minecraft → caddy) as the public name does outside it (client → nginx
→ caddy).
#### HTTPS variant (`extra_hosts: host-gateway`)
The caddy alias only serves **http** on `mcnet` (port 80). Once the stack moved
to https URLs (drasl `BaseURL`; the authlib-injector agent must match the
client's https endpoint), the internal http alias is no longer enough — the
container needs to reach a **TLS** terminator holding a valid cert.
Caddy does not terminate TLS internally; the host's nginx does (on `0.0.0.0:443`).
So pin the public name to the host in the service, not to caddy:
```yaml
minecraft:
extra_hosts:
- "auth.${BASE_DOMAIN}:host-gateway"
```
`/etc/hosts` (extra_hosts) wins over the Docker resolver, so the container reaches
the host's nginx (valid LE cert) → caddy → service. This is required because
**containers do not inherit the host's `/etc/hosts`** — on cochi the host resolves
`auth.` to the public IP, but inside a container the LAN resolver returns a
dead address (`192.168.0.3:443`, connection refused), which crash-loops session
validation on boot. `host-gateway` sidesteps DNS entirely.
## Build / run flow
```
PREP (run once, online):
1. render configs → tooling/render-config.sh (08-tooling.md)
2. build landing page → cd landing && pnpm run build → www/ (09-landing.md)
3. download launcher releases → tooling/fetch-launcher.sh (06-launcher.md)
4. sync server mods → tooling/sync-server-mods.sh (04-mods.md)
5. (TLS) issue Let's Encrypt certs → tooling/issue-letsencrypt.sh (15-letsencrypt.md)
6. (ingress) render+install nginx → tooling/render-nginx.sh --install (15-letsencrypt.md)
UP:
docker compose up -d --build
```
Bring-up is a single command. There are no run modes and no override compose
files — one `docker-compose.yml` holds drasl, minecraft, mc-backup, caddy, nmsr,
mc-status, uptime-kuma and filestash.
## Why each hard call was made
- **No OIDC/Keycloak (for now)** — drasl password login. Keycloak left out of
this stack's scope. Re-add later if desired.
- **Distribution-fed mods** — the modset's source of truth is the HeliosLauncher
distribution repo; clients install the full set, the server loads a filtered
`both`/`server` subset from `./server-mods`. See `04-mods.md`.
- **Fjord launcher** has authlib-injector built in → no manual JVM agent on
clients.
- **host nginx in front of caddy** — real Let's Encrypt certs (JVM trusts them
with no CA import) and a single public TLS terminator; caddy stays an internal
Host-routed multiplexer for the stack's own services.
## Target folder layout
```
.
├── docker-compose.yml # the entire stack (no overrides)
├── .env / .env.example # BASE_DOMAIN, RCON_PASSWORD, CADDY_*, LE block
├── caddy/conf.d/ # per-vhost snippets (00-core, 10-static, …)
├── nginx/ulicraft-caddy.conf.tmpl # host nginx → caddy template (TLS terminator)
├── certs/<name>/{cert,key}.pem # Let's Encrypt output (gitignored)
├── drasl/config/
│ ├── config.toml.tmpl # render-config.sh source
│ └── config.toml # generated (gitignored)
├── nmsr/ # avatar renderer config + Dockerfile context
├── filestash/
│ ├── config.json.tmpl # render-config.sh source (files. share)
│ └── config.json # generated, mounted :ro (gitignored — holds hashes)
├── docker/nmsr/ # nmsr image build context
├── server-mods/ # filtered server modset (synced, gitignored)
├── launcher/ # vendored Fjord launcher releases (gitignored)
├── runtime/ # authlib-injector.jar
├── tooling/ # render/fetch/issue scripts (see 08-tooling.md)
├── landing/ # Astro+TS landing source (build → www/)
├── www/ # landing build output — caddy apex (gitignored)
├── backups/
└── plan/ # these files
```
## Service plan files
- `02-caddy.md` — internal router (vhost conf.d snippets) behind host nginx
- `03-drasl.md` — auth (password login)
- `04-mods.md` — distribution-fed mod volume + server filtering (supersedes `04-packwiz.md`)
- `05-minecraft.md` — itzg NeoForge server
- `06-launcher.md` — Fjord launcher fetch + distribution
- `07-mc-backup.md` — world backups
- `08-tooling.md` — render/fetch/issue scripts
- `09-landing.md` — Astro+TS guest onboarding page → www/
- `12-build-order.md` — commit-per-task build sequence
- `14-deploy.md` — redeploy to the production host
- `15-letsencrypt.md` — Let's Encrypt (OVH DNS-01) + host nginx ingress
- `17-mod-shortlist.md` — kitchen-sink gap doc + landing mod-list dataset (PLANNED)
- `18-landing-rework.md` — join flow / rosters / account / mod list / footer (PLANNED)
- `19-routes.md` — HTTP route reference (nmsr / landing / auth / files + internal)
- `20-files.md` — Filestash player file share (`files.`, one shared login)
## Boot/dependency order
`caddy` (aliases) → `drasl``minecraft` (depends on drasl) → `mc-backup`.
`nmsr`, `mc-status`, `uptime-kuma` and `filestash` are always-on and join `mcnet`
(no ordering constraints — none of them are on the auth/boot path).