# 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.` `pack.` `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)
pack.ulicraft.net          → nginx → caddy → /srv/pack      (packwiz pack metadata)
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:

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

Result: `http://auth.ulicraft.net/authlib-injector` and
`http://pack.ulicraft.net/pack.toml` resolve identically inside the stack
(minecraft → caddy) as the public names do 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`, `PACKWIZ_URL`, 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 names to the host in the service, not to caddy:

```yaml
minecraft:
  extra_hosts:
    - "auth.${BASE_DOMAIN}:host-gateway"
    - "pack.${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./pack.` 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 the
packwiz install 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. curate packwiz pack            → see 04-packwiz.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.
- **packwiz only serves metadata** — `.pw.toml` points at Modrinth/CF CDNs;
  server and clients fetch jars from there. The internet is assumed present.
- **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
├── pack/                        # packwiz source of truth
├── 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-packwiz.md` — modpack source
- `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

## Boot/dependency order

`caddy` (aliases) → `drasl` → `minecraft` (depends on drasl + pack served by
caddy) → `mc-backup`. `nmsr` and `uptime-kuma` are always-on and join `mcnet`.
