From 06bff53e4db855ba9c6da02f0d482706dca8ef3b Mon Sep 17 00:00:00 2001 From: kuwacom Date: Fri, 6 Mar 2026 21:38:43 +0900 Subject: [PATCH] add: add Docker support for Dedicated Server add with entrypoint and build scripts --- .gitignore | 1 + README.md | 45 ++++++++++++++ docker-build-dedicated-server.sh | 1 + docker-compose.dedicated-server.yml | 27 ++++++++ docker/dedicated-server/Dockerfile | 33 ++++++++++ docker/dedicated-server/entrypoint.sh | 74 ++++++++++++++++++++++ start-dedicated-server.sh | 90 +++++++++++++++++++++++++++ 7 files changed, 271 insertions(+) create mode 100644 docker-build-dedicated-server.sh create mode 100644 docker-compose.dedicated-server.yml create mode 100644 docker/dedicated-server/Dockerfile create mode 100644 docker/dedicated-server/entrypoint.sh create mode 100644 start-dedicated-server.sh diff --git a/.gitignore b/.gitignore index 3bc14d738..e91764ca0 100644 --- a/.gitignore +++ b/.gitignore @@ -442,3 +442,4 @@ Minecraft.Client/Saves/ tmp*/ _server_asset_probe/ +server-data/ diff --git a/README.md b/README.md index 9fe589e0c..790e38e3a 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,51 @@ Minecraft.Client.exe -server -ip 0.0.0.0 -port 25565 The headless server also reads and writes `server.properties` in the working directory. If `-ip` / `-port` are omitted in `-server` mode, it falls back to `server-ip` / `server-port` from that file. Dedicated-server host options such as `trust-players`, `pvp`, `fire-spreads`, `tnt`, `difficulty`, `gamemode`, `spawn-animals`, and `spawn-npcs` are persisted there as well. +## Dedicated Server in Docker (Wine) + +This repository includes a lightweight Docker setup for running the Windows dedicated server under Wine. +**To build the image, a release build of `Minecraft.Server` is required.** + +1. Build the dedicated server (`Minecraft.Server`) first. +2. Start with Docker Compose (server runtime files are copied into the image at build time): + +```bash +docker compose -f docker-compose.dedicated-server.yml up -d +``` + +or use the helper script: + +```bash +./start-dedicated-server.sh +``` + +You can explicitly choose runtime: +**However, debug builds often fail to run because some debug-related libraries are missing.** + +```bash +./start-dedicated-server.sh release +./start-dedicated-server.sh debug +``` + +By default, build input is `x64/Minecraft.Server/Release`. Override it when needed (path must be inside this repository): + +```bash +MC_RUNTIME_DIR=x64/Minecraft.Server/Debug docker compose -f docker-compose.dedicated-server.yml up -d --build +``` + +Useful environment variables: +- `XVFB_DISPLAY` (default: `:99`) +- `XVFB_SCREEN` (default: `64x64x16`, tiny virtual display used by Wine) + +Fixed server runtime behavior in container: +- executable path: `/srv/mc/Minecraft.Server.exe` +- bind IP: `0.0.0.0` +- server port: `25565` + +Persistent files are bind-mounted to host: +- `./server-data/server.properties` -> `/srv/mc/server.properties` +- `./server-data/GameHDD` -> `/srv/mc/Windows64/GameHDD` + ## Controls (Keyboard & Mouse) - **Movement**: `W` `A` `S` `D` diff --git a/docker-build-dedicated-server.sh b/docker-build-dedicated-server.sh new file mode 100644 index 000000000..d84f7c61b --- /dev/null +++ b/docker-build-dedicated-server.sh @@ -0,0 +1 @@ +docker compose -f docker-compose.dedicated-server.yml build \ No newline at end of file diff --git a/docker-compose.dedicated-server.yml b/docker-compose.dedicated-server.yml new file mode 100644 index 000000000..e4ec3eae9 --- /dev/null +++ b/docker-compose.dedicated-server.yml @@ -0,0 +1,27 @@ +services: + minecraft-lce-dedicated-server: + build: + context: . + dockerfile: docker/dedicated-server/Dockerfile + args: + MC_RUNTIME_DIR: ${MC_RUNTIME_DIR:-x64/Minecraft.Server/Release} + container_name: minecraft-lce-dedicated-server + restart: unless-stopped + environment: + TZ: ${TZ:-Asia/Tokyo} + WINEARCH: win64 + WINEPREFIX: /var/opt/wineprefix64 + WINEDEBUG: -all + # minimum required virtual screen + XVFB_DISPLAY: ${XVFB_DISPLAY:-:99} + XVFB_SCREEN: ${XVFB_SCREEN:-64x64x16} + volumes: + # - wineprefix64:/var/opt/wineprefix64 + - ./server-data:/srv/persist + ports: + - "25565:25565/tcp" + - "25565:25565/udp" + stop_grace_period: 30s + +# volumes: +# wineprefix64: diff --git a/docker/dedicated-server/Dockerfile b/docker/dedicated-server/Dockerfile new file mode 100644 index 000000000..14760afdc --- /dev/null +++ b/docker/dedicated-server/Dockerfile @@ -0,0 +1,33 @@ +FROM debian:bookworm-slim + +ARG DEBIAN_FRONTEND=noninteractive +# basically, it only works with a Release build.(libs are not included in Debug build) +ARG MC_RUNTIME_DIR=x64/Minecraft.Server/Release + +RUN dpkg --add-architecture i386 \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + wine \ + wine64 \ + wine32:i386 \ + xvfb \ + tini \ + && rm -rf /var/lib/apt/lists/* + +ENV WINEARCH=win64 +ENV WINEPREFIX=/var/opt/wineprefix64 + +WORKDIR /srv/mc + +COPY ${MC_RUNTIME_DIR}/Minecraft.Server.exe /srv/mc/Minecraft.Server.exe +COPY ${MC_RUNTIME_DIR}/iggy_w64.dll /srv/mc/iggy_w64.dll +COPY ${MC_RUNTIME_DIR}/Common /srv/mc/Common +COPY ${MC_RUNTIME_DIR}/Windows64 /srv/mc/Windows64 +COPY docker/dedicated-server/entrypoint.sh /usr/local/bin/entrypoint.sh + +RUN chmod 0755 /usr/local/bin/entrypoint.sh \ + && mkdir -p /var/opt/wineprefix64 /srv/mc \ + && test -f /srv/mc/Minecraft.Server.exe + +ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/entrypoint.sh"] diff --git a/docker/dedicated-server/entrypoint.sh b/docker/dedicated-server/entrypoint.sh new file mode 100644 index 000000000..a36c16475 --- /dev/null +++ b/docker/dedicated-server/entrypoint.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +set -euo pipefail + +SERVER_DIR="/srv/mc" +SERVER_EXE="Minecraft.Server.exe" +# ip & port are fixed since they run inside the container +SERVER_PORT="25565" +SERVER_BIND_IP="0.0.0.0" + +PERSIST_DIR="/srv/persist" +WINE_CMD="" + +if [ ! -d "$SERVER_DIR" ]; then + echo "[error] Server directory not found: $SERVER_DIR" >&2 + exit 1 +fi + +cd "$SERVER_DIR" + +if [ ! -f "$SERVER_EXE" ]; then + echo "[error] ${SERVER_EXE} not found in ${SERVER_DIR}" >&2 + echo "[hint] Rebuild image with a valid MC_RUNTIME_DIR build arg that contains dedicated server runtime files." >&2 + exit 1 +fi + +# created because it is not implemented on the server side +mkdir -p "${PERSIST_DIR}/GameHDD" + +if [ ! -f "${PERSIST_DIR}/server.properties" ]; then + if [ -f "server.properties" ] && [ ! -L "server.properties" ]; then + cp -f "server.properties" "${PERSIST_DIR}/server.properties" + else + : > "${PERSIST_DIR}/server.properties" # the server writes the default configuration if the file is empty, so it works + fi +fi + +# differs from the structure, but it’s reorganized into a more manageable structure to the host side +if [ -e "Windows64/GameHDD" ] && [ ! -L "Windows64/GameHDD" ]; then + rm -rf "Windows64/GameHDD" +fi +ln -sfn "${PERSIST_DIR}/GameHDD" "Windows64/GameHDD" +ln -sfn "${PERSIST_DIR}/server.properties" "server.properties" + +# for compatibility with other images +if command -v wine64 >/dev/null 2>&1; then + WINE_CMD="wine64" +elif [ -x "/usr/lib/wine/wine64" ]; then + WINE_CMD="/usr/lib/wine/wine64" +elif command -v wine >/dev/null 2>&1; then + WINE_CMD="wine" +else + echo "[error] No Wine executable found (wine64/wine)." >&2 + exit 1 +fi + +if [ ! -d "${WINEPREFIX}" ] || [ -z "$(ls -A "${WINEPREFIX}" 2>/dev/null)" ]; then + mkdir -p "${WINEPREFIX}" +fi + +# in the current implementation, a virtual screen is required because the client-side logic is being called for compatibility +if [ -z "${DISPLAY:-}" ]; then + export DISPLAY="${XVFB_DISPLAY:-:99}" + XVFB_SCREEN="${XVFB_SCREEN:-64x64x16}" + Xvfb "${DISPLAY}" -nolisten tcp -screen 0 "${XVFB_SCREEN}" >/tmp/xvfb.log 2>&1 & + sleep 0.2 +fi + +args=( + -port "${SERVER_PORT}" + -bind "${SERVER_BIND_IP}" +) + +echo "[info] Starting ${SERVER_EXE} on ${SERVER_BIND_IP}:${SERVER_PORT}" +exec "${WINE_CMD}" "${SERVER_EXE}" "${args[@]}" diff --git a/start-dedicated-server.sh b/start-dedicated-server.sh new file mode 100644 index 000000000..d7d7d5012 --- /dev/null +++ b/start-dedicated-server.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +COMPOSE_FILE="${SCRIPT_DIR}/docker-compose.dedicated-server.yml" +SERVICE_NAME="minecraft-lce-dedicated-server" +PERSIST_DIR="${SCRIPT_DIR}/mc-server-data" + +if [[ ! -f "${COMPOSE_FILE}" ]]; then + echo "[error] docker-compose file not found: ${COMPOSE_FILE}" >&2 + exit 1 +fi + +if command -v docker >/dev/null 2>&1; then + COMPOSE_CMD=(docker compose) +elif command -v docker-compose >/dev/null 2>&1; then + COMPOSE_CMD=(docker-compose) +else + echo "[error] docker compose is not available." >&2 + exit 1 +fi + +# can choose between release and debug, but honestly there isn’t much use +if [[ "${#}" -gt 1 ]]; then + echo "Usage: $0 [release|debug|]" >&2 + exit 1 +fi + +mkdir -p "${PERSIST_DIR}/GameHDD" +if [[ ! -f "${PERSIST_DIR}/server.properties" ]]; then + : > "${PERSIST_DIR}/server.properties" +fi + +if [[ "${#}" -eq 1 ]]; then + case "${1}" in + release) + MC_RUNTIME_DIR_INPUT="x64/Minecraft.Server/Release" + ;; + debug) + MC_RUNTIME_DIR_INPUT="x64/Minecraft.Server/Debug" + ;; + *) + MC_RUNTIME_DIR_INPUT="${1}" + ;; + esac +else + if [[ -f "${SCRIPT_DIR}/x64/Minecraft.Server/Release/Minecraft.Server.exe" ]]; then + MC_RUNTIME_DIR_INPUT="x64/Minecraft.Server/Release" + else + MC_RUNTIME_DIR_INPUT="x64/Minecraft.Server/Debug" + fi +fi + +if [[ "${MC_RUNTIME_DIR_INPUT}" = /* ]]; then + if [[ "${MC_RUNTIME_DIR_INPUT}" != "${SCRIPT_DIR}"/* ]]; then + echo "[error] runtime-dir must be inside repository: ${MC_RUNTIME_DIR_INPUT}" >&2 + exit 1 + fi + MC_RUNTIME_DIR_REL="${MC_RUNTIME_DIR_INPUT#${SCRIPT_DIR}/}" +else + MC_RUNTIME_DIR_REL="${MC_RUNTIME_DIR_INPUT#./}" +fi + +if [[ -z "${MC_RUNTIME_DIR_REL}" || "${MC_RUNTIME_DIR_REL}" = "." ]]; then + echo "[error] runtime-dir is invalid: ${MC_RUNTIME_DIR_INPUT}" >&2 + exit 1 +fi + +if ! RUNTIME_ABS="$(cd "${SCRIPT_DIR}" && cd "${MC_RUNTIME_DIR_REL}" 2>/dev/null && pwd)"; then + echo "[error] runtime-dir not found: ${MC_RUNTIME_DIR_INPUT}" >&2 + exit 1 +fi + +if [[ "${RUNTIME_ABS}" != "${SCRIPT_DIR}"/* ]]; then + echo "[error] runtime-dir must resolve inside repository: ${MC_RUNTIME_DIR_INPUT}" >&2 + exit 1 +fi + +MC_RUNTIME_DIR_REL="${RUNTIME_ABS#${SCRIPT_DIR}/}" + +if [[ ! -f "${RUNTIME_ABS}/Minecraft.Server.exe" ]]; then + echo "[error] Minecraft.Server.exe not found in: ${RUNTIME_ABS}" >&2 + echo "[hint] Build dedicated server first, then retry." >&2 + exit 1 +fi + +echo "[info] Using runtime (build arg): ${MC_RUNTIME_DIR_REL}" +echo "[info] Persistent data dir: ${PERSIST_DIR}" +MC_RUNTIME_DIR="${MC_RUNTIME_DIR_REL}" "${COMPOSE_CMD[@]}" -f "${COMPOSE_FILE}" up -d --build "${SERVICE_NAME}" +echo "[info] Dedicated server started."