Files
ulicraft-server-v1/plan/14-deploy.md
Oier Bravo Urtasun 27237bc7c1 feat(mods): replace packwiz with distribution-fed custom mod volume
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>
2026-06-09 23:34:38 +02:00

5.4 KiB
Raw Blame History

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 downcompose 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 forced httphttps (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. Stashpullpopgit stashgit pull --ff-onlygit 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

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 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).