Compare commits

...

8 Commits

Author SHA1 Message Date
dab509a234 testing 2024-05-28 03:55:52 -04:00
b1e6604093
Default bStats configuration to false (opt-in)
- Pre-create bStats config with `enabled: false` by default
- Apply changes to CraftBukkit, Spigot, and Paper Dockerfiles
- Consistent with existing implementation in Velocity proxy image
2024-05-26 23:07:25 -04:00
695fae915b
Handle diff nonzero return in set_eula
- Suppress nonzero return code from diff
- Remove false fix `return 0`
2024-05-26 21:47:55 -04:00
c4e3fc042c
Prevent error by adding return 0 to set_eula() 2024-05-26 01:41:57 -04:00
f83c8499e6
Implement basic Velocity proxy
- Split entrypoint into functions for Minecraft and Velocity
- Implement Velocity Dockerfile based on JRE image
- Add velocity default build to ./builds
- Change error to warning for missing plugins.json build file
2024-05-25 23:56:43 -04:00
3a943e9397
Rewrite README for clarity and accuracy
- Simplify build steps in README
- Add `make configure` for custom builds
- Recommend using `make paper` for PaperMC server setup
- Advise tagging images for private repository
- Highlight the importance of managing your own compose files
2024-05-24 20:52:03 -04:00
4ce50becd2
Add build configuration system and paper image
- Add 'make paper' target to build a Paper image with plugins based on JRE
- Introduce dynamic .env and plugins.json configuration using Makefile
- Enable users to extend builds by managing their own directories in scratch/
- Implement copy_build_files macro for reproducible build management
- Add BUILDKIT_PROGRESS and DOCKER_BUILDKIT as configurable make vars
2024-05-24 03:15:54 -04:00
813b9de410
Create specific Dockerfiles for Minecraft setups
- Decompose Dockerfile into modular components
- Establish building JRE and JDK images from Docker.java
- Establish Dockerfile.vanilla for vanilla server setup
- Establish Dockerfile.bukkit for Spigot and CraftBukkit
- Establish a comprehensive Makefile for building and managing
- Add docker-compose.build.yml for streamlined image construction
- Use docker-compose.yml to test built images without a volume
2024-05-17 23:58:46 -04:00
16 changed files with 1028 additions and 210 deletions

5
.gitignore vendored
View File

@ -1,3 +1,4 @@
data
.env
/.env
/plugins.json
scratch
screenlog.0

View File

@ -1,85 +0,0 @@
FROM debian:stable-slim
ARG VERSION=latest
ARG JAVA_VERSION=latest
ENV DEBIAN_FRONTEND=noninteractive
# Create minecraft user
RUN groupadd -g 1000 minecraft && \
useradd -m -u 1000 -g 1000 -d /app minecraft
# Install scripting dependencies
RUN apt-get update && \
apt-get install -y curl gpg jq procps screen strace && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Eclipse Adoptium DEB installer package
RUN set -ux && \
# 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 && \
# Configure the Eclipse Adoptium apt repository
VERSION_CODENAME="$(awk -F= '/^VERSION_CODENAME/{print $2}' /etc/os-release)" && \
echo "deb https://packages.adoptium.net/artifactory/deb $VERSION_CODENAME main" \
| tee /etc/apt/sources.list.d/adoptium.list
# Install Adoptium Temurin (OpenJDK Distribution)
RUN set -ux && \
# Grab latest LTS version if not specified
if [ "$JAVA_VERSION" = "latest" ]; then \
JAVA_VERSION="$( \
curl -s https://api.adoptium.net/v3/info/available_releases \
| jq '.most_recent_lts' \
)"; \
fi && \
# Install the Temurin version
apt-get update && \
apt-get install -y "temurin-${JAVA_VERSION}-jre" && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Download files and run as user
USER minecraft
WORKDIR /app
# Download and verify sha1sum for server.jar
RUN set -ux && \
# Grab latest version if not specified
if [ "$VERSION" = "latest" ]; then \
VERSION="$( \
curl -s https://launchermeta.mojang.com/mc/game/version_manifest.json \
| jq -r '.latest.release' \
)"; \
fi && \
# Get server.jar based on $VERSION
curl -s https://launchermeta.mojang.com/mc/game/version_manifest.json \
| jq -r --arg id "$VERSION" '.versions[] | select(.id == $id) | .url' \
| xargs curl -s | jq -r '.downloads.server' | tee "/tmp/dl.json" \
| jq -r '.url' | xargs curl -s -o server.jar && \
# Get SHA1 hash of server.jar and compare
SHA1="$(sha1sum server.jar | awk '{print $1}')" && \
EXPECTED="$(jq -r .sha1 /tmp/dl.json)"; rm /tmp/dl.json && \
if [ ! "$SHA1" = "$EXPECTED" ]; then \
echo "[ERROR] SHA1=\"$SHA1\" expected \"$EXPECTED\""; \
exit 1; \
fi
# Generate initial settings
RUN java -jar server.jar --initSettings --nogui
# Back to root to copy the entrypoint in
USER root
WORKDIR /app
# Copy in entrypoint script
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"]

104
Makefile
View File

@ -1,17 +1,97 @@
CONTAINER = minecraft-minecraft-1
DOCKER_BUILDKIT ?= 1
BUILDKIT_PROGRESS ?= auto
BUILD ?= basic
.PHONY: default build clean install
default: build
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
build:
docker compose build
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 \
localhost/velocity:latest
.PHONY: all clean configure craftbukkit default install jdk jre spigot vanilla
default: vanilla
all: vanilla paper spigot craftbukkit velocity
jre:
$(DOCKER_COMPOSE_BUILD) minecraft-jre
jdk:
$(DOCKER_COMPOSE_BUILD) minecraft-jdk
vanilla: jre
$(DOCKER_COMPOSE_BUILD) minecraft-vanilla
paper: jre
$(DOCKER_COMPOSE_BUILD) minecraft-paper
spigot: jre jdk
$(DOCKER_COMPOSE_BUILD) minecraft-spigot
craftbukkit: jre jdk
$(DOCKER_COMPOSE_BUILD) minecraft-craftbukkit
velocity: jre
$(DOCKER_COMPOSE_BUILD) minecraft-velocity
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 "[WARN]: Source file \"$${SRC_FILE}\" does not exist."; \
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:
rm -f screenlog.0
docker compose down --rmi all
docker image rm $(PRUNE_IMAGES) || true
docker builder prune -f
install: build
touch screenlog.0
docker compose up -d && \
docker logs -f $(CONTAINER)

View File

@ -1,42 +1,79 @@
# Minecraft Docker Image
This Dockerfile sets up a Minecraft server based on the `debian-slim` image.
# Minecraft Container Image
## Quick Start
By running the following and building this image, you are agreeing to
[Minecraft's EULA](https://www.minecraft.net/en-us/eula):
```
echo "EULA=true" > .env
```
Assume a clean repository (i.e., without .env and plugins.json in the top
directory), and Docker cache. You can use `make clean` to clear specific
default images and prune the unused build cache. However, you'll still need to
inspect all containers and images to ensure you've removed them all.
Build the image using the Makefile:
```
make build
```
- `make clean` only removes certain images and prunes the builder cache.
### Steps
1. **Configure your build:**
```
make configure
```
This defaults to `make configure BUILD=basic`, but if you have directories
in `./scratch`, you can specify those build names here. Repository-included
builds are in`./builds`, but it's advised to copy `./builds/basic` or
whichever build configuration you are basing off and copy it into
`./scratch/X` to control your settings apart from the repository. This
separation allows you to manage your configurations independently and avoid
overwriting repository defaults.
2. **Build the PaperMC server:**
```
make paper
```
This builds the PaperMC server, which is likely what you want unless you
prefer a 100% vanilla server experience. PaperMC is recommended for its
performance benefits and support for Bukkit API server mods. Both
EssentialsX and WorldGuard suggest using Paper for better performance and
stability.
3. **(Optional) Install for testing:**
Optionally, build _and_ run to test it:
```
make install
```
This runs `docker compose up -d` and brings up a `minecraft-minecraft-1`
network/container compose stack using the `.env` and `plugins.json` in the
root of the repository. It includes settings for image overrides, the
`EULA` agreement, and a `DEBUG` option for the custom `entrypoint.sh`
script, Java options (defaulting to `-Xms1G -Xmx2G`), and the ability to
set any `server.properties` file entry using the `SETTINGS_` prefix in the
compose file. The purpose of `make install` is for testing only, and it is
advised not to rely on it for managing an actual server deployment. You
will likely want to add other settings not specified in the
`docker-compose.yml` and manage your own compose files.
Feel free to use `docker compose` directly to build and test:
## Additional Notes
### Image Management
All images are tagged with `localhost/minecraft`, etc. It's acceptable not to
override these default image names and just tag your own versions after
building the `localhost` images. The images will include git hash information
for extra traceability.
After using the quick start, you'll get something like this:
```
docker compose build
docker compose up -d
docker logs -f minecraft-minecraft-1
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
localhost/minecraft 1.20.1-paper 814edda474c4 9 seconds ago 568MB
localhost/minecraft-jre latest 50350d8d3947 30 seconds ago 379MB
```
## Copyright and License
Copyright (C) 2024 Kris Lamoureux
It is advisable to tag your own images and push them to a private container
repository, as you'll want to avoid pushing these images to a public DockerHub
repository due to the Minecraft EULA with typical proprietary software
non-redistribution rules.
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
```
docker tag localhost/minecraft:1.20.1-paper example.org/minecraft:1.20.1-paper
docker push example.org/minecraft:1.20.1-paper
```
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation, version 3 of the License.
This program is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
this program. If not, see <https://www.gnu.org/licenses/>.
## License
This project is licensed under the GPLv3 License. See the LICENSE file for details.

28
builds/basic/.env Normal file
View File

@ -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
#

39
builds/basic/plugins.json Normal file
View File

@ -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"
}
]
}

18
builds/velocity/.env Normal file
View File

@ -0,0 +1,18 @@
# No EULA requirement to run Velocity
VERSION=3.3.0-SNAPSHOT
VELOCITY_TAG=${VERSION}
# Run
RUN_IMAGE=${VELOCITY_IMAGE:-localhost/velocity}
RUN_TAG=${VERSION}
########################
# Extra image settings #
########################
#
# JAVA_VERSION=latest
# JRE_IMAGE=localhost/minecraft-jre
# JRE_TAG=latest
#
# VELOCITY_IMAGE=localhost/velocity
#

82
docker-compose.build.yml Normal file
View File

@ -0,0 +1,82 @@
services:
minecraft-jre:
build:
context: .
dockerfile: ./dockerfiles/Dockerfile.java
args:
JAVA_VERSION: ${JAVA_VERSION:-latest}
JAVA_RUNTIME: 'true'
image: ${JRE_IMAGE:-localhost/minecraft-jre}:${JRE_TAG:-latest}
minecraft-jdk:
build:
context: .
dockerfile: ./dockerfiles/Dockerfile.java
args:
JAVA_VERSION: ${JAVA_VERSION:-latest}
JAVA_RUNTIME: 'false'
image: ${JDK_IMAGE:-localhost/minecraft-jdk}:${JDK_TAG:-latest}
minecraft-vanilla:
build:
context: .
dockerfile: ./dockerfiles/Dockerfile.vanilla
args:
VERSION: ${VERSION:-latest}
image: ${VANILLA_IMAGE:-localhost/minecraft}:${VANILLA_TAG:-latest}
depends_on:
- minecraft-jre
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: ${VERSION:-latest}
SPIGOT: 'true'
image: ${SPIGOT_IMAGE:-localhost/minecraft}:${SPIGOT_TAG:-latest-spigot}
depends_on:
- minecraft-jre
- minecraft-jdk
environment:
EULA: "${EULA:-false}"
minecraft-craftbukkit:
build:
context: .
dockerfile: ./dockerfiles/Dockerfile.bukkit
args:
VERSION: ${VERSION:-latest}
SPIGOT: 'false'
image: ${CRAFTBUKKIT_IMAGE:-localhost/minecraft}:${CRAFTBUKKIT_TAG:-latest-craftbukkit}
depends_on:
- minecraft-jre
- minecraft-jdk
environment:
EULA: "${EULA:-false}"
minecraft-velocity:
build:
context: .
dockerfile: ./dockerfiles/Dockerfile.velocity
args:
VERSION: ${VELOCITY_VERSION:-latest}
image: ${VELOCITY_IMAGE:-localhost/velocity}:${VELOCITY_TAG:-latest}
depends_on:
- minecraft-jre
environment:
EULA: "${EULA:-false}"

View File

@ -1,12 +1,6 @@
services:
minecraft:
build:
context: .
dockerfile: Dockerfile
args:
VERSION: ${VERSION:-latest}
JAVA_VERSION: ${JAVA_VERSION:-latest}
image: ${IMAGE:-minecraft}:${TAG:-latest}
image: ${RUN_IMAGE:-localhost/minecraft}:${RUN_TAG:-latest}
ports:
- "0.0.0.0:25565:25565"
environment:

View File

@ -0,0 +1,136 @@
# Build from OpenJDK image
FROM "${JDK_IMAGE:-localhost/minecraft-jdk}":"${JDK_TAG:-latest}" as build
# Minecraft version
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"
# Consider turning bStats (https://bStats.org) on but I'm turning it off by
# default because it collects information
ARG BSTATS_ENABLED=false
# Build in common container location
WORKDIR /build
# Download and build Spigot using BuildTools
RUN set -eux && \
# Grab latest version if not specified
if [ "$VERSION" = "latest" ]; then \
VERSION="$( \
curl -s https://launchermeta.mojang.com/mc/game/version_manifest.json \
| jq -r '.latest.release' \
)"; \
fi && \
# Download BuildTools.jar
curl -s -o BuildTools.jar "${BASE_URL}${ARTIFACT_PATH}" && \
# Build Craftbukkit if SPIGOT is false
case "$SPIGOT" in \
true) \
BUILD_TYPE='SPIGOT' ;; \
false) \
BUILD_TYPE='CRAFTBUKKIT' ;; \
*) \
echo "[ERROR]: Invalid value for SPIGOT. Set to 'true' or 'false'"; \
exit 1 ;; \
esac && \
# 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" \
"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
# Run as Minecraft user
USER minecraft
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 && \
# Disable bStats by default
if [ "$BSTATS_ENABLED" = "false" ]; then \
mkdir -p /app/plugins/bStats/ && \
echo "enabled: false" > /app/plugins/bStats/config.yml; \
fi
# Back to root to copy the entrypoint in
USER root
WORKDIR /app
# Copy in entrypoint script
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"]

View File

@ -0,0 +1,60 @@
FROM debian:stable-slim
ARG JAVA_RUNTIME
ARG JAVA_VERSION=latest
ENV DEBIAN_FRONTEND=noninteractive
# Install scripting dependencies
RUN apt-get update && \
apt-get install -y curl git gpg jq && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Eclipse Adoptium DEB installer package
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 && \
# Configure the Eclipse Adoptium APT repository
VERSION_CODENAME="$(awk -F= '/^VERSION_CODENAME/{print $2}' /etc/os-release)" && \
echo "deb https://packages.adoptium.net/artifactory/deb $VERSION_CODENAME main" \
| tee /etc/apt/sources.list.d/adoptium.list
# Install Adoptium Temurin (OpenJDK/OpenJRE)
RUN set -eux && \
# Grab latest LTS version if not specified
if [ "$JAVA_VERSION" = "latest" ]; then \
JAVA_VERSION="$( \
curl -s https://api.adoptium.net/v3/info/available_releases \
| jq '.most_recent_lts' \
)"; \
fi && \
# Install OpenJDK if JAVA_RUNTIME is false
case "$JAVA_RUNTIME" in \
true) \
JAVA_TYPE='jre' ;; \
false) \
JAVA_TYPE='jdk' ;; \
*) \
echo "[ERROR]: Invalid value for JAVA_RUNTIME. Set to 'true' or 'false'"; \
exit 1 ;; \
esac && \
# Install the Temurin version
apt-get update && \
apt-get install -y "temurin-$JAVA_VERSION-$JAVA_TYPE" && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Create minecraft user for runtime
RUN if [ "$JAVA_RUNTIME" = "true" ]; then \
groupadd -g 1000 minecraft && \
useradd -m -u 1000 -g 1000 -d /app minecraft; \
fi
# Install additional runtime dependencies
RUN if [ "$JAVA_RUNTIME" = "true" ]; then \
apt-get update && \
apt-get install -y procps screen strace && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*; \
fi

View File

@ -0,0 +1,120 @@
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}"
# Consider turning bStats (https://bStats.org) on but I'm turning it off by
# default because it collects information
ARG BSTATS_ENABLED=false
# 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 && \
# Disable bStats by default
if [ "$BSTATS_ENABLED" = "false" ]; then \
mkdir -p /app/plugins/bStats/ && \
echo "enabled: false" > /app/plugins/bStats/config.yml; \
fi
# 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"]

View File

@ -0,0 +1,49 @@
FROM "${JRE_IMAGE:-localhost/minecraft-jre}":"${JRE_TAG:-latest}"
# Minecraft version to download
ARG VERSION=latest
# Download files and run as user
USER minecraft
WORKDIR /app
# Download and verify sha1sum for server.jar
RUN set -eux && \
# Grab latest version if not specified
if [ "$VERSION" = "latest" ]; then \
VERSION="$( \
curl -s https://launchermeta.mojang.com/mc/game/version_manifest.json \
| jq -r '.latest.release' \
)"; \
fi && \
# Get server.jar based on $VERSION
curl -s https://launchermeta.mojang.com/mc/game/version_manifest.json \
| jq -r --arg id "$VERSION" '.versions[] | select(.id == $id) | .url' \
| xargs curl -s | jq -r '.downloads.server' | tee "/tmp/dl.json" \
| jq -r '.url' | xargs curl -s -o server.jar && \
# Get SHA1 hash of server.jar and compare
SHA1="$(sha1sum server.jar | awk '{print $1}')" && \
EXPECTED="$(jq -r .sha1 /tmp/dl.json)"; rm /tmp/dl.json && \
if [ ! "$SHA1" = "$EXPECTED" ]; then \
echo "[ERROR]: SHA1=\"$SHA1\" expected \"$EXPECTED\""; \
exit 1; \
fi
# Generate initial settings
RUN java -jar server.jar --initSettings --nogui
# Back to root to copy the entrypoint in
USER root
WORKDIR /app
# Copy in entrypoint script
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"]

View File

@ -0,0 +1,90 @@
FROM "${JRE_IMAGE:-localhost/minecraft-jre}":"${JRE_TAG:-latest}"
# Server version to download
ARG VERSION=latest
# PaperMC base URL
ARG BASE_URL="https://api.papermc.io/v2/projects/velocity/versions"
# Consider turning bStats (https://bStats.org) on but I'm turning it off by
# default because it collects information
ARG BSTATS_ENABLED=false
# For the entrypoint.sh script
ENV VELOCITY=true
# Download files
USER root
WORKDIR /app
# Install expect
RUN apt-get update && \
apt-get install -y expect && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Download and verify sha256sum for Velocity
RUN set -eux && \
# Grab latest version if not specified
if [ "$VERSION" = "latest" ]; then \
VERSION="$( \
curl -s https://api.papermc.io/v2/projects/velocity | \
jq -r '.versions[-1]' \
)"; \
fi && \
# Get latest build for the specified version
BUILD="$( \
curl -s "${BASE_URL}/${VERSION}" \
| jq -r '.builds[-1]' \
)" && \
URL="${BASE_URL}/${VERSION}/builds/${BUILD}/downloads/velocity-${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/$VERSION/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/velocity.jar
# Generate files as minecraft user
USER minecraft
WORKDIR /app
# Start server to generate initial files
RUN set -ux; \
expect -c "\
set timeout -1; \
spawn /usr/bin/java -Xms1G -Xmx1G -XX:+UseG1GC -XX:G1HeapRegionSize=4M \
-XX:+UnlockExperimentalVMOptions -XX:+ParallelRefProcEnabled \
-XX:+AlwaysPreTouch -XX:MaxInlineLevel=15 -jar velocity.jar; \
expect -re {\[[0-9]{2}:[0-9]{2}:[0-9]{2} INFO\]: Done .*!} { \
send \"stop\r\"; \
expect eof \
} \
" && \
# Disable bStats by default and clear server-uuid
cd /app/plugins/bStats/ || exit 1; \
sed -i.bak "s/^enabled=.*\$/enabled=${BSTATS_ENABLED}/" config.txt && \
diff --unified=1 config.txt.bak config.txt || true && rm config.txt.bak && \
sed -i.bak "s/^server-uuid=.*\$/server-uuid=/" config.txt && \
diff --unified=1 config.txt.bak config.txt || true && rm config.txt.bak && \
# Truncate forwarding secret
truncate -s 0 /app/forwarding.secret
# 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"]

View File

@ -1,19 +1,188 @@
#!/bin/bash
set -eu
# Set eula value in eula.txt
set_eula() {
local EULA
EULA="${1:-false}"
EULAFILE="${EULAFILE:-/app/eula.txt}"
sed -i.bak "s/^eula=.*\$/eula=${EULA:-false}/" "$EULAFILE"
diff --unified=1 "${EULAFILE}.bak" "$EULAFILE" || true
rm "${EULAFILE}.bak"
}
# Update server.properties using env
set_properties() {
# Basic settings
DEBUG="${DEBUG:-false}"
PREFIX="${PREFIX:-SETTINGS_}"
FILE="${FILE:-/app/server.properties}"
# Update server.properties
while IFS='=' read -r ENVVAR VALUE ; do
if echo "$ENVVAR" | grep -q "^${PREFIX}.*$"; then
KEY="${ENVVAR#"$PREFIX"}"
if ! grep -q "^${KEY}=" "$FILE"; then
echo "[WARN]: \"$KEY\" does not exist in $FILE and was not updated"
else
[ "$DEBUG" = "true" ] && echo "[DEBUG]: Updating \"$KEY\" to \"$VALUE\""
sed -i.bak "s/^${KEY}=.*/${KEY}=${VALUE}/" "$FILE"
diff --unified=1 "${FILE}.bak" "$FILE" || true
rm "${FILE}.bak"
fi
else
if [ "$DEBUG" = "true" ]; then
echo "[DEBUG]: \"$ENVVAR\" doesn't have the prefix \"$PREFIX\""
fi
fi
done < <(env)
# Show server.properties in DEBUG mode
if [ "$DEBUG" = "true" ]; then
echo "[DEBUG]: Showing ${FILE}:"
cat "$FILE"
fi
}
# Set Velocity's forwarding.secret
set_forwarding_secret() {
local WRITE_FILE
local FILE_CONTENT
WRITE_FILE=false
# Check file is not empty
if [ -s /app/forwarding.secret ]; then
FILE_CONTENT="$(head -c 1025 /app/forwarding.secret)"
# Check that FORWARDING_SECRET is blank
if [ ! "${#FORWARDING_SECRET}" -gt 0 ]; then
# Only the file was set, so FORWARDING_SECRET becomes the file
FORWARDING_SECRET="$(head -c 1025 /app/forwarding.secret)"
else
if [ ! "$FORWARDING_SECRET" = "$FILE_CONTENT" ]; then
# You should either bind mount a file in OR set a value
echo "[ERROR]: FORWARDING_SECRET is set with an existing file"
exit 1
fi
fi
# If the file is zero, we make sure the variable isn't also zero
elif [ "${#FORWARDING_SECRET}" -eq 0 ]; then
echo "[ERROR]: You must set FORWARDING_SECRET or set a value in the file"
exit 1
else
# File is zero, so we must write the variable out to the file
WRITE_FILE=true
fi
# Check length
if [ "${#FORWARDING_SECRET}" -lt 32 ]; then
echo "[ERROR]: FORWARDING_SECRET needs to be at least 32 characters long"
exit 1
elif [ "${#FORWARDING_SECRET}" -gt 1024 ]; then
echo "[ERROR]: FORWARDING_SECRET is >1024 bytes"
exit 1
fi
# Add secret to file
if [ "$WRITE_FILE" = "true" ]; then
echo "$FORWARDING_SECRET" > /app/forwarding.secret
fi
# Unset sensitive value
unset FORWARDING_SECRET
}
# Check if the minecraft screen is still running
# shellcheck disable=SC2317
check_screen() {
if [ "$(screen -ls | grep -cE '[0-9]+\.minecraft')" -eq 1 ]; then
local SCREEN_NAME
SCREEN_NAME="$1"
if [ "$(screen -ls | grep -cE "[0-9]+\.$SCREEN_NAME")" -eq 1 ]; then
return 0
else
return 1
fi
}
# Find Java PID, strace it, and wait for it to exit
wait_on_java() {
local JAVA_PID
local JAVA_EXIT
# Debug mode
[ "$DEBUG" = "true" ] && ps aux
# Capture PID and test
JAVA_PID="$(pgrep java)"
if ! kill -0 "$SCREEN_PID" 2>/dev/null; then
echo "[ERROR]: Cannot find running Java process (PID: \"$JAVA_PID\")"
exit 1
fi
# strace Java PID and get return code
JAVA_EXIT="$(strace -e trace=exit -p "$JAVA_PID" 2>&1 \
| grep -oP '^\+\+\+ exited with \K[0-9]+')"
# Delay if Java exits non-zero
if [ ! "$JAVA_EXIT" = "0" ]; then
echo "[ERROR]: Java exited with non-zero status"
sleep "${EXIT_DELAY:-5}"
exit "$JAVA_EXIT"
fi
}
# Find screen PID, strace it, and wait for it to exit
wait_on_screen() {
local SCREEN_PID
local SCREEN_NAME
local STRACE_PID
local TAIL_PID
SCREEN_NAME="$1"
# Get screen PID
[ "$DEBUG" = "true" ] && screen -ls
SCREEN_PID="$(
screen -ls | grep -oE "[0-9]+\.$SCREEN_NAME" | cut -d. -f1
)"
# Check screen PID
if ! kill -0 "$SCREEN_PID" 2>/dev/null; then
echo "[ERROR] Cannot find \"$SCREEN_NAME\" screen (PID: \"$SCREEN_PID\")"
exit 1
fi
# Output logs to stdout (touch in case slow to create)
touch screenlog.0
tail -f screenlog.0 &
TAIL_PID="$!"
# Debug mode
[ "$DEBUG" = "true" ] && ps aux
# Wait for screen to exit
strace -e exit -e signal=none -p "$SCREEN_PID" 2>/dev/null &
STRACE_PID="$!"
# Wait on Java PID first
wait_on_java
# Wait if screen is somehow still running
wait "$STRACE_PID"
# Kill tail PID
if kill -0 "$TAIL_PID" 2>/dev/null; then
kill "$TAIL_PID"
fi
}
# Function to stop the server gracefully
# shellcheck disable=SC2317
stop_server() {
if check_screen; then
local SCREEN_NAME
SCREEN_NAME="$1"
if check_screen "$SCREEN_NAME"; then
# Run 'stop' inside screen and wait for the screen to exit
/usr/bin/screen -p 0 -S minecraft -X eval 'stuff "stop"\015'
/usr/bin/screen -p 0 -S "$SCREEN_NAME" -X eval 'stuff "stop"\015'
wait "$STRACE_PID"
# Stop tail -f to stdout
@ -24,13 +193,13 @@ stop_server() {
# Check only this script is running (PID 1) and pgrep (2 PIDs total)
PGREP_OUTPUT="$(pgrep .)"
if ! [ "$(echo "$PGREP_OUTPUT" | wc -l)" -eq 2 ]; then
echo "[WARN] Some processes might not have exited:"
echo "[WARN]: Some processes might not have exited:"
echo "$PGREP_OUTPUT"
exit 1
fi
# Exit cleanly
echo "[INFO] Server stopped gracefully"
echo "[INFO]: Server stopped gracefully"
exit 0
else
echo "[ERROR]: Can't find which screen to use"
@ -39,77 +208,75 @@ stop_server() {
fi
}
# Start the Minecraft server
minecraft_server() {
# Settings
JVM_OPTS="${JVM_OPTS:--Xms1G -Xmx2G}"
# Set EULA
set_eula "${EULA:-false}"
# Update server.properties using env
set_properties
# Set up a SIGTERM signal trap to stop the server
trap 'stop_server minecraft' SIGTERM
# temp
ls -al /app
ls -al /app/config
# Run server in screen (without attaching)
echo "[INFO]: Starting Minecraft server"
/usr/bin/screen -dmS minecraft -L \
bash -c "/usr/bin/java $JVM_OPTS -jar server.jar --nogui"
# Wait for 'minecraft' screen PID to exit
wait_on_screen minecraft
exit 0
}
# Start the Velocity proxy Minecraft server
velocity_server() {
# Settings
JVM_OPTS="${JVM_OPTS:--Xms1G -Xmx2G}"
# Ensure there is a forwarding.secret
set_forwarding_secret
# Set up a SIGTERM signal trap to stop the server
trap 'stop_server velocity' SIGTERM
# Start server
echo "[INFO]: Starting Velocity server"
/usr/bin/screen -dmS velocity -L \
bash -c "
/usr/bin/java $JVM_OPTS -XX:+UseG1GC -XX:G1HeapRegionSize=4M \
-XX:+UnlockExperimentalVMOptions -XX:+ParallelRefProcEnabled \
-XX:+AlwaysPreTouch -XX:MaxInlineLevel=15 -jar velocity.jar
"
# Wait for 'velocity' screen PID to exit
wait_on_screen velocity
}
# Enable debug mode
DEBUG="${DEBUG:-false}"
if [ "$DEBUG" = "true" ]; then
echo "[DEBUG] Running entrypoint script at $(which entrypoint.sh)"
echo "[DEBUG]: Running entrypoint script at $(which entrypoint.sh)"
sleep 0.2
set -ux
fi
# Settings
FILE="${FILE:-/app/server.properties}"
EULAFILE="${EULAFILE:-/app/eula.txt}"
PREFIX="${PREFIX:-SETTINGS_}"
JVM_OPTS="${JVM_OPTS:--Xms1G -Xmx2G}"
# Set EULA
sed -i.bak "s/^eula=.*\$/eula=${EULA:-false}/" "$EULAFILE"
diff --unified=1 "${EULAFILE}.bak" "$EULAFILE"
rm "${EULAFILE}.bak"
# Update server.properties using env
while IFS='=' read -r ENVVAR VALUE ; do
if echo "$ENVVAR" | grep -q "^${PREFIX}.*$"; then
KEY="${ENVVAR#"$PREFIX"}"
if ! grep -q "^${KEY}=" "$FILE"; then
echo "[WARN]: \"$KEY\" does not exist in $FILE and was not updated"
# Start Velocity proxy if VELOCITY='true' otherwise start a Minecraft server
VELOCITY="${VELOCITY:-false}"
if [ "$VELOCITY" = "true" ]; then
# Start Velocity proxy
velocity_server
else
[ "$DEBUG" = "true" ] && echo "[DEBUG] Updating \"$KEY\" to \"$VALUE\""
sed -i.bak "s/^${KEY}=.*/${KEY}=${VALUE}/" "$FILE"
diff --unified=1 "${FILE}.bak" "$FILE"
rm "${FILE}.bak"
fi
else
if [ "$DEBUG" = "true" ]; then
echo "[DEBUG] \"$ENVVAR\" doesn't have the prefix \"$PREFIX\""
fi
fi
done < <(env)
# Show server.properties in DEBUG mode
if [ "$DEBUG" = "true" ]; then
echo "[DEBUG] Showing ${FILE}:"
cat "$FILE"
# Start Minecraft
minecraft_server
fi
# Set up a SIGTERM signal trap to stop the server
trap 'stop_server' SIGTERM
# Run server in screen (without attaching)
echo "[INFO] Starting Minecraft server"
/usr/bin/screen -dmS minecraft -L \
bash -c "/usr/bin/java $JVM_OPTS -jar server.jar --nogui"
# Get screen PID
[ "$DEBUG" = "true" ] && screen -ls
SCREEN_PID="$(
screen -ls | grep -oE '[0-9]+\.minecraft' | cut -d. -f1
)"
# Check screen PID
if ! kill -0 "$SCREEN_PID" 2>/dev/null; then
echo "[ERROR] Cannot find Minecraft screen (PID: \"$SCREEN_PID\")"
exit 1
fi
# Output logs to stdout (touch in case slow to create)
touch screenlog.0
tail -f screenlog.0 &
TAIL_PID="$!"
# Wait for screen to exit
strace -e exit -e signal=none -p "$SCREEN_PID" 2>/dev/null &
STRACE_PID="$!"
[ "$DEBUG" = "true" ] && ps aux
wait "$STRACE_PID"
# Exit gracefully
exit 0

2
scratch/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore