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>
110 lines
4.8 KiB
Bash
Executable File
110 lines
4.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# render-nginx.sh — render the nginx vhost template with the .env values.
|
|
#
|
|
# Substitutes $BASE_DOMAIN, $APP_DIR, $CADDY_HTTP_PORT into
|
|
# nginx/ulicraft-caddy.conf.tmpl (the host nginx that terminates TLS in front of
|
|
# caddy) and prints the result to stdout, or installs it with --install (writes
|
|
# to sites-available, symlinks sites-enabled, tests, reloads nginx).
|
|
#
|
|
# Usage:
|
|
# tooling/render-nginx.sh # render -> stdout
|
|
# tooling/render-nginx.sh --install # render + install + reload nginx
|
|
#
|
|
# .env vars used:
|
|
# BASE_DOMAIN required
|
|
# CADDY_HTTP_PORT localhost port nginx reverse-proxies to (caddy)
|
|
# optional:
|
|
# APP_DIR host path to this repo (cert files live under APP_DIR/certs)
|
|
# — defaults to the repo root (this checkout)
|
|
# SITE_NAME installed filename (default ulicraft.conf)
|
|
# FILES_BROWSE on|off (default on) — emit the files.${BASE_DOMAIN}
|
|
# read-only browse vhost. `off` omits the server block AND
|
|
# the name from the port-80 redirect, so the host serves
|
|
# nothing for it (no cert needed either). Same var gates the
|
|
# caddy side in docker-compose.yml.
|
|
# MONITOR_ENABLED on|off (default on) — emit the monitor.${BASE_DOMAIN}
|
|
# admin log-stream vhost. Same on/off semantics as
|
|
# FILES_BROWSE, same var gates the caddy side.
|
|
set -euo pipefail
|
|
|
|
cd "$(dirname "$0")/.." # repo root
|
|
|
|
log() { printf '\n\033[1;32m==>\033[0m %s\n' "$*"; }
|
|
warn() { printf '\033[1;33m!!\033[0m %s\n' "$*" >&2; }
|
|
die() { printf '\033[1;31mxx\033[0m %s\n' "$*" >&2; exit 1; }
|
|
|
|
[ -f .env ] || die ".env not found (copy .env.example and fill it)"
|
|
# shellcheck disable=SC1091
|
|
set -a; . ./.env; set +a
|
|
: "${BASE_DOMAIN:?BASE_DOMAIN unset in .env}"
|
|
|
|
TEMPLATE="${TEMPLATE:-nginx/ulicraft-caddy.conf.tmpl}"
|
|
[ -f "$TEMPLATE" ] || die "template not found: $TEMPLATE"
|
|
APP_DIR="${APP_DIR:-$PWD}"
|
|
SITE_NAME="${SITE_NAME:-ulicraft.conf}"
|
|
|
|
# CADDY_HTTP_PORT only matters for the caddy-front template; default 80 so the
|
|
# static template (which never references it) still renders cleanly.
|
|
export BASE_DOMAIN APP_DIR
|
|
export CADDY_HTTP_PORT="${CADDY_HTTP_PORT:-80}"
|
|
|
|
# files.${BASE_DOMAIN} — the no-auth read-only browse. Per-host toggle: on for
|
|
# the LAN-party box, off where the share must not be publicly listable. nginx
|
|
# has no conditionals, so the vhost is a separate template spliced in here (or
|
|
# replaced by a comment). Keep in sync with the caddy mount in docker-compose.yml,
|
|
# which reads the same var.
|
|
: "${FILES_BROWSE:=on}"
|
|
FILES_BROWSE_TEMPLATE="${FILES_BROWSE_TEMPLATE:-nginx/files-browse.server.tmpl}"
|
|
case "$FILES_BROWSE" in
|
|
on)
|
|
[ -f "$FILES_BROWSE_TEMPLATE" ] || die "template not found: $FILES_BROWSE_TEMPLATE"
|
|
FILES_BROWSE_NAME=" files.${BASE_DOMAIN}"
|
|
FILES_BROWSE_SERVER="$(envsubst '$BASE_DOMAIN $APP_DIR' < "$FILES_BROWSE_TEMPLATE")"
|
|
;;
|
|
off)
|
|
FILES_BROWSE_NAME=""
|
|
FILES_BROWSE_SERVER="# files.${BASE_DOMAIN} disabled on this host (FILES_BROWSE=off in .env)."
|
|
;;
|
|
*) die "FILES_BROWSE must be 'on' or 'off' (got '$FILES_BROWSE')" ;;
|
|
esac
|
|
export FILES_BROWSE_NAME FILES_BROWSE_SERVER
|
|
|
|
# monitor.${BASE_DOMAIN} — admin-only log stream (plan/23-monitor.md). Same
|
|
# splice-or-comment trick as FILES_BROWSE above, and the same var gates the
|
|
# caddy conf mount in docker-compose.yml. Turning it off here is what removes
|
|
# the public name entirely; the container itself publishes no port either way.
|
|
: "${MONITOR_ENABLED:=on}"
|
|
MONITOR_TEMPLATE="${MONITOR_TEMPLATE:-nginx/monitor.server.tmpl}"
|
|
case "$MONITOR_ENABLED" in
|
|
on)
|
|
[ -f "$MONITOR_TEMPLATE" ] || die "template not found: $MONITOR_TEMPLATE"
|
|
MONITOR_NAME=" monitor.${BASE_DOMAIN}"
|
|
MONITOR_SERVER="$(envsubst '$BASE_DOMAIN $APP_DIR' < "$MONITOR_TEMPLATE")"
|
|
;;
|
|
off)
|
|
MONITOR_NAME=""
|
|
MONITOR_SERVER="# monitor.${BASE_DOMAIN} disabled on this host (MONITOR_ENABLED=off in .env)."
|
|
;;
|
|
*) die "MONITOR_ENABLED must be 'on' or 'off' (got '$MONITOR_ENABLED')" ;;
|
|
esac
|
|
export MONITOR_NAME MONITOR_SERVER
|
|
|
|
rendered="$(envsubst '$BASE_DOMAIN $APP_DIR $CADDY_HTTP_PORT $FILES_BROWSE_NAME $FILES_BROWSE_SERVER $MONITOR_NAME $MONITOR_SERVER' < "$TEMPLATE")"
|
|
|
|
if [ "${1:-}" != "--install" ]; then
|
|
printf '%s\n' "$rendered"
|
|
warn "dry run — pipe to a file or pass --install to deploy"
|
|
exit 0
|
|
fi
|
|
|
|
command -v nginx >/dev/null 2>&1 || die "nginx not found on PATH"
|
|
avail="/etc/nginx/sites-available/$SITE_NAME"
|
|
enabled="/etc/nginx/sites-enabled/$SITE_NAME"
|
|
|
|
log "installing $TEMPLATE -> $avail (APP_DIR=$APP_DIR, CADDY_HTTP_PORT=$CADDY_HTTP_PORT, FILES_BROWSE=$FILES_BROWSE, MONITOR_ENABLED=$MONITOR_ENABLED)"
|
|
printf '%s\n' "$rendered" | sudo tee "$avail" >/dev/null
|
|
sudo ln -sf "$avail" "$enabled"
|
|
sudo nginx -t
|
|
sudo systemctl reload nginx
|
|
log "done — nginx reloaded"
|