Phase 1 of plan/23-monitor.md: a small Go service that tails the Minecraft server's latest.log into an in-memory ring buffer and serves it to an embedded single-page UI (autoscroll, substring/regex filter, level chips). Read-only by construction — /data is mounted :ro, there is no mutating route, and the container holds no credentials. It publishes NO port: caddy's basic_auth vhost is the only way in, which is the point, since the service has no auth of its own and the log carries player IPs, chat and admin commands. Two non-obvious choices, both to avoid prod-only failures: - Polling, not SSE. The chain is host nginx -> caddy -> app, and nginx buffers proxied responses by default, so an EventSource would stall on cochi (whose vhost is an untracked per-host file) and not reproduce locally. Delay was accepted at design time, so polling is strictly better. - The cursor is a sequence number, not a byte offset, and the tailer reopens on inode change or size shrink. itzg rotates latest.log on every server restart, i.e. every deploy — a naive tailer goes silent afterwards while still answering HTTP 200. Verified against rotate, truncate, delete/recreate, partial-line and ring-overflow cases. MONITOR_ENABLED (on|off) gates both layers per host, mirroring FILES_BROWSE. MONITOR_PASSWORD_HASH must be bcrypt AND single-quoted in .env: Compose interpolates $-sequences in .env values, so a bare hash is silently mangled ($2a$14$abc... -> $$2a$$14...) and every login fails with no hint why. Confirmed empirically, and it is the opposite of filestash, which needs $6$. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
180 lines
9.3 KiB
Bash
180 lines
9.3 KiB
Bash
# Copy to .env and fill in real values
|
|
|
|
# Single base domain for the whole stack; every service is a subdomain of this.
|
|
# DNS is handled OUTSIDE this repo — point ${BASE_DOMAIN} and every subdomain
|
|
# (auth. avatar. status. distribution. www.) at the host running nginx.
|
|
BASE_DOMAIN=ulicraft.net
|
|
|
|
RCON_PASSWORD=change-me-to-something-random
|
|
|
|
# Registration mode (BACKEND gate) for self-hosted accounts (Drasl).
|
|
# invite = closed; guests need an admin-minted invite code (recommended on a
|
|
# public, internet-present host). Admin mints via POST /drasl/api/v2/invites.
|
|
# open = anyone can self-register with username+password (set [RateLimit]!).
|
|
# Consumed by render-config.sh (-> Drasl RegistrationNewPlayer.RequireInvite).
|
|
# Default: invite. NOTE: this no longer controls the landing invite FIELD — that's
|
|
# REGISTRATION_SHOW_INVITE below.
|
|
REGISTRATION_MODE=invite
|
|
|
|
# Show the invite-code FIELD on the landing register form. Read by the landing
|
|
# BUILD (data/site.ts) only; default false (hidden). Independent of the backend
|
|
# gate above. CAVEAT: if REGISTRATION_MODE=invite (Drasl requires a code) but this
|
|
# is false, public self-registration can't supply one — enable both together, or
|
|
# mint accounts admin-side. true | false.
|
|
REGISTRATION_SHOW_INVITE=false
|
|
|
|
# NMSR avatar render mode for the landing's avatar tiles. Read by the landing
|
|
# BUILD (data/site.ts) only. Avatar URL =
|
|
# https://avatar.${BASE_DOMAIN}/<mode>/<uuid>?size=N. e.g. headiso | head | fullbody.
|
|
# AVATAR_MODE → ServerCard online row + account preview (heads)
|
|
# ROSTER_AVATAR_MODE → Members grid (full body; default fullbodyiso)
|
|
AVATAR_MODE=headiso
|
|
ROSTER_AVATAR_MODE=fullbodyiso
|
|
|
|
# Drasl admin creds for the registered-players roster (/api/mcstatus/players).
|
|
# Consumed ONLY by the mc-status container (server-side login → sanitized
|
|
# [{uuid,name}] list); these NEVER reach the browser. Use a DEDICATED, rotatable
|
|
# admin account (Drasl has no read-only role) to limit blast radius. Leave blank
|
|
# to disable the roster (the Members grid then just shows its empty state).
|
|
DRASL_ADMIN_USERNAME=
|
|
DRASL_ADMIN_PASSWORD=
|
|
|
|
# caddy host-published port. The host's own nginx terminates TLS and
|
|
# reverse-proxies to caddy by Host header (nginx/ulicraft-caddy.conf.tmpl), so
|
|
# bind caddy to a high localhost-only port — only nginx should reach it.
|
|
CADDY_HTTP_PORT=8880
|
|
CADDY_HTTP_BIND=127.0.0.1
|
|
|
|
# distribution.${BASE_DOMAIN} static vhost. Absolute host path to the built
|
|
# static site — it lives in ANOTHER repo. Bind-mounted read-only into caddy.
|
|
DISTRIBUTION_WEB_ROOT=/home/oier/projects/mc-mods/ulicraft-group/ulicraft-distribution/dist
|
|
|
|
# ── upload.${BASE_DOMAIN} — Filestash player file share (write) ───────
|
|
# (files.${BASE_DOMAIN} = read-only, no-auth caddy browse of the same dir)
|
|
# Screenshots / schematics / maps. ONE shared login for all players, writable.
|
|
# Full design + gotchas: plan/20-files.md.
|
|
|
|
# Host path holding the shared files. Keep it OUTSIDE the repo — guest-writable
|
|
# data must not live in a git tree we `git pull` into.
|
|
FILES_DATA_DIR=/srv/ulicraft-files
|
|
|
|
# Literal mount flag for FILES_DATA_DIR: rw | ro. `ro` makes the share
|
|
# read-only at the KERNEL, regardless of what the Filestash UI thinks. It is a
|
|
# mount flag rather than a boolean because compose has no conditionals — the
|
|
# value is substituted straight into the volume string.
|
|
FILES_MOUNT_MODE=rw
|
|
|
|
# files.${BASE_DOMAIN} — the read-only, NO-AUTH browse of the same share.
|
|
# Per-host toggle, `on` | `off` (default on). It gates two layers, both reading
|
|
# this var: the caddy vhost (docker-compose.yml substitutes the value into the
|
|
# conf.d mount path — 62-files.on.caddy vs the 62-files.off.caddy stub) and the
|
|
# public nginx vhost (tooling/render-nginx.sh). `off` also drops the name from
|
|
# the port-80 redirect, so no cert is needed — drop `files` from LE_SUBDOMAINS
|
|
# too on that host. upload. (filestash, authenticated) is NOT affected.
|
|
# After changing it: tooling/render-nginx.sh --install
|
|
# docker compose up -d --force-recreate caddy
|
|
FILES_BROWSE=on
|
|
|
|
# Runtime uid:gid of the filestash process (docker `user:`). Leave at the image
|
|
# default 1000:1000 UNLESS you want files it writes into the share owned by a
|
|
# host account. To own the share as `oier` on oxps: set both to 1002 and run
|
|
# tooling/filestash-set-run-user.sh (it chowns the state volume + share to match
|
|
# — a bare override without the chown makes filestash fail to write its sqlite).
|
|
# The compose keeps group_add 1000 so any uid can still exec the image binary.
|
|
FILES_UID=1000
|
|
FILES_GID=1000
|
|
|
|
# Auth middleware (rendered into config.json by render-config.sh):
|
|
# htpasswd one shared user/pass — the only internet-safe value
|
|
# passthrough NO LOGIN. Anyone reaching the vhost is in.
|
|
# none alias of passthrough
|
|
# passthrough/none on a WRITABLE, internet-reachable share is an open upload
|
|
# relay (malware/phishing hosted on your domain, under your cert, with your
|
|
# bandwidth — and a domain blocklisting would take auth. and the apex down
|
|
# with it). Only set it once this host is unreachable from the internet.
|
|
FILES_AUTH=htpasswd
|
|
|
|
# The shared player credential (FILES_AUTH=htpasswd).
|
|
# FILES_PASSWORD_HASH: openssl passwd -6 '<password>'
|
|
# MUST be $6$ (sha512). The htpasswd plugin's bcrypt branch only accepts $2a$,
|
|
# so a $2y$ hash from `htpasswd -B` fails EVERY login, silently.
|
|
# render-config.sh rejects the wrong format rather than shipping it.
|
|
FILES_USER=uli
|
|
FILES_PASSWORD_HASH=
|
|
|
|
# Filestash /admin console password — bcrypt, and /admin IS publicly exposed, so
|
|
# use a 20+ char generated password. It is the only credential guarding config.
|
|
# docker run --rm caddy:alpine caddy hash-password --plaintext '<password>'
|
|
FILES_ADMIN_PASSWORD_HASH=
|
|
|
|
# Session key (openssl rand -hex 16). Must be non-empty: an empty secret_key
|
|
# makes filestash rewrite config.json at boot, which fails on the :ro mount.
|
|
FILES_SECRET_KEY=
|
|
|
|
# Unlocks the local storage backend, which filestash admin-gates: it refuses to
|
|
# init unless the connection params carry this secret. config.json supplies it
|
|
# server-side; players never see it. openssl rand -hex 24.
|
|
FILES_LOCAL_SECRET=
|
|
|
|
# ── generators.${BASE_DOMAIN} — offline Minecraft data pack generators ────
|
|
# A pinned fork of misode.github.io, built from source and served with its own
|
|
# data mirror so it works with ZERO internet at the LAN party. The app is 100%
|
|
# client-side, so the mirror is what makes it offline-capable — not the fact
|
|
# that the host has no WAN. Full design: plan/21-generators.md.
|
|
|
|
# Absolute host path to the ulicraft-generators checkout (the fork). This is the
|
|
# docker build context; the fork carries its own Dockerfile + Caddyfile.
|
|
GENERATORS_REPO=/home/oier/projects/mc-mods/ulicraft-group/ulicraft-generators
|
|
|
|
# Where tooling/sync-generators-data.sh puts the ~107M mcmeta + vanilla-mcdoc
|
|
# mirror. Gitignored, fetched deliberately with WAN, mounted read-only.
|
|
# Must be re-synced (with a matching MC_REF) if the pinned MC version moves.
|
|
GENERATORS_DATA_DIR=./generators-data
|
|
|
|
# Only needed if using CurseForge-exclusive mods:
|
|
# CF_API_KEY=your-curseforge-api-key
|
|
|
|
# ── Monitor (monitor.${BASE_DOMAIN}) — admin log stream ───────────────
|
|
# Read-only view of the Minecraft server's latest.log. ADMIN ONLY: the log
|
|
# carries player IPs, chat and admin commands verbatim, and nothing sanitizes
|
|
# it. Full design + gotchas: plan/23-monitor.md.
|
|
#
|
|
# Per-host toggle, `on` | `off` (default on), same two-layer gating as
|
|
# FILES_BROWSE: the caddy conf mount (docker-compose.yml substitutes the value
|
|
# into the path — 80-monitor.on.caddy vs the .off.caddy stub) and the public
|
|
# nginx vhost (tooling/render-nginx.sh). `off` also drops the name from the
|
|
# port-80 redirect, so no cert is needed — drop `monitor` from LE_SUBDOMAINS
|
|
# too on that host.
|
|
# After changing it: tooling/render-nginx.sh --install
|
|
# docker compose up -d --force-recreate caddy
|
|
MONITOR_ENABLED=on
|
|
|
|
# The ONLY credential guarding the log stream — caddy basic_auth. The monitor
|
|
# container has no auth of its own and publishes no port, so this is the whole
|
|
# access control.
|
|
MONITOR_USER=admin
|
|
#
|
|
# MUST be bcrypt, generated with:
|
|
# docker run --rm caddy:alpine caddy hash-password --plaintext 'your-password'
|
|
# (This is the OPPOSITE of FILES_PASSWORD_HASH, which must be $6$ and silently
|
|
# rejects bcrypt. Do not reuse a hash between the two.)
|
|
#
|
|
# MUST be SINGLE-QUOTED here. Docker Compose interpolates $-sequences in .env
|
|
# values, so an unquoted bcrypt hash is silently mangled — $2a$14$abcdefg…
|
|
# becomes $$2a$$14… with the $abcdefg… segment eaten as an unset variable, and
|
|
# every login fails with no hint as to why. Verified behaviour, not a guess.
|
|
MONITOR_PASSWORD_HASH='$2a$14$replace.me.with.a.real.bcrypt.hash'
|
|
|
|
# ── Let's Encrypt via OVH DNS-01 (tooling/issue-letsencrypt.sh) ───────
|
|
# Only needed to issue real TLS certs. DNS-01 needs no inbound ports.
|
|
LE_EMAIL=you@example.com
|
|
# space-separated subdomains; apex ${BASE_DOMAIN} is always included
|
|
LE_SUBDOMAINS=auth distribution www avatar status upload files generators monitor
|
|
# OVH API creds (DNS zone write). Create at https://eu.api.ovh.com/createToken/
|
|
# OVH_CK can be blank on first run — acme.sh prints an auth URL, then paste it.
|
|
OVH_END_POINT=ovh-eu
|
|
OVH_AK=
|
|
OVH_AS=
|
|
OVH_CK=
|
|
# LE_STAGING=1 # use the untrusted staging CA for dry runs
|