diff --git a/Makefile b/Makefile index 5240d90..a4f162a 100644 --- a/Makefile +++ b/Makefile @@ -14,11 +14,12 @@ PRUNE_IMAGES = \ localhost/minecraft:latest-spigot \ localhost/minecraft:latest-craftbukkit \ localhost/minecraft-jre:latest \ - localhost/minecraft-jdk: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 +all: vanilla paper spigot craftbukkit velocity jre: $(DOCKER_COMPOSE_BUILD) minecraft-jre @@ -38,6 +39,9 @@ spigot: jre jdk craftbukkit: jre jdk $(DOCKER_COMPOSE_BUILD) minecraft-craftbukkit +velocity: jre + $(DOCKER_COMPOSE_BUILD) minecraft-velocity + install: $(DOCKER_COMPOSE_UP) @@ -70,8 +74,7 @@ define copy_build_files echo "INFO: \"$${SRC_FILE}\" copied to \"$${DEST_FILE}\""; \ fi; \ else \ - echo "ERROR: Source file \"$${SRC_FILE}\" does not exist."; \ - exit 1; \ + echo "WARN: Source file \"$${SRC_FILE}\" does not exist."; \ fi endef diff --git a/builds/velocity/.env b/builds/velocity/.env new file mode 100644 index 0000000..aca6f88 --- /dev/null +++ b/builds/velocity/.env @@ -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 +# diff --git a/docker-compose.build.yml b/docker-compose.build.yml index 01ce93a..aae746b 100644 --- a/docker-compose.build.yml +++ b/docker-compose.build.yml @@ -68,3 +68,15 @@ services: - minecraft-jdk environment: EULA: "${EULA:-false}" + + minecraft-velocity: + build: + context: . + dockerfile: ./dockerfiles/Dockerfile.velocity + args: + VERSION: ${VERSION:-latest} + image: ${VELOCITY_IMAGE:-localhost/velocity}:${VELOCITY_TAG:-latest} + depends_on: + - minecraft-jre + environment: + EULA: "${EULA:-false}" diff --git a/dockerfiles/Dockerfile.velocity b/dockerfiles/Dockerfile.velocity new file mode 100644 index 0000000..0abb1f7 --- /dev/null +++ b/dockerfiles/Dockerfile.velocity @@ -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"] diff --git a/entrypoint.sh b/entrypoint.sh index 0ea2776..6f0c60d 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,19 +1,102 @@ #!/bin/bash +set -e + +# 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" + 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" + 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 +} # 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 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="$!" + + # 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" +} + # 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 @@ -39,6 +122,51 @@ 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 + + # 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}" + + # 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 @@ -47,69 +175,15 @@ if [ "$DEBUG" = "true" ]; then 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" - 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 Velocity proxy if VELOCITY='true' otherwise start a Minecraft server +VELOCITY="${VELOCITY:-false}" +if [ "$VELOCITY" = "true" ]; then + # Start Velocity proxy + velocity_server +else + # 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