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>
5.4 KiB
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
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:
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):
( 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 forcedhttp→https(JVM_OPTSauthlib-injector), for the in-container HTTPS path. (The oldPACKWIZ_URLdivergence is moot — packwiz was removed; see04-mods.md.)dnsmasq/(new),caddy/caddy-root-ca.crt,landing/pnpm-workspace.yaml— internal DNS + private CA so containers resolve/trusthttps://auth.etc.landing/package.jsonmodified. (The formerpack/divergence is resolved —pack/andcustom/were deleted with the packwiz removal.)
Do not steamroll it. Before a deploy that edits docker-compose.yml, either:
- 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. - 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/modsdrasl_state— Drasl accounts/skinskuma_data— Uptime Kuma config/historybackups/— 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
cochialias. .envpresent and complete:BASE_DOMAIN,RCON_PASSWORD,CADDY_HTTP_PORT,CADDY_HTTP_BIND,DISTRIBUTION_WEB_ROOT,REGISTRATION_MODE(invite|open, defaultsinvite), and the Let's Encrypt block (render-config.shhalts on an unsetBASE_DOMAINor an invalidREGISTRATION_MODE).- Docker + compose plugin,
envsubst,jq(used bysync-server-mods.sh; falls back topython3if 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
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
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
cochiat 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 --installifBASE_DOMAIN/CADDY_HTTP_PORTchanged (see15-letsencrypt.md).