Hytale Server in Docker: Containerization Guide
Running a Hytale server directly on the host works fine - we covered that in our Debian 13 guide. But once you’re managing updates, juggling Java versions, or spinning up test instances, Docker becomes the obvious next step. This guide walks through containerizing a Hytale server from scratch - Dockerfile, Compose, QUIC networking, and production hardening.
In this article: Why Docker · Prerequisites · Project Structure · Dockerfile · Docker Compose · Volumes · Networking & Firewall · Authentication · Updating · Mods & Plugins · Monitoring · Troubleshooting
Why Run Your Hytale Server in Docker
If your bare-metal setup already works, why bother?
- Reproducibility. Your entire environment is two files (Dockerfile + compose.yaml).
docker compose upon any machine gives you an identical server. - Isolation. No stray files, no conflicting Java versions. Blow away the container and start fresh - world data survives on the volume.
- Safe updates. Pull a new image, recreate the container, volumes carry over. Roll back in seconds if something breaks.
- Multi-server setups. Copy the compose file, change the port, done. No extra user accounts or systemd units.
Prerequisites
You need Docker Engine and the Compose plugin on your Linux host. The official convenience script handles both:
curl -fsSL https://get.docker.com | sh
You also need a Hytale account that owns the game for server authentication.
Hardware is the same as bare metal: 4 GB RAM minimum, a modern CPU (clock speed over core count), and an SSD. Docker’s overhead is usually small for this kind of workload because the container uses the host’s kernel instead of a full virtual machine.
Project Structure
Create a dedicated directory for the project. Everything lives here - the Dockerfile, compose config, and persistent data:
mkdir -p ~/hytale-docker && cd ~/hytale-docker
By the end of this guide, the directory will look like this:
hytale-docker/
├── Dockerfile
├── compose.yaml
└── data/
├── server/
│ ├── HytaleServer.jar
│ ├── HytaleServer.aot
│ ├── config.json
│ ├── permissions.json
│ ├── mods/
│ ├── universe/
│ └── logs/
└── assets/
└── Assets.zip
The data/ directory is bind-mounted into the container. Everything inside it persists across container restarts, updates, and rebuilds.
Writing the Dockerfile
Most guides point you at a pre-built image and call it a day. We’ll write our own Dockerfile so you understand what’s inside, then mention pre-built alternatives at the end.
Base Image and Java 25
Hytale requires Java 25. Eclipse Temurin provides official Docker images:
FROM eclipse-temurin:25-jre-alpine
Why JRE and not JDK? The server only needs a Java runtime. The HytaleServer.aot file (AOT cache for faster startup) just needs to be loaded at runtime. Hytale ships the AOT cache pre-built, so the JDK’s ability to create one isn’t needed for normal server operation. Only reach for the JDK if you need diagnostic tools (jstack, jmap) inside the container.
Alpine keeps the image at ~100 MB versus 350+ MB for a Debian-based JDK image.
Server Files and Permissions
The Hytale server should never run as root inside the container. Create a dedicated user and set up the directory structure:
FROM eclipse-temurin:25-jre-alpine
RUN addgroup -S hytale && adduser -S -G hytale -h /home/hytale hytale
RUN apk add --no-cache wget
WORKDIR /home/hytale
# Download JMX Exporter for Prometheus monitoring
RUN mkdir -p /home/hytale/monitoring \
&& wget -q -O /home/hytale/monitoring/jmx_prometheus_javaagent.jar \
https://repo1.maven.org/maven2/io/prometheus/jmx/jmx_prometheus_javaagent/1.2.0/jmx_prometheus_javaagent-1.2.0.jar
# Server files and assets are mounted via volumes at runtime
RUN mkdir -p /home/hytale/server /home/hytale/assets \
&& chown -R hytale:hytale /home/hytale
USER hytale
EXPOSE 5520/udp
ENTRYPOINT ["java"]
CMD ["-Xms4G", "-Xmx4G", "-XX:AOTCache=/home/hytale/server/HytaleServer.aot", \
"-jar", "/home/hytale/server/HytaleServer.jar", \
"--assets", "/home/hytale/assets/Assets.zip", \
"--bind", "5520"]
JMX Exporter is baked into the image so it’s always available - enable it with a -javaagent flag (see monitoring). Server files are bind-mounted from the host, keeping updates independent of image rebuilds.
Getting the Server Files
The Hytale downloader authenticates with your account (OAuth2 device flow) and downloads a zip with the server files. Run it once on the host:
cd ~/hytale-docker
mkdir -p data/server data/assets
wget https://downloader.hytale.com/hytale-downloader.zip
unzip hytale-downloader.zip
chmod +x hytale-downloader
./hytale-downloader
It opens a browser auth flow and downloads a versioned zip. Unpack into the data directories:
unzip 2026*.zip
mv Server/* data/server/
mv Assets.zip data/assets/
No reason to put the downloader in the Dockerfile - it requires interactive browser auth. Download once on the host, mount the files in.
Pre-Built Alternatives
If you’d rather skip writing a Dockerfile entirely, several community images are available:
| Image | Notes |
|---|---|
indifferentbroccoli/hytale-server-docker | Auto-downloads server files, configurable via env vars |
deinfreu/hytale-server | Alpine/Liberica image, non-root, multi-arch |
These work well for quick deployments, but do not copy the custom volumes: and command: blocks below into a pre-built image blindly. Each image has its own paths and environment variables.
Docker Compose for Your Hytale Server
Create compose.yaml in your project directory:
services:
hytale:
build: .
container_name: hytale-server
restart: unless-stopped
ports:
- "5520:5520/udp"
volumes:
- ./data/server:/home/hytale/server
- ./data/assets:/home/hytale/assets
- /etc/machine-id:/etc/machine-id:ro
environment:
- TZ=UTC
deploy:
resources:
limits:
memory: 6G
cpus: "2.0"
reservations:
memory: 4G
cpus: "1.0"
stop_grace_period: 30s
Key details:
/etc/machine-idis mounted read-only to give the container a stable host identity. Several community Docker images include the same mount; if your server repeatedly asks for authentication after container recreation, check this mount and your persistent server volume first.restart: unless-stoppedhandles crashes and host reboots.stop_grace_period: 30sgives the server time to save world data before Docker kills it.- No
stdin_openortty- you’ll add those temporarily for initial auth then remove them.tty: trueinjects terminal control characters into logs, andstdin_open: truelets an accidentalCtrl+Condocker attachkill the server.
For a pre-built image, start from that image’s own Compose example. At minimum, build: . becomes an image: line such as:
image: indifferentbroccoli/hytale-server-docker
Environment Variables
Pre-built images expose configuration through image-specific env vars (MAX_MEMORY, MAX_PLAYERS, VIEW_DISTANCE, SERVER_PORT, TZ, or similar). With our custom Dockerfile, you’d pass JVM flags through the CMD override instead. Check your chosen image’s docs for exact names and paths.
Resource Limits
The deploy.resources block caps what the container can consume on current Docker Compose versions. Set the memory limit higher than -Xmx - the JVM uses memory beyond the heap (metaspace, thread stacks, native allocations). Rule of thumb: heap + 1.5-2 GB, so -Xmx4G means a container limit around 6G.
Health Checks
Hytale uses UDP, so TCP port checks won’t work. Check the Java process directly:
healthcheck:
test: ["CMD-SHELL", "pgrep -f HytaleServer.jar || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 60s
This confirms the process is alive but not that it’s accepting connections.
Volumes and Persistent Data
Everything that changes at runtime must live on a volume: world saves (universe/), configs, mods, logs, the server jar, and Assets.zip. Our compose file mounts the entire server/ and assets/ directories, covering all of it.
We use bind mounts (./data/server:/home/hytale/server) so you can browse, edit, and back up files directly from the host. Named volumes (hytale-data:/home/hytale/server) are an alternative if you prefer managing everything through docker volume commands, but the files aren’t as accessible from the host filesystem.
Backups
With bind mounts, a cron job that tars the data directory works:
# crontab -e
0 4 * * * tar czf /backups/hytale-$(date +\%Y\%m\%d).tar.gz -C ~/hytale-docker/data .
For consistency, stop the container first or use Hytale’s built-in backup flags (--backup --backup-dir ./backups --backup-frequency 60) which handle world saves safely.
Docker Networking for Hytale’s QUIC Protocol
Hytale uses QUIC exclusively - that’s UDP, not TCP. Every guide that tells you to map a TCP port is wrong.
Host Networking vs Port Mapping
Port mapping ("5520:5520/udp") works for most setups. Docker handles the NAT, players connect to your host’s IP on 5520.
Host networking (network_mode: host) removes the Docker network layer entirely - the container shares the host’s network stack. Zero NAT overhead, but no network isolation.
| Approach | Latency | Isolation | Recommended for |
|---|---|---|---|
| Port mapping | Negligible | Full | Most setups |
| Host networking | Zero | None | Competitive/low-latency servers |
Use port mapping unless you’re running a competitive server or hit UDP-related issues with Docker’s userland proxy.
Docker Bypasses Your Firewall (Seriously)
Published Docker ports can bypass ufw rules. Docker’s own documentation calls out this incompatibility: published container traffic is routed in NAT before it reaches the INPUT/OUTPUT chains that ufw uses. When a packet arrives for a published port, Docker’s DNAT rule in nat/PREROUTING rewrites the destination to the container’s bridge IP before the routing decision. The kernel then routes it through FORWARD (where Docker allows it) instead of INPUT (where your ufw rules live).
Packet arrives at host:5520/udp
→ nat/PREROUTING: Docker DNAT rewrites dest to 172.17.0.2:5520
→ Routing decision: "this is for the bridge network, not localhost"
→ filter/FORWARD chain (Docker's rules → ACCEPT)
→ Packet reaches container
→ filter/INPUT chain: never consulted
For the game port (5520/udp), that’s fine - you want it public. But every other published port (Prometheus, Grafana, exporters) may also be reachable even when your ufw rules look restrictive.
Three ways to protect internal services:
1. Bind to localhost - simplest approach. "127.0.0.1:9090:9090" means Docker’s DNAT only matches loopback traffic. Access via SSH tunnel (ssh -L 3000:localhost:3000 your-server).
2. DOCKER-USER chain - Docker’s official way to filter forwarded traffic, evaluated before Docker’s own ACCEPT rules:
iptables -I DOCKER-USER -p tcp --dport 9225 -s <monitoring-ip> -j ACCEPT
iptables -I DOCKER-USER -p tcp --dport 9225 -j DROP
Catch: ufw doesn’t manage these rules, and they aren’t persistent by default.
3. Host networking - network_mode: host means Docker does not create bridge-network firewall rules for the container. Traffic flows through the host network stack directly, so host firewall behavior is easier to reason about.
Server Authentication in Docker
Hytale requires server authentication before players can connect. Temporarily add stdin_open: true and tty: true to your compose file, then:
docker compose up -d
docker attach hytale-server
In the console, run /auth login device. The server prints a URL and device code - open the URL, sign in, and enter the code.
Detach with Ctrl+P then Ctrl+Q (not Ctrl+C - that kills the server). Remove stdin_open and tty from compose and recreate with docker compose up -d.
For auth to survive restarts, the server directory must be on a persistent volume. If your containerized server still asks for re-authentication after recreation, also keep /etc/machine-id mounted read-only so the container identity stays stable.
Updating Your Hytale Docker Server
Custom Dockerfile: docker compose down, run the downloader on the host, extract new files into data/server/ and data/assets/, docker compose up -d. World data and configs are untouched on the volume.
Pre-built image: docker compose pull && docker compose down && docker compose up -d. Some images can fetch the latest server files automatically when configured to do so; check the image’s update settings before relying on that behavior.
Rolling back: For custom builds, keep the previous server files. For pre-built images, tag the working image before pulling (docker tag image:latest hytale-backup:working) and switch back in compose if something breaks.
Adding Mods and Plugins
The mods/ directory is on the bind mount, so adding plugins is the same as bare metal - drop the jar in and docker compose restart.
OneQuery is a good first plugin - it adds a UDP query protocol for health checks, monitoring, and registration with the HytaleONE server list. Plugin configs appear in mods/ on the host after first run - edit directly and restart.
Monitoring a Dockerized Hytale Server
If you followed our Prometheus and Grafana monitoring guide, the Docker setup changes how you deploy exporters. For busy production servers, Prometheus and Grafana are better on a separate machine - Prometheus writes to disk constantly and can compete for the IOPS your game server needs.
On the game server, Node Exporter must use network_mode: host to report accurate metrics - otherwise it only sees the container’s virtual interface instead of the host’s actual eth0. This is the officially recommended deployment:
node-exporter:
image: prom/node-exporter:latest
container_name: node-exporter
restart: unless-stopped
network_mode: host
pid: host
volumes:
- /:/host:ro,rslave
command:
- '--path.rootfs=/host'
Node Exporter listens on port 9100 directly on the host. For JVM metrics, the JMX Exporter is already in our Dockerfile. Enable it by overriding the command in compose:
hytale:
# ... other settings ...
command:
- "-Xms4G"
- "-Xmx4G"
- "-XX:AOTCache=/home/hytale/server/HytaleServer.aot"
- "-javaagent:/home/hytale/monitoring/jmx_prometheus_javaagent.jar=9225:/home/hytale/server/jmx-config.yml"
- "-jar"
- "/home/hytale/server/HytaleServer.jar"
- "--assets"
- "/home/hytale/assets/Assets.zip"
- "--bind"
- "5520"
The config path (/home/hytale/server/jmx-config.yml) points into the bind mount so you can edit scrape rules at data/server/jmx-config.yml without rebuilding. The monitoring guide has a complete JMX config.
JMX Exporter listens on port 9225 inside the container. Your Prometheus on a separate machine needs to reach it, and publishing the port (9225:9225) hits the firewall bypass we discussed. Two options:
- Host networking:
network_mode: hoston the Hytale container.ufwworks normally -ufw allow from <monitoring-ip> to any port 9225 proto tcp. - Bridge + DOCKER-USER: Publish port 9225, then restrict with
iptables -I DOCKER-USER -p tcp --dport 9225 -s <monitoring-ip> -j ACCEPTand-j DROPfor everything else.
On the monitoring machine, point Prometheus at the game server’s IP for Node Exporter (9100) and JMX Exporter (9225). The monitoring guide’s firewall section covers the Prometheus-side setup.
Single-machine fallback: Don’t publish port 9225 at all - put Prometheus in the same Docker network and scrape via container name (hytale-server:9225). No port exposed to the host, no firewall bypass.
Troubleshooting
Container starts but players can’t connect.
Check that you’re mapping UDP, not TCP. 5520:5520/udp - the /udp suffix matters. Hytale uses QUIC, which runs over UDP exclusively. If you’re using network_mode: host, also verify the host firewall allows UDP 5520 (ufw allow 5520/udp). With bridge networking and port mapping, published Docker ports can bypass ufw, so if the port is mapped correctly, a normal ufw deny rule usually is not the problem.
“Permission denied” errors in container logs.
Your bind mount files are owned by a different UID than the container user. Our Dockerfile creates a system user with adduser -S, which gets a low UID (typically around 100 on Alpine, not 1000). Run docker exec hytale-server id to find the exact UID, then chown the host files to match. Pre-built images often use UID 1000 by default - check their documentation.
Server runs out of memory and gets killed.
Docker’s OOM killer terminates the container when it exceeds its memory limit. Your -Xmx value plus JVM overhead must fit within the deploy.resources.limits.memory setting. If -Xmx is 4G, set the container limit to at least 6G.
Auth token lost after container recreate.
Two common causes are an ephemeral server directory and an unstable container identity. Make sure the server directory is on a persistent volume. If the problem continues, mount /etc/machine-id:/etc/machine-id:ro, matching the pattern used by community Docker images.
Container exits immediately on start.
Check logs with docker compose logs hytale. Common causes: missing server files (the jar or Assets.zip isn’t in the mounted directory), wrong Java version in the base image, or insufficient memory allocation.
High latency compared to bare metal.
Switch to network_mode: host in your compose file to bypass Docker’s network layer. This eliminates the userland proxy and any NAT overhead. The difference is usually negligible, but it’s the first thing to try if players report higher ping than expected.
That’s the full path from an empty directory to a production Hytale server in Docker. List your server on HytaleONE by installing the OneQuery plugin - it works identically in Docker as on bare metal.