Skip to content

Architecture

Understand the internal architecture of Lumo Server, from Docker build stages to background services.

The Dockerfile uses a 4-stage build process to create a minimal, optimized image.

Purpose: Compile the RCON CLI tool from source

Base image: alpine:3.20

Process:

  1. Install build tools (gcc, make, git)
  2. Clone mcrcon from GitHub
  3. Compile binary

Output: /tmp/mcrcon/mcrcon binary

Why: Provides RCON command-line access for automation and init-worlds.sh

Purpose: Build PlotSquared plugin from source

Base image: eclipse-temurin:21-jdk-alpine

Process:

  1. Clone PlotSquared repository
  2. Run Gradle build (./gradlew :plotsquared-bukkit:shadowJar)
  3. Extract compiled JAR

Output: /PlotSquared.jar

Why: PlotSquared releases may lag behind Paper versions. Building from source ensures compatibility.

Purpose: Download all plugins and datapacks in parallel

Base image: alpine:3.20

Process:

  1. Install curl, jq, parallel
  2. Download Paper server from PaperMC API
  3. Define download script for Modrinth
  4. Download 20+ plugins in parallel using GNU parallel
  5. Download Terralith datapack

Downloaded:

  • Paper server JAR
  • 20+ plugins from Modrinth
  • Vault and SmoothTimber from Spiget
  • Shopkeepers from GitHub
  • PlotSquared from builder stage
  • Terralith datapack

Parallelization: Uses -j8 (8 concurrent downloads) for speed

Purpose: Minimal runtime image with all components

Base image: eclipse-temurin:21-jre-alpine

Setup:

  1. Create minecraft user (UID 1000, GID 1000)
  2. Install runtime dependencies (bash, tini, python3, socat, netcat)
  3. Copy Paper server and plugins from downloader
  4. Copy mcrcon from builder
  5. Copy plugin configurations from repo
  6. Copy entrypoint and service scripts
  7. Set permissions

Exposed ports:

  • 25565/tcp - Minecraft
  • 25575/tcp - RCON
  • 8100/tcp - BlueMap
  • 24454/udp - Voice chat

Volume: /data - Persistent world and configuration storage

Entrypoint: /sbin/tini -- /entrypoint.sh

Why: Tini is a minimal init system that properly reaps zombie processes.

Responsibilities:

Terminal window
if [ "$EULA" != "true" ]; then
echo "EULA not accepted. Set EULA=true"
exit 1
fi

Ensures user has accepted Minecraft EULA.

Syncs plugin configs from /server/plugins/ to /data/plugins/:

  • Only copies if file doesn’t exist in /data
  • Preserves user customizations
  • Ensures defaults are present

Generates server.properties from environment variables:

  • Sets all configurable properties
  • Uses sensible defaults
  • Only regenerates if changed

Parses OPS and WHITELIST_USERS environment variables:

  • Generates ops.json
  • Generates whitelist.json
  • Supports op levels (:1, :2, :3, :4)

If /data/world doesn’t exist (first run):

  • Starts init-worlds.sh in background
  • Script waits for server to start
  • Creates 5 worlds via RCON
  • Installs Terralith datapack

If BACKUP_ENABLED=true:

  • Starts /backup.py in background
  • Runs on interval specified by BACKUP_INTERVAL

If ENABLE_AUTOPAUSE=true:

  • Starts socat proxy on port 25565 → 25566
  • Starts Minecraft on port 25566 (not 25565)
  • Starts /autopause.sh daemon
  • Starts /wake-listener.py daemon

Launches Paper server with Aikar’s flags:

Terminal window
java -Xms${MEMORY} -Xmx${MEMORY} \
-XX:+UseG1GC \
-XX:+ParallelRefProcEnabled \
-XX:MaxGCPauseMillis=200 \
# ... more flags ...
-jar paper.jar --nogui

Purpose: Create initial worlds on first startup

How it works:

  1. Waits up to 60s for server to start (polls for “Done” in logs)
  2. Connects via RCON
  3. Executes Multiverse commands to create 5 worlds
  4. Installs Terralith datapack to lumo_wilds
  5. Exits after completion

Runs: Once on first startup (when /data/world doesn’t exist)

Purpose: Automated backup scheduler

How it works:

  1. Runs in infinite loop with sleep interval
  2. Disables world saving via RCON: save-off
  3. Creates tar.gz backup of /data
  4. Enables world saving: save-on
  5. Applies retention policy (deletes old backups)
  6. Uploads to S3/rclone if configured
  7. Sends Discord notification if configured

Runs: Continuously if BACKUP_ENABLED=true

Purpose: Monitor activity and pause JVM when idle

How it works:

  1. Polls every AUTOPAUSE_POLL_INTERVAL seconds
  2. Checks player count via RCON
  3. Checks chunk loading (indicates plugin activity)
  4. If idle for AUTOPAUSE_TIMEOUT minutes:
    • Sends SIGSTOP to Java process
    • Switches socat proxy to wake-listener
  5. When activity detected:
    • Sends SIGCONT to Java process
    • Switches socat proxy to Minecraft server

Runs: Continuously if ENABLE_AUTOPAUSE=true

Purpose: Show “sleeping” message and wake server

How it works:

  1. Listens on port 25565 (via socat proxy when paused)
  2. When connection received:
    • Sends Minecraft protocol message: “Server is sleeping, waking up…”
    • Signals autopause.sh to wake server
    • Closes connection

Runs: Continuously if ENABLE_AUTOPAUSE=true

External:25565 → Container:25565 → Minecraft server
External:25565 → Container:25565 → socat proxy → Container:25566 → Minecraft server
External:25565 → Container:25565 → wake-listener.py (shows message, wakes server)
/server/ # Static files (built into image)
├── paper.jar # Paper server
├── plugins/ # Plugin JARs and default configs
└── datapacks/ # Terralith datapack
/data/ # Persistent volume mount
├── world/ # Default world
├── lumo_wilds/ # Custom worlds...
├── plugins/ # Live plugin configs
├── server.properties
├── ops.json
├── whitelist.json
└── logs/
/backups/ # Backup volume mount (optional)
└── *.tar.gz # Backup archives
/entrypoint.sh # Container startup script
/init-worlds.sh # World initialization
/autopause.sh # Autopause daemon
/wake-listener.py # Wake listener daemon
/backup.py # Backup scheduler
/restore.sh # Restore script

Critical volumes:

  • /data → Persistent storage for worlds, configs, player data
  • /backups → Backup storage (optional but recommended)

Config mount (optional):

  • ~/.config/rclone:/root/.config/rclone:ro → Rclone config for cloud backups

When running with all features:

tini (PID 1)
└─ entrypoint.sh (PID 7)
├─ java (Minecraft server) (PID 50)
├─ init-worlds.sh (PID 60) [exits after completion]
├─ python3 /backup.py (PID 70)
├─ autopause.sh (PID 80)
├─ wake-listener.py (PID 90)
└─ socat (port proxy) (PID 100)

Typical consumption:

ComponentCPUMemory
Minecraft server50-200%~4GB (configurable)
Python backup daemon<1%~50MB
Autopause script<1%~10MB
Wake listener<1%~30MB
Socat proxy<1%~5MB

Total: ~2-4 CPU cores, 4-6GB RAM

Built-in Docker health check:

HEALTHCHECK --interval=30s --timeout=10s --start-period=300s --retries=3 \
CMD nc -z localhost 25565 || exit 1

Check status:

Terminal window
docker inspect --format='{{json .State.Health}}' minecraft-server

GitHub Actions workflow (.github/workflows/build.yml):

  1. Build job:

    • Set up Docker Buildx
    • Build image with cache
    • Save image as artifact
  2. Test job:

    • Load image
    • Verify file structure
    • Start server
    • Wait for “Done” in logs
    • Test RCON connectivity
    • Verify 17+ plugins loaded
    • Test plugin functionality (BlueMap, PlotSquared, economy, shops)
  3. Push job:

    • Tag as latest, {MC_VERSION}, {sha}
    • Push to ghcr.io

Container security:

  • Runs as non-root user (minecraft, UID 1000)
  • Minimal Alpine base (smaller attack surface)
  • No SSH or unnecessary services
  • Read-only rclone mount

Network security:

  • Only necessary ports exposed
  • RCON password configurable (change from default!)
  • Online mode enabled by default (Mojang authentication)