Minecraft on GraalVM Java in Pterodactyl
Running Minecraft Java on GraalVM Java container images in Pterodactyl for performance and stability improvements.

In some previous posts I have gone into detail around running Minecraft on stock Docker using images built and distributed by ITZG. This post goes over a different approach where we run the same or just our existing Minecraft server deployments but this time switching over to a GraalVM container image.

What is GraalVM?

What lead me to testing and running GraalVM java images?
I've recently undergone a migration of all hosted game servers from a fully hands-on manual deployment to stock docker container-host VMs into a deployment of Pterodactyl Wings VMs.
The migration of game servers into the Pterodactyl platform enables the end-users and game server owners to manage and directly interact with their own instances which frees up a ton of time on my end while giving significantly more management ability and empowerment to end-users to solve issues quickly and manage their own instance states.
Performance issues with standard Java images
The metrics and details Pterodactyl provides has shown some significant inefficiency and major performance issues with running massive mod-packs on Minecraft Java using stock java images.
Performance improvements with GraalVM
After building GraalVM images for Pterodactyl and restarting the game-server instances with the new GraalVM based images, we saw CPU usage per container drop 40%-55%, we saw memory usage drop around 15%, and we saw gameplay experience go without impact when a Minecraft server is falling behind in ticks – the usual message similar to "Can't keep up! Is the server overloaded? Running 9307ms or 186 ticks behind".
This is a big deal when running absolutely massive mod-packs such as Forge packs:
- All the mods 10
- Prodigium Reforged (Terraria pack)
- Project Ozone 3
How to build and use GraalVM container / Docker images for use with Pterodactyl
I use Gitlab and Gitlab Runners for storing code and building Docker/Container images.
My Gitlab repo for this project: https://gitlab.largenut.com/pterodactyl/graalvm
I built this off of the official Pterodactyl Yolk builds found on the Pterodactyl Github repo with some changes to the Dockerfile to build GraalVM images for use with Pterodactyl.
Feel free to use or test my images from my Container registry for the following GraalVM Java versions
https://gitlab.largenut.com/pterodactyl/graalvm/container_registry/13
Container registry images -- prebuilt
registry.largenut.com/pterodactyl/graalvm:8
registry.largenut.com/pterodactyl/graalvm:11
registry.largenut.com/pterodactyl/graalvm:17
registry.largenut.com/pterodactyl/graalvm:19
registry.largenut.com/pterodactyl/graalvm:20
registry.largenut.com/pterodactyl/graalvm:21
registry.largenut.com/pterodactyl/graalvm:22
registry.largenut.com/pterodactyl/graalvm:23
registry.largenut.com/pterodactyl/graalvm:24
Adding custom Docker Images for use with Pterodactyl Eggs
Navigate to Admin > Nests > Minecraft
For each Minecraft Egg listed, edit by clicking the name of the egg...
In the 'Docker Images' box, youll add the new images for each GraalVM version desired or that you've built.
For example:
GraalVM Java 8|registry.largenut.com:443/pterodactyl/graalvm:8
GraalVM Java 11|registry.largenut.com:443/pterodactyl/graalvm:11
GraalVM Java 17|registry.largenut.com:443/pterodactyl/graalvm:17
GraalVM Java 20|registry.largenut.com:443/pterodactyl/graalvm:20
GraalVM Java 21|registry.largenut.com:443/pterodactyl/graalvm:21
GraalVM Java 22|registry.largenut.com:443/pterodactyl/graalvm:22
GraalVM Java 23|registry.largenut.com:443/pterodactyl/graalvm:23
GraalVM Java 24|registry.largenut.com:443/pterodactyl/graalvm:24

Building for a multiple GraalVM Java versions
When running modpacks there is often the case where the version of Minecraft required for the modpack uses an older version of Java. In my case I have a need to support all the way back to Java version 8.
I'm accounting for the situation where GraalVM (Oracle) changed repo naming for the GraalVM container image releases by setting the base image for each GraalVM version being built (this also lets us use a specific image release while tagging on our end the specific java version only).
I'm using the official Oracle GraalVM images as base images for the Pterodactyl compatible GraalVM container image builds.
Scroll down to build for only a single GraalVM version.
Gitlab CI for building for multiple GraalVM (Java) versions
stages:
- build
variables:
DOCKER_BUILDKIT: 1
build-image:
stage: build
image: docker:latest
services:
- docker:dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
parallel:
matrix:
- GRAALVM_VERSION: ["8", "11", "17", "20", "21", "22", "23", "24"]
script:
- |
case "$GRAALVM_VERSION" in
"8")
BASE_IMAGE="ghcr.io/graalvm/graalvm-ce:ol8-java8-21.1.0"
;;
"11")
BASE_IMAGE="ghcr.io/graalvm/graalvm-ce:ol8-java11-22.3.3"
;;
"17")
BASE_IMAGE="ghcr.io/graalvm/jdk-community:17"
;;
"20")
BASE_IMAGE="ghcr.io/graalvm/jdk-community:20"
;;
"21")
BASE_IMAGE="ghcr.io/graalvm/jdk-community:21"
;;
"22")
BASE_IMAGE="ghcr.io/graalvm/jdk-community:22"
;;
"23")
BASE_IMAGE="ghcr.io/graalvm/jdk-community:23"
;;
"24")
BASE_IMAGE="ghcr.io/graalvm/jdk-community:24"
;;
*)
echo "Unsupported GraalVM version: $GRAALVM_VERSION"
exit 1
;;
esac
echo "Building GraalVM $GRAALVM_VERSION with base image: $BASE_IMAGE"
docker build --build-arg BASE_IMAGE=${BASE_IMAGE} -t $CI_REGISTRY_IMAGE:${GRAALVM_VERSION} .
docker push $CI_REGISTRY_IMAGE:${GRAALVM_VERSION}
only:
changes:
- Dockerfile
- entrypoint.sh
The modified Dockerfile to build with GraalVM Java (multi-version for Gitlab-CI)
ARG BASE_IMAGE
FROM ${BASE_IMAGE}
LABEL author="Gregg" maintainer="gregg@largenut.com"
LABEL org.opencontainers.image.source="gitlab.largenut.com"
RUN microdnf update -y && \
microdnf install -y lsof curl ca-certificates openssl git tar sqlite fontconfig freetype tzdata iproute libstdc++ && \
microdnf clean all && \
useradd -d /home/container -m container
USER container
ENV USER=container HOME=/home/container
WORKDIR /home/container
COPY entrypoint.sh /entrypoint.sh
CMD [ "/bin/bash", "/entrypoint.sh" ]

Building for a single GraalVM Java version
Gitlab CI for building for single GraalVM (Java) version
stages:
- build
variables:
DOCKER_BUILDKIT: 1
build-image:
stage: build
image: docker:latest
services:
- docker:dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
- docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG $CI_REGISTRY_IMAGE:latest
- docker push $CI_REGISTRY_IMAGE:latest
only:
changes:
- Dockerfile
The modified Dockerfile to build with GraalVM Java (single version – Java 24)
FROM ghcr.io/graalvm/jdk-community:24
LABEL author="Gregg" maintainer="gregg@largenut.com"
LABEL org.opencontainers.image.source="gitlab.largenut.com"
RUN microdnf update -y && \
microdnf install -y lsof curl ca-certificates openssl git tar sqlite fontconfig freetype tzdata iproute libstdc++ && \
microdnf clean all && \
useradd -d /home/container -m container
USER container
ENV USER=container HOME=/home/container
WORKDIR /home/container
COPY entrypoint.sh /entrypoint.sh
CMD [ "/bin/bash", "/entrypoint.sh" ]

Entrypoint Script
The entrypoint.sh script – from the Pterodactyl Github repo
This script is needed for either type of build, whether building with CI or not as this is copied during build as referenced in the Dockerfile.
https://github.com/pterodactyl/yolks/blob/master/java/entrypoint.sh
#!/bin/bash
#
# Copyright (c) 2021 Matthew Penner
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Default the TZ environment variable to UTC.
TZ=${TZ:-UTC}
export TZ
# Set environment variable that holds the Internal Docker IP
INTERNAL_IP=$(ip route get 1 | awk '{print $(NF-2);exit}')
export INTERNAL_IP
# Switch to the container's working directory
cd /home/container || exit 1
# Print Java version
printf "\033[1m\033[33mcontainer@pterodactyl~ \033[0mjava -version\n"
java -version
# Convert all of the "{{VARIABLE}}" parts of the command into the expected shell
# variable format of "${VARIABLE}" before evaluating the string and automatically
# replacing the values.
PARSED=$(echo "${STARTUP}" | sed -e 's/{{/${/g' -e 's/}}/}/g' | eval echo "$(cat -)")
# Display the command we're running in the output, and then execute it with the env
# from the container itself.
printf "\033[1m\033[33mcontainer@pterodactyl~ \033[0m%s\n" "$PARSED"
# shellcheck disable=SC2086
exec env ${PARSED}