Drop packwiz entirely. The server now loads a filtered mod subset from a local ./server-mods volume instead of fetching a packwiz pack at boot. New flow: - tooling/classify-mods.py (tomllib only, no network) reads each jar's META-INF/neoforge.mods.toml and seeds mods-sides.json with a side (client|server|both; default both). Existing entries are preserved. - mods-sides.json: committed, keyed by jar filename, hand-editable override surface. Initial seed: 65 both, 11 client, 0 server (76 jars). - tooling/sync-server-mods.sh mirrors the both/server jars from $DISTRIBUTION_WEB_ROOT into ./server-mods/ (authoritative; prunes strays; halts on a missing jar). Replaces render-pack.sh. - compose: minecraft mounts ./server-mods:/mods:ro with REMOVE_OLD_MODS=TRUE (itzg auto-syncs /mods -> /data/mods). Removed PACKWIZ_URL, the pack. extra_host (auth. kept for authlib), and depends_on caddy (drasl kept). Both classify-mods.py and sync-server-mods.sh resolve their source dir as: CLI arg / MODS_DIST_DIR env / $DISTRIBUTION_WEB_ROOT (in that order). Removed: tooling/render-pack.sh + add-custom-mod.sh (packwiz tooling), pack/ (packwiz source), custom/ (76 jars now sourced from the distribution), the pack. caddy vhost + its mounts/alias, and the packwiz .gitignore block. Client distribution is UNCHANGED: HeliosLauncher still installs the full modset from distribution.json. Docs (CLAUDE.md, plan/04/05/14, runtime) updated; plan/04-packwiz.md stubbed as superseded by plan/04-mods.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
135 lines
5.4 KiB
Markdown
135 lines
5.4 KiB
Markdown
# Deployment — redeploy to `cochi`
|
||
|
||
Production host for the Ulicraft stack.
|
||
|
||
| | |
|
||
|---|---|
|
||
| Host | `cochi` (SSH alias) |
|
||
| Path | `/home/ubuntu/mc/ulicraft-server-v1` |
|
||
| Branch | `main` |
|
||
| Compose | single `docker-compose.yml` (no overrides) |
|
||
| Auth | Drasl (password login) |
|
||
| Ingress | host nginx (Let's Encrypt TLS) → caddy → services (see 15-letsencrypt.md) |
|
||
| Restart | **hard** (`compose down` → `compose up --build`) — brief full downtime |
|
||
|
||
## Routine redeploy
|
||
|
||
```bash
|
||
ssh cochi
|
||
cd /home/ubuntu/mc/ulicraft-server-v1
|
||
git pull --ff-only
|
||
tooling/render-config.sh # re-render drasl/nmsr configs from .env
|
||
tooling/sync-server-mods.sh # sync ./server-mods from $DISTRIBUTION_WEB_ROOT
|
||
docker compose down --remove-orphans
|
||
docker compose up -d --build
|
||
```
|
||
|
||
Or as a one-shot from your workstation:
|
||
|
||
```bash
|
||
ssh cochi 'set -e
|
||
cd /home/ubuntu/mc/ulicraft-server-v1
|
||
git pull --ff-only
|
||
tooling/render-config.sh
|
||
tooling/sync-server-mods.sh
|
||
docker compose down --remove-orphans
|
||
docker compose up -d --build'
|
||
```
|
||
|
||
`render-config.sh` re-renders the drasl/nmsr configs from `.env`;
|
||
`sync-server-mods.sh` mirrors the `both`/`server` mod subset (from
|
||
`mods-sides.json`) out of `$DISTRIBUTION_WEB_ROOT` into `./server-mods` before
|
||
the stack comes back up. `$DISTRIBUTION_WEB_ROOT` must resolve on the host (it
|
||
also backs the caddy `distribution.` mount).
|
||
|
||
### Landing rebuild (not part of `compose`)
|
||
|
||
The landing site (incl. `/register`) is static `www/`, gitignored — a `git pull`
|
||
does **not** update it. Rebuild on the host whenever `landing/` changed or you
|
||
toggled `REGISTRATION_MODE` (it bakes the invite-field on/off at build, and Drasl's
|
||
`RequireInvite` is derived from the same key by `render-config.sh`):
|
||
|
||
```bash
|
||
( cd landing && set -a && . ../.env && set +a && pnpm run build ) # → www/
|
||
docker compose restart drasl # apply CORS / RateLimit / RequireInvite
|
||
```
|
||
|
||
`down`/`up` is not needed for a landing-only or flag-only change — rebuild `www/`
|
||
and restart `drasl`.
|
||
|
||
## Known host-local divergence on `cochi` (uncommitted)
|
||
|
||
`cochi` carries deliberate local edits that are **not in the repo**, so
|
||
`git pull --ff-only` **aborts** ("local changes would be overwritten") whenever
|
||
an incoming commit also touches `docker-compose.yml`. As of 2026-06-09 the host
|
||
has, uncommitted:
|
||
|
||
- `docker-compose.yml` — internal URLs forced `http`→`https`
|
||
(`JVM_OPTS` authlib-injector), for the in-container HTTPS path. (The old
|
||
`PACKWIZ_URL` divergence is moot — packwiz was removed; see `04-mods.md`.)
|
||
- `dnsmasq/` (new), `caddy/caddy-root-ca.crt`, `landing/pnpm-workspace.yaml` —
|
||
internal DNS + private CA so containers resolve/trust `https://auth.` etc.
|
||
- `landing/package.json` modified. (The former `pack/` divergence is resolved —
|
||
`pack/` and `custom/` were deleted with the packwiz removal.)
|
||
|
||
**Do not steamroll it.** Before a deploy that edits `docker-compose.yml`, either:
|
||
1. **Converge** — commit the host edits to `main` + push, then a clean ff-pull
|
||
deploys (preferred — ends the divergence). The https/dnsmasq/CA setup belongs
|
||
in the repo.
|
||
2. **Stash–pull–pop** — `git stash` → `git pull --ff-only` → `git stash pop`.
|
||
Non-destructive; safe only when the incoming and host edits are in different
|
||
regions of the file. Stop on a pop conflict.
|
||
|
||
Never `git reset --hard` / discard on the host without confirming first.
|
||
|
||
## What survives a hard restart
|
||
|
||
Named volumes persist — world and auth data are safe:
|
||
- `mc_data` — world + installed NeoForge/mods
|
||
- `drasl_state` — Drasl accounts/skins
|
||
- `kuma_data` — Uptime Kuma config/history
|
||
- `backups/` — mc-backup snapshots
|
||
|
||
`down` removes **containers**, not volumes. Players are disconnected during the
|
||
restart (a few minutes); they reconnect once `minecraft` is healthy again.
|
||
|
||
## Prerequisites (one-time, on `cochi`)
|
||
|
||
- SSH access as the `cochi` alias.
|
||
- `.env` present and complete: `BASE_DOMAIN`, `RCON_PASSWORD`, `CADDY_HTTP_PORT`,
|
||
`CADDY_HTTP_BIND`, `DISTRIBUTION_WEB_ROOT`, `REGISTRATION_MODE` (`invite`|`open`,
|
||
defaults `invite`), and the Let's Encrypt block (`render-config.sh` halts on an
|
||
unset `BASE_DOMAIN` or an invalid `REGISTRATION_MODE`).
|
||
- Docker + compose plugin, `envsubst`, `jq` (used by `sync-server-mods.sh`;
|
||
falls back to `python3` if absent), `git`.
|
||
- Host nginx + Let's Encrypt certs installed once — see `15-letsencrypt.md`.
|
||
- **First deploy only** — online prep (build landing, fetch launcher, fetch
|
||
authlib into `runtime/`). Not needed for routine redeploys.
|
||
|
||
## Verify
|
||
|
||
```bash
|
||
docker compose ps # caddy, drasl, minecraft, mc-backup, nmsr, mc-status, uptime-kuma up
|
||
# self-hosted live status feed (plan/15-mc-status.md):
|
||
curl -fsS "https://$(grep '^BASE_DOMAIN=' .env | cut -d= -f2)/api/mcstatus/v2/status/java/$(grep '^BASE_DOMAIN=' .env | cut -d= -f2)" | jq .online
|
||
docker compose logs -f minecraft # watch for "Done"
|
||
curl -fsS "https://auth.$(grep '^BASE_DOMAIN=' .env | cut -d= -f2)/" >/dev/null && echo "auth ok"
|
||
```
|
||
|
||
## Rollback
|
||
|
||
```bash
|
||
git log --oneline -5 # find the last-good commit
|
||
git checkout <sha> # or: git reset --hard <sha>
|
||
docker compose down --remove-orphans
|
||
docker compose up -d --build
|
||
```
|
||
|
||
## Notes
|
||
|
||
- Production needs internet on `cochi` at restart: the server installs/re-syncs
|
||
NeoForge + mods over the network.
|
||
- All ingress goes through host nginx → caddy. After a redeploy, nginx config is
|
||
unchanged; only re-run `tooling/render-nginx.sh --install` if `BASE_DOMAIN` /
|
||
`CADDY_HTTP_PORT` changed (see `15-letsencrypt.md`).
|