The pack./packwiz service was removed operationally long ago, but several plan docs still presented pack. as a live caddy vhost / DNS subdomain and render-pack.sh as a current tool. Remove those references from the current-architecture docs (overview, caddy, tooling, uptime-kuma, letsencrypt, mc-backup); leave the migration log (04-mods.md), superseded banner (04-packwiz.md), commit history (12-build-order.md), and planned landing rework (18) intact as deliberate history. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6.9 KiB
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 only ${BASE_DOMAIN} into the templates.
DNS is not this repo's concern: point ${BASE_DOMAIN} and all subdomains
(auth. avatar. status. 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)
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:
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:
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
and uptime-kuma.
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/serversubset from./server-mods. See04-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
├── 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 nginx03-drasl.md— auth (password login)04-mods.md— distribution-fed mod volume + server filtering (supersedes04-packwiz.md)05-minecraft.md— itzg NeoForge server06-launcher.md— Fjord launcher fetch + distribution07-mc-backup.md— world backups08-tooling.md— render/fetch/issue scripts09-landing.md— Astro+TS guest onboarding page → www/12-build-order.md— commit-per-task build sequence14-deploy.md— redeploy to the production host15-letsencrypt.md— Let's Encrypt (OVH DNS-01) + host nginx ingress17-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 + internal)
Boot/dependency order
caddy (aliases) → drasl → minecraft (depends on drasl) → mc-backup.
nmsr and uptime-kuma are always-on and join mcnet.