# ────────────────────────────────────────────────────────────────── # 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=http://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