Every avatar rendered the default skin. NMSR fetched the player profile from
Drasl fine, but the profile embeds an absolute HTTPS skin URL (Drasl BaseURL is
https), and NMSR could not connect to auth.${BASE_DOMAIN}:443: the mcnet alias
resolves auth. -> caddy, which only listens on :80 internally. Logs showed
`fetch_texture_from_mojang … client error (Connect)` for the skin URL, so NMSR
fell back to the default skin.
Mirror what the minecraft service already does: pin auth.${BASE_DOMAIN} to the
host via extra_hosts so NMSR reaches the host nginx on :443 (real LE cert,
publicly trusted — no private CA needed), and switch the nmsr mojank endpoints
from http:// to https:// so the profile/texture scheme matches the embedded
skin URLs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
214 lines
9.0 KiB
YAML
214 lines
9.0 KiB
YAML
# ──────────────────────────────────────────────────────────────────
|
|
# Ulicraft LAN-party stack — Minecraft 1.21.1 NeoForge + Drasl auth
|
|
# ──────────────────────────────────────────────────────────────────
|
|
# Single, unified compose. One file holds the whole stack:
|
|
# drasl auth + skins (Yggdrasil), internal only
|
|
# minecraft the server (itzg/NEOFORGE), mods from ./server-mods volume
|
|
# mc-backup world backups every 6h via RCON
|
|
# caddy internal ingress — routes auth./apex/launcher/avatar.
|
|
# /distribution. by Host header (conf.d/*.caddy)
|
|
# nmsr skin/avatar renderer at avatar.${BASE_DOMAIN}
|
|
# uptime-kuma status monitoring at status.${BASE_DOMAIN}
|
|
#
|
|
# Bring up: docker compose up -d (render configs first: tooling/render-config.sh)
|
|
#
|
|
# DNS is handled OUTSIDE this repo — point every service name at the host.
|
|
# Production TLS terminates at the HOST's nginx in front of caddy
|
|
# (nginx/ulicraft-caddy.conf.tmpl + Let's Encrypt); caddy is published on a
|
|
# localhost-only port (CADDY_HTTP_BIND/CADDY_HTTP_PORT in .env). Containers reach
|
|
# the stack's own names internally via caddy's mcnet alias (auth.).
|
|
# ──────────────────────────────────────────────────────────────────
|
|
|
|
services:
|
|
drasl:
|
|
image: unmojang/drasl:latest
|
|
container_name: drasl
|
|
restart: unless-stopped
|
|
# Internal-only: no published host port. Reached via caddy at
|
|
# auth.${BASE_DOMAIN} (caddy holds the network alias on mcnet).
|
|
volumes:
|
|
- ./drasl/config:/etc/drasl:ro
|
|
- drasl_state:/var/lib/drasl
|
|
networks:
|
|
- mcnet
|
|
# No env vars — config is the rendered ./drasl/config/config.toml
|
|
# (from config.toml.tmpl via tooling/render-config.sh).
|
|
|
|
minecraft:
|
|
image: itzg/minecraft-server:java21
|
|
container_name: minecraft
|
|
restart: unless-stopped
|
|
stdin_open: true
|
|
tty: true
|
|
ports:
|
|
- "25565:25565"
|
|
- "24454:24454/udp" # Simple Voice Chat, if used
|
|
environment:
|
|
EULA: "TRUE"
|
|
TYPE: "NEOFORGE"
|
|
VERSION: "1.21.1"
|
|
# Verify NEOFORGE_VERSION against projects.neoforged.net before deploy.
|
|
# Must match the distribution's NeoForge (distribution.json → 21.1.233);
|
|
# several mods (ferritecore≥218, farmersdelight≥219, configured≥211) need it.
|
|
NEOFORGE_VERSION: "21.1.233" # pin a known-good build; update deliberately
|
|
|
|
# ── Memory / performance ────────────────────────────────────
|
|
INIT_MEMORY: "4G"
|
|
MAX_MEMORY: "10G" # 8 players + 100 mods comfortably
|
|
USE_AIKAR_FLAGS: "TRUE"
|
|
JVM_XX_OPTS: "-XX:+UseG1GC"
|
|
|
|
# ── Auth: offline mode + authlib-injector pointing at Drasl ─
|
|
ONLINE_MODE: "FALSE"
|
|
# Resolves over mcnet to caddy (alias auth.${BASE_DOMAIN}) -> drasl.
|
|
JVM_OPTS: "-javaagent:/extras/authlib-injector.jar=https://auth.${BASE_DOMAIN}/authlib-injector"
|
|
|
|
# ── Server config ───────────────────────────────────────────
|
|
DIFFICULTY: "normal"
|
|
MODE: "survival"
|
|
MOTD: "Uli LAN party"
|
|
MAX_PLAYERS: "10"
|
|
VIEW_DISTANCE: "10"
|
|
SIMULATION_DISTANCE: "8"
|
|
ENFORCE_SECURE_PROFILE: "FALSE" # required for authlib-injector on 1.21+
|
|
ALLOW_FLIGHT: "TRUE" # many tech mods need this
|
|
|
|
# ── Mod source: filtered subset of the distribution modset ──
|
|
# Jars are synced into ./server-mods by tooling/sync-server-mods.sh
|
|
# (both/server entries from mods-sides.json) and mounted at /mods below;
|
|
# itzg auto-syncs /mods → /data/mods. REMOVE_OLD_MODS makes that mirror
|
|
# authoritative so removed mods get pruned. See plan/04-mods.md.
|
|
REMOVE_OLD_MODS: "TRUE"
|
|
|
|
# ── RCON for remote admin ───────────────────────────────────
|
|
ENABLE_RCON: "TRUE"
|
|
RCON_PASSWORD: ${RCON_PASSWORD}
|
|
RCON_PORT: 25575
|
|
|
|
TZ: "Europe/Madrid"
|
|
volumes:
|
|
- mc_data:/data
|
|
- ./runtime:/extras:ro # authlib-injector.jar lives here
|
|
- ./server-mods:/mods:ro # itzg auto-syncs /mods → /data/mods
|
|
depends_on:
|
|
- drasl # authlib (auth.) must resolve at boot
|
|
# Containers don't inherit the host's /etc/hosts; the LAN resolver returns a
|
|
# dead address for the public names. Pin auth. to the host so HTTPS to it
|
|
# lands on the host's nginx (valid LE cert) -> caddy -> drasl.
|
|
extra_hosts:
|
|
- "auth.${BASE_DOMAIN}:host-gateway"
|
|
networks:
|
|
- mcnet
|
|
|
|
mc-backup:
|
|
image: itzg/mc-backup
|
|
container_name: mc-backup
|
|
restart: unless-stopped
|
|
environment:
|
|
BACKUP_INTERVAL: "6h"
|
|
RCON_HOST: "minecraft"
|
|
RCON_PASSWORD: ${RCON_PASSWORD}
|
|
PRUNE_BACKUPS_DAYS: "14"
|
|
TZ: "Europe/Madrid"
|
|
volumes:
|
|
- mc_data:/data:ro
|
|
- ./backups:/backups
|
|
depends_on:
|
|
- minecraft
|
|
networks:
|
|
- mcnet
|
|
|
|
# Internal ingress. Routes every vhost by Host header (conf.d/*.caddy):
|
|
# auth. -> drasl, apex -> landing + /launcher,
|
|
# avatar. -> nmsr, status. -> uptime-kuma, distribution. -> static site.
|
|
# Published on a localhost-only port; the host's nginx terminates TLS in
|
|
# front of it (nginx/ulicraft-caddy.conf.tmpl).
|
|
caddy:
|
|
image: caddy:alpine
|
|
container_name: caddy
|
|
restart: unless-stopped
|
|
ports:
|
|
- "${CADDY_HTTP_BIND:-127.0.0.1}:${CADDY_HTTP_PORT:-8880}:80"
|
|
environment:
|
|
BASE_DOMAIN: ${BASE_DOMAIN}
|
|
volumes:
|
|
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
|
|
- ./caddy/conf.d/00-core.caddy:/etc/caddy/conf.d/00-core.caddy:ro
|
|
- ./caddy/conf.d/10-static.caddy:/etc/caddy/conf.d/10-static.caddy:ro
|
|
- ./caddy/conf.d/30-distribution.caddy:/etc/caddy/conf.d/30-distribution.caddy:ro
|
|
- ./caddy/conf.d/40-avatar.caddy:/etc/caddy/conf.d/40-avatar.caddy:ro
|
|
- ./caddy/conf.d/50-status.caddy:/etc/caddy/conf.d/50-status.caddy:ro
|
|
- ./www:/srv/www:ro
|
|
- ./launcher:/srv/launcher:ro
|
|
# Static site from ANOTHER repo (absolute host path in .env).
|
|
- ${DISTRIBUTION_WEB_ROOT:?DISTRIBUTION_WEB_ROOT unset in .env}:/srv/distribution:ro
|
|
networks:
|
|
mcnet:
|
|
# Containers resolve the stack's own public names to caddy internally.
|
|
aliases:
|
|
- "auth.${BASE_DOMAIN}"
|
|
|
|
# Avatar/skin renderer — built from source (docker/nmsr/Dockerfile), pulls
|
|
# player profiles from Drasl over mcnet. caddy proxies avatar. -> nmsr:8080.
|
|
nmsr:
|
|
build:
|
|
context: ./docker/nmsr
|
|
image: ulicraft/nmsr:local
|
|
container_name: nmsr
|
|
restart: unless-stopped
|
|
volumes:
|
|
- ./nmsr/config.toml:/nmsr/config.toml:ro
|
|
# Drasl embeds absolute HTTPS skin URLs (BaseURL is https) in the profile.
|
|
# The mcnet alias resolves auth. -> caddy (http :80 only), so NMSR's https
|
|
# skin fetch to auth.:443 finds no listener and every avatar falls back to
|
|
# the default skin. Pin auth. to the host (same as the minecraft service) so
|
|
# NMSR reaches the host nginx on :443 with the real LE cert.
|
|
extra_hosts:
|
|
- "auth.${BASE_DOMAIN}:host-gateway"
|
|
networks:
|
|
- mcnet
|
|
|
|
# Self-hosted SLP→JSON pinger (built from docker/mc-status, mcutil wrapper).
|
|
# Emits mcstatus.io v2 JSON so the landing's ServerCard reads it unchanged.
|
|
# No published port — caddy proxies apex /api/mcstatus/* -> mc-status:8080.
|
|
# Pings the minecraft service internally over mcnet; result cached 30s.
|
|
mc-status:
|
|
build:
|
|
context: ./docker/mc-status
|
|
image: ulicraft/mc-status:local
|
|
container_name: mc-status
|
|
restart: unless-stopped
|
|
environment:
|
|
MC_STATUS_TARGET: "minecraft:25565"
|
|
MC_STATUS_CACHE_TTL: "30s"
|
|
# Registered-players roster (/api/mcstatus/players). mc-status logs into
|
|
# the Drasl admin API server-side and exposes a sanitized [{uuid,name}]
|
|
# list; these creds NEVER reach the browser. Use a dedicated, rotatable
|
|
# admin account. Isolated from the status pinger (own TTL/client).
|
|
DRASL_API_URL: "http://drasl:25585/drasl/api/v2"
|
|
DRASL_ADMIN_USERNAME: ${DRASL_ADMIN_USERNAME}
|
|
DRASL_ADMIN_PASSWORD: ${DRASL_ADMIN_PASSWORD}
|
|
MC_STATUS_ROSTER_TTL: "60s"
|
|
networks:
|
|
- mcnet
|
|
|
|
# Status monitoring + public status page. caddy proxies status. -> :3001.
|
|
# Joins mcnet so monitors probe the stack by internal service name.
|
|
uptime-kuma:
|
|
image: louislam/uptime-kuma:1
|
|
container_name: uptime-kuma
|
|
restart: unless-stopped
|
|
volumes:
|
|
- kuma_data:/app/data
|
|
networks:
|
|
- mcnet
|
|
|
|
volumes:
|
|
drasl_state:
|
|
mc_data:
|
|
kuma_data:
|
|
|
|
networks:
|
|
mcnet:
|
|
driver: bridge
|