Files
ulicraft-server-v1/docker-compose.yml
Oier Bravo Urtasun de1ac2ee17 fix(nmsr): render real skins, not default — pin auth. to host over https
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>
2026-06-10 21:12:59 +02:00

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