Files
ulicraft-server-v1/docker-compose.yml
2026-06-09 22:13:31 +02:00

202 lines
8.1 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), installs mods via packwiz
# mc-backup world backups every 6h via RCON
# caddy internal ingress — routes auth./pack./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 aliases (auth./pack.).
# ──────────────────────────────────────────────────────────────────
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.
NEOFORGE_VERSION: "21.1.209" # 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: packwiz pack served by caddy (alias pack.) ──
PACKWIZ_URL: "http://pack.${BASE_DOMAIN}/pack.toml"
# ── 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
depends_on:
- drasl
- caddy # pack./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./pack. to the host so HTTPS to
# them lands on the host's nginx (valid LE cert) -> caddy -> service.
extra_hosts:
- "auth.${BASE_DOMAIN}:host-gateway"
- "pack.${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, pack. -> packwiz files, 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
- ./pack:/srv/pack:ro
# Custom (locally-hosted) mod jars live OUTSIDE ./pack so packwiz refresh
# doesn't index them as direct files. Served at pack.${BASE_DOMAIN}/custom/.
# Nested under the :ro /srv/pack mount — the mountpoint pack/custom/ must
# pre-exist on the host (tracked via pack/custom/.gitkeep) or Docker can't
# mkdir it in the read-only parent.
- ./custom:/srv/pack/custom: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}"
- "pack.${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
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"
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