diff --git a/.gitignore b/.gitignore index d8c21ac..243d51a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ -data -.env +/.env +/plugins.json +scratch screenlog.0 diff --git a/Makefile b/Makefile index 926ecee..5240d90 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,24 @@ -DOCKER_COMPOSE_BUILD = docker compose -f docker-compose.build.yml build +DOCKER_BUILDKIT ?= 1 +BUILDKIT_PROGRESS ?= auto +BUILD ?= basic + +DOCKER_COMPOSE_OPTS = BUILDX_GIT_LABELS=full \ + DOCKER_BUILDKIT=$(DOCKER_BUILDKIT) \ + BUILDKIT_PROGRESS=$(BUILDKIT_PROGRESS) +DOCKER_COMPOSE_BUILD = $(DOCKER_COMPOSE_OPTS) docker compose -f docker-compose.build.yml build DOCKER_COMPOSE_UP = docker compose up -d PRUNE_IMAGES = \ localhost/minecraft:latest \ + localhost/minecraft:latest-paper \ localhost/minecraft:latest-spigot \ localhost/minecraft:latest-craftbukkit \ localhost/minecraft-jre:latest \ localhost/minecraft-jdk:latest -.PHONY: all clean craftbukkit default install jdk jre spigot vanilla +.PHONY: all clean configure craftbukkit default install jdk jre spigot vanilla default: vanilla -all: vanilla spigot craftbukkit +all: vanilla paper spigot craftbukkit jre: $(DOCKER_COMPOSE_BUILD) minecraft-jre @@ -21,6 +29,9 @@ jdk: vanilla: jre $(DOCKER_COMPOSE_BUILD) minecraft-vanilla +paper: jre + $(DOCKER_COMPOSE_BUILD) minecraft-paper + spigot: jre jdk $(DOCKER_COMPOSE_BUILD) minecraft-spigot @@ -30,6 +41,54 @@ craftbukkit: jre jdk install: $(DOCKER_COMPOSE_UP) +# Macro to copy files if they don't already exist or are the same +define copy_build_files + set -eu; \ + BUILD=$(1) && \ + FILE=$(2) && \ + DIRECTORY=$(if $(3),$(3),builds) && \ + SRC_FILE="./$${DIRECTORY}/$${BUILD}/$${FILE}" && \ + DEST_FILE="./$${FILE}" && \ + TEMP_DEST_FILE="$$(mktemp)" && \ + if [ -f "$${SRC_FILE}" ]; then \ + if [ -f "$${DEST_FILE}" ]; then \ + cp "$${DEST_FILE}" "$${TEMP_DEST_FILE}"; \ + if [ "$${FILE}" = ".env" ] && grep -q 'EULA=false' "$${SRC_FILE}"; then \ + sed -i 's/EULA=true/EULA=false/' "$${TEMP_DEST_FILE}"; \ + fi; \ + if cmp -s "$${SRC_FILE}" "$${TEMP_DEST_FILE}"; then \ + echo "INFO: \"$${DEST_FILE}\" is up to date."; \ + rm "$${TEMP_DEST_FILE}"; \ + else \ + echo "ERROR: \"$${DEST_FILE}\" is different from \"$${SRC_FILE}\""; \ + diff -u "$${DEST_FILE}" "$${SRC_FILE}"; \ + rm "$${TEMP_DEST_FILE}"; \ + exit 1; \ + fi; \ + else \ + cp "$${SRC_FILE}" "$${DEST_FILE}"; \ + echo "INFO: \"$${SRC_FILE}\" copied to \"$${DEST_FILE}\""; \ + fi; \ + else \ + echo "ERROR: Source file \"$${SRC_FILE}\" does not exist."; \ + exit 1; \ + fi +endef + +configure: + @if [ -d "./builds/$(BUILD)" ]; then \ + echo "INFO: Configuring build $(BUILD) (./builds/$(BUILD))" && \ + $(call copy_build_files,$(BUILD),.env); \ + $(call copy_build_files,$(BUILD),plugins.json); \ + elif [ -d "./scratch/$(BUILD)" ]; then \ + echo "INFO: Configuring build $(BUILD) (./scratch/$(BUILD))" && \ + $(call copy_build_files,$(BUILD),.env,scratch); \ + $(call copy_build_files,$(BUILD),plugins.json,scratch); \ + else \ + echo "ERROR: Build directory for \"$(BUILD)\" not found"; \ + exit 1; \ + fi + clean: docker image rm $(PRUNE_IMAGES) || true docker builder prune -f diff --git a/builds/basic/.env b/builds/basic/.env new file mode 100644 index 0000000..68d11ca --- /dev/null +++ b/builds/basic/.env @@ -0,0 +1,28 @@ +# While setting EULA to false in 'builds' is potentially annoying, it is done +# intentionally to bring to attention the need to manually and explicitly agree +# to Minecraft's EULA before being able to run or build these images +EULA=false +VERSION=1.20.1 +RUN_TAG=${VERSION}-paper + +# Builds +VANILLA_TAG=${VERSION} +PAPER_TAG=${VERSION}-paper +SPIGOT_TAG=${VERSION}-spigot +CRAFTBUKKIT_TAG=${VERSION}-craftbukkit + +######################## +# Extra image settings # +######################## +# +# JAVA_VERSION=latest +# JRE_IMAGE=localhost/minecraft-jre +# JDK_IMAGE=localhost/minecraft-jdk +# JRE_TAG=latest +# JDK_TAG=latest +# +# VANILLA_IMAGE=localhost/minecraft +# PAPER_IMAGE=localhost/minecraft +# SPIGOT_IMAGE=localhost/minecraft +# CRAFTBUKKIT_IMAGE=localhost/minecraft +# diff --git a/builds/basic/plugins.json b/builds/basic/plugins.json new file mode 100644 index 0000000..28dc3fa --- /dev/null +++ b/builds/basic/plugins.json @@ -0,0 +1,39 @@ +{ + "plugins": [ + { + "name": "EssentialsX", + "version": "2.20.1", + "hash": "sha256:802ea30bda460ca4597e818925816933c123b08d8126a814fac28d03a61bf542", + "url": "https://github.com/EssentialsX/Essentials/releases/download/${version}/EssentialsX-${version}.jar", + "info_url": "https://essentialsx.net/wiki/Home.html" + }, + { + "name": "EssentialsXChat", + "version": "2.20.1", + "hash": "sha256:40aa5c20241ceb3007ebcb5cfbf19bf2c467b0c090ae50e70653ee87ab775ca6", + "url": "https://github.com/EssentialsX/Essentials/releases/download/${version}/EssentialsXChat-${version}.jar", + "info_url": "https://essentialsx.net/wiki/Module-Breakdown.html#essentialsx-chat" + }, + { + "name": "EssentialsXSpawn", + "version": "2.20.1", + "hash": "sha256:650d7c6a33865a02c5ffa4eb710def28e73d972c9aef85b2b1f4e71b9bd261a0", + "url": "https://github.com/EssentialsX/Essentials/releases/download/${version}/EssentialsXSpawn-${version}.jar", + "info_url": "https://essentialsx.net/wiki/Module-Breakdown.html#essentialsx-spawn" + }, + { + "name": "WorldEdit", + "version": "7.3.1", + "hash": "md5:c44cd1c16c3d84d8efc57bbf417606cb", + "url": "https://dev.bukkit.org/projects/worldedit/files/5326355/download", + "info_url": "https://dev.bukkit.org/projects/worldedit" + }, + { + "name": "WorldGuard", + "version": "7.0.9", + "hash": "md5:70d6418dd6a2e4492a9f18e5f9ecb10c", + "url": "https://dev.bukkit.org/projects/worldguard/files/4675318/download", + "info_url": "https://dev.bukkit.org/projects/worldguard" + } + ] +} diff --git a/docker-compose.build.yml b/docker-compose.build.yml index 4336043..01ce93a 100644 --- a/docker-compose.build.yml +++ b/docker-compose.build.yml @@ -29,12 +29,24 @@ services: environment: EULA: "${EULA:-false}" + minecraft-paper: + build: + context: . + dockerfile: ./dockerfiles/Dockerfile.paper + args: + VERSION: ${VERSION:-latest} + image: ${PAPER_IMAGE:-localhost/minecraft}:${PAPER_TAG:-latest-paper} + depends_on: + - minecraft-jre + environment: + EULA: "${EULA:-false}" + minecraft-spigot: build: context: . dockerfile: ./dockerfiles/Dockerfile.bukkit args: - VERSION: ${MINECRAFT_VERSION:-latest} + VERSION: ${VERSION:-latest} SPIGOT: 'true' image: ${SPIGOT_IMAGE:-localhost/minecraft}:${SPIGOT_TAG:-latest-spigot} depends_on: @@ -48,7 +60,7 @@ services: context: . dockerfile: ./dockerfiles/Dockerfile.bukkit args: - VERSION: ${MINECRAFT_VERSION:-latest} + VERSION: ${VERSION:-latest} SPIGOT: 'false' image: ${CRAFTBUKKIT_IMAGE:-localhost/minecraft}:${CRAFTBUKKIT_TAG:-latest-craftbukkit} depends_on: diff --git a/docker-compose.yml b/docker-compose.yml index 0fd4bff..3b4bc87 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: minecraft: - image: ${IMAGE:-localhost/minecraft}:${TAG:-latest} + image: ${RUN_IMAGE:-localhost/minecraft}:${RUN_TAG:-latest} ports: - "0.0.0.0:25565:25565" environment: diff --git a/dockerfiles/Dockerfile.bukkit b/dockerfiles/Dockerfile.bukkit index 06e4911..4954d29 100644 --- a/dockerfiles/Dockerfile.bukkit +++ b/dockerfiles/Dockerfile.bukkit @@ -7,6 +7,9 @@ ARG VERSION=latest # Defaults to building Spigot over CraftBukkit ARG SPIGOT=true +# Plugins prefix +ARG PREFIX="PLUGIN_" + # SpigotMC BuildTools URL ARG BASE_URL="https://hub.spigotmc.org/jenkins/job/BuildTools/" ARG ARTIFACT_PATH="lastSuccessfulBuild/artifact/target/BuildTools.jar" @@ -15,7 +18,7 @@ ARG ARTIFACT_PATH="lastSuccessfulBuild/artifact/target/BuildTools.jar" WORKDIR /build # Download and build Spigot using BuildTools -RUN set -ux && \ +RUN set -eux && \ # Grab latest version if not specified if [ "$VERSION" = "latest" ]; then \ VERSION="$( \ @@ -38,9 +41,59 @@ RUN set -ux && \ # Run BuildTools to build specified version java -jar BuildTools.jar --rev "$VERSION" --compile "$BUILD_TYPE" && \ ln -s \ - "$(echo $BUILD_TYPE | tr '[:upper:]' '[:lower:]')-${VERSION}.jar" \ + "$(echo "$BUILD_TYPE" | tr '[:upper:]' '[:lower:]')-${VERSION}.jar" \ "server.jar" +# Move into a directory just for storing plugins +WORKDIR /plugins + +# Copy in plugins +COPY ../plugins.json /plugins/ + +# Download defined plugins +RUN set -eux && \ + # Download defined plugins and check against hash + tmp_downloads="$(mktemp -d)" && \ + # Iterate over all plugins in plugins.json + jq -c '.plugins[]' plugins.json | while read -r PLUGIN; do \ + # Set variables from plugins.json + name=$(echo "$PLUGIN" | jq -r '.name') && \ + version=$(echo "$PLUGIN" | jq -r '.version') && \ + # Interpolate instances of '${version}' in the URL + url=$(echo "$PLUGIN" | jq -r '.url' | sed "s/\${version}/$version/g") && \ + hash=$(echo "$PLUGIN" | jq -r '.hash') && \ + info=$(echo "$PLUGIN" | jq -r '.info_url') && \ + # Extract hash type and value, e.g., `md5:6f5902ac237024bdd0c176cb93063dc4` + hash_type=$(echo "$hash" | cut -d':' -f1) && \ + hash_value=$(echo "$hash" | cut -d':' -f2-) && \ + # Download and compare the hash + tmp_file="${tmp_downloads}/${name}-${version}.jar" && \ + curl -s -L "$url" -o "${tmp_file}" && \ + case "$hash_type" in \ + sha256) \ + echo "${hash_value} $tmp_file" | sha256sum -c - || { \ + echo "SHA256 hash mismatch for ${name}-${version}.jar"; \ + rm -rf "$tmp_downloads"; \ + exit 1; \ + } \ + ;; \ + md5) \ + echo "${hash_value} $tmp_file" | md5sum -c - || { \ + echo "MD5 hash mismatch for ${name}-${version}.jar"; \ + rm -rf "$tmp_downloads"; \ + exit 1; \ + } \ + ;; \ + *) \ + echo "Unsupported hash type: ${hash_type}"; \ + rm -rf "$tmp_downloads"; \ + exit 1; \ + ;; \ + esac && \ + mv "$tmp_file" "${name}-${version}.jar"; \ + done && \ + rm -rf "$tmp_downloads" + # Use OpenJRE image for runtime FROM "${JRE_IMAGE:-localhost/minecraft-jre}":"${JRE_TAG:-latest}" as runtime @@ -51,6 +104,9 @@ WORKDIR /app # Copy the built bukkit jar from the build stage COPY --from=build /build/server.jar /app/server.jar +# Copy in plugins +COPY --from=build /plugins/ /app/plugins/ + # Generate initial settings RUN java -jar server.jar --initSettings --nogui diff --git a/dockerfiles/Dockerfile.java b/dockerfiles/Dockerfile.java index 5d563b1..fe4b239 100644 --- a/dockerfiles/Dockerfile.java +++ b/dockerfiles/Dockerfile.java @@ -11,7 +11,7 @@ RUN apt-get update && \ rm -rf /var/lib/apt/lists/* # Eclipse Adoptium DEB installer package -RUN set -ux && \ +RUN set -eux && \ # Download the Eclipse Adoptium GPG key curl -s https://packages.adoptium.net/artifactory/api/gpg/key/public \ | gpg --dearmor | tee /etc/apt/trusted.gpg.d/adoptium.gpg > /dev/null && \ @@ -21,7 +21,7 @@ RUN set -ux && \ | tee /etc/apt/sources.list.d/adoptium.list # Install Adoptium Temurin (OpenJDK/OpenJRE) -RUN set -ux && \ +RUN set -eux && \ # Grab latest LTS version if not specified if [ "$JAVA_VERSION" = "latest" ]; then \ JAVA_VERSION="$( \ diff --git a/dockerfiles/Dockerfile.paper b/dockerfiles/Dockerfile.paper new file mode 100644 index 0000000..6887780 --- /dev/null +++ b/dockerfiles/Dockerfile.paper @@ -0,0 +1,111 @@ +FROM "${JRE_IMAGE:-localhost/minecraft-jre}":"${JRE_TAG:-latest}" + +# Minecraft version to download +ARG VERSION=latest + +# Plugins prefix +ARG PREFIX="PLUGIN_" + +# PaperMC base URL +ARG BASE_URL="https://api.papermc.io/v2/projects/paper/versions/${VERSION}" + +# Download files +USER root +WORKDIR /app + +# Download and verify sha256sum for PaperMC server.jar +RUN set -eux && \ + # Grab latest version if not specified + if [ "$VERSION" = "latest" ]; then \ + VERSION="$( \ + curl -s https://api.papermc.io/v2/projects/paper | \ + jq -r '.versions[-1]' \ + )"; \ + fi && \ + # Get latest build for the specified version + BUILD="$( \ + curl -s "$BASE_URL" \ + | jq -r '.builds[-1]' \ + )" && \ + URL="${BASE_URL}/builds/${BUILD}/downloads/paper-${VERSION}-${BUILD}.jar" && \ + curl -s -o /tmp/server.jar "$URL" && \ + # Get SHA256 hash of server.jar and compare + SHA256="$(sha256sum /tmp/server.jar | awk '{print $1}')" && \ + EXPECTED="$( \ + curl -s "$BASE_URL/builds/$BUILD" \ + | jq -r '.downloads.application.sha256' \ + )" && \ + if [ ! "$SHA256" = "$EXPECTED" ]; then \ + echo "[ERROR] SHA256=\"$SHA256\" expected \"$EXPECTED\""; \ + exit 1; \ + fi && \ + mv /tmp/server.jar /app/server.jar + +# Move into a directory just for storing plugins +WORKDIR /app/plugins + +# Copy in plugins +COPY ../plugins.json /app/plugins + +# Download defined plugins +RUN set -eux && \ + # Download defined plugins and check against hash + tmp_downloads="$(mktemp -d)" && \ + # Iterate over all plugins in plugins.json + jq -c '.plugins[]' plugins.json | while read -r PLUGIN; do \ + # Set variables from plugins.json + name=$(echo "$PLUGIN" | jq -r '.name') && \ + version=$(echo "$PLUGIN" | jq -r '.version') && \ + # Interpolate instances of '${version}' in the URL + url=$(echo "$PLUGIN" | jq -r '.url' | sed "s/\${version}/$version/g") && \ + hash=$(echo "$PLUGIN" | jq -r '.hash') && \ + info=$(echo "$PLUGIN" | jq -r '.info_url') && \ + # Extract hash type and value, e.g., `md5:6f5902ac237024bdd0c176cb93063dc4` + hash_type=$(echo "$hash" | cut -d':' -f1) && \ + hash_value=$(echo "$hash" | cut -d':' -f2-) && \ + # Download and compare the hash + tmp_file="${tmp_downloads}/${name}-${version}.jar" && \ + curl -s -L "$url" -o "${tmp_file}" && \ + case "$hash_type" in \ + sha256) \ + echo "${hash_value} $tmp_file" | sha256sum -c - || { \ + echo "SHA256 hash mismatch for ${name}-${version}.jar"; \ + rm -rf "$tmp_downloads"; \ + exit 1; \ + } \ + ;; \ + md5) \ + echo "${hash_value} $tmp_file" | md5sum -c - || { \ + echo "MD5 hash mismatch for ${name}-${version}.jar"; \ + rm -rf "$tmp_downloads"; \ + exit 1; \ + } \ + ;; \ + *) \ + echo "Unsupported hash type: ${hash_type}"; \ + rm -rf "$tmp_downloads"; \ + exit 1; \ + ;; \ + esac && \ + mv "$tmp_file" "${name}-${version}.jar"; \ + done && \ + rm -rf "$tmp_downloads" && \ + chown minecraft:minecraft /app/plugins/ + +# Generate initial settings +USER minecraft +WORKDIR /app +RUN java -jar server.jar --initSettings --nogui + +# Back to root to copy the entrypoint in +USER root +COPY ../entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +# Run app as minecraft user +USER minecraft +WORKDIR /app + +# Expose port and run entrypoint script +EXPOSE 25565 +ENTRYPOINT ["entrypoint.sh"] diff --git a/dockerfiles/Dockerfile.vanilla b/dockerfiles/Dockerfile.vanilla index da00840..1a188ae 100644 --- a/dockerfiles/Dockerfile.vanilla +++ b/dockerfiles/Dockerfile.vanilla @@ -8,7 +8,7 @@ USER minecraft WORKDIR /app # Download and verify sha1sum for server.jar -RUN set -ux && \ +RUN set -eux && \ # Grab latest version if not specified if [ "$VERSION" = "latest" ]; then \ VERSION="$( \ diff --git a/scratch/.gitignore b/scratch/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/scratch/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore