# Ulicraft Server — Plan Overview

Self-hosted modded Minecraft LAN-party stack, designed to run **fully offline**
(air-gapped) during the party while remaining a persistent homelab service.

## Single source of configuration

Everything is driven by two env vars in `.env`:

```
BASE_DOMAIN=ulicraft.local      # all services are subdomains of this
HOST_LAN_IP=192.168.x.x         # the Docker host's LAN IP (dnsmasq target)
```

> `.local` collides with mDNS/Bonjour (RFC 6762). macOS + Linux Avahi may resolve
> `*.local` via multicast and bypass dnsmasq. Accepted risk for now; alternatives
> if it bites: `.lan`, `home.arpa`. Flagged, user chose `.local`.

## Subdomain / ingress map

```
ulicraft.local            → caddy → landing page (launcher downloads + guide)
auth.ulicraft.local       → caddy → drasl:25585      (auth web UI + Yggdrasil API)
packwiz.ulicraft.local    → caddy → /srv/pack         (pack metadata + mod-jar mirror)
assets.ulicraft.local     → caddy → /srv/assets       (MC asset-object mirror; HTTPS forced — see 10)
mc.ulicraft.local:25565   → minecraft                 (NOT http; raw MC protocol)
```

Caddy is the only HTTP ingress (plain `:80` for now, TLS/step-ca later).
drasl's host port is removed — internal only, reached through Caddy.

### Internal resolution trick

Containers use the Docker resolver, NOT dnsmasq. Caddy gets network aliases so
the stack resolves its own subdomains internally:

```yaml
caddy:
  networks:
    mcnet:
      aliases: [ "auth.${BASE_DOMAIN}", "packwiz.${BASE_DOMAIN}" ]
```

Result: `http://auth.ulicraft.local/authlib-injector` and
`http://packwiz.ulicraft.local/pack.toml` resolve identically inside and outside
the stack. dnsmasq exists **only for guest laptops**.

## Online vs offline split

```
PRE-PARTY  (internet, run once):
  1. curate packwiz pack            → see 04-packwiz.md
  2. mirror mod jars + rewrite URLs → tooling/mirror-mods.sh  (08-tooling.md)
  3. pre-bake server volume         → TYPE=NEOFORGE install    (05-minecraft.md)
  4. download launcher releases     → tooling/fetch-launcher.sh (06-launcher.md)
  5. capture+mirror client air-gap  → tooling/mirror-airgap.sh  (11-full-airgap-mirror.md)
  6. build landing page             → cd landing && pnpm run build → www/ (09-landing.md)
  7. render configs from templates  → tooling/render-config.sh  (08-tooling.md)

PARTY  (air-gapped):
  dnsmasq  : *.ulicraft.local → HOST_LAN_IP
  caddy    : local CDN + landing page
  drasl    : password login (NO Keycloak/OIDC for now)
  minecraft: TYPE=CUSTOM, no Mojang re-resolution
  guests   : install Fjord launcher, import pack, add authlib account → auth subdomain
```

## 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.
- **itzg can't air-gap-boot in NEOFORGE mode** (re-resolves Mojang metadata every
  start, issue #2340). Pre-bake then switch to `TYPE=CUSTOM`.
- **packwiz only serves metadata** — `.pw.toml` points at Modrinth CDN. Offline
  needs jars mirrored onto Caddy + URL rewrite.
- **Fjord launcher** has authlib-injector built in → no manual JVM agent on clients.

## Target folder layout

```
.
├── docker-compose.yml
├── .env / .env.example          # BASE_DOMAIN, HOST_LAN_IP, RCON_PASSWORD
├── caddy/Caddyfile              # {$BASE_DOMAIN} vhosts + landing page
├── dnsmasq/dnsmasq.conf.tmpl    # wildcard template
├── drasl/config/
│   ├── config.toml.tmpl         # envsubst source
│   └── config.toml              # generated (gitignored)
├── pack/                        # packwiz source of truth
├── mirror/                      # vendored mod jars + launcher releases (gitignored)
├── runtime/                     # authlib-injector.jar (legacy; may be unused w/ Fjord)
├── tooling/
│   ├── render-config.sh         # envsubst templates → real configs, halt if unset
│   ├── mirror-mods.sh           # vendor mod jars + rewrite .pw.toml URLs
│   ├── mirror-assets.sh         # mirror MC asset objects for offline clients (10)
│   └── fetch-launcher.sh        # download all Fjord release assets
├── landing/                     # Astro+TS landing source (build → www/)
│   ├── astro.config.mjs         # outDir ../www
│   ├── public/logo.png          # stone 3D logo
│   └── src/{data/site.ts,pages/index.astro}
├── www/                         # caddy landing page — GENERATED by landing build (gitignored)
├── backups/
└── plan/                        # these files
```

## Service plan files

- `01-dnsmasq.md` — LAN wildcard DNS
- `02-caddy.md` — ingress + local CDN + landing page
- `03-drasl.md` — auth (password login)
- `04-packwiz.md` — modpack source + mod mirror
- `05-minecraft.md` — itzg NeoForge server + offline launch
- `06-launcher.md` — Fjord launcher fetch + distribution
- `07-mc-backup.md` — world backups
- `08-tooling.md` — render/mirror/fetch scripts
- `09-landing.md` — Astro+TS guest onboarding page → www/
- `10-assets-mirror.md` — lighter alternative: objects-only Fjord Assets Server override
- `11-full-airgap-mirror.md` — CHOSEN: DNS+TLS transparent mirror for full client air-gap
- `12-build-order.md` — commit-per-task build sequence
- `13-dns-and-run-modes.md` — domain (.lan), online/airgap modes, party DNS, mDNS option

## Boot/dependency order

`dnsmasq` → `caddy` (aliases) → `drasl` → `minecraft` (depends on drasl + packwiz served by caddy) → `mc-backup`.
