Underground 2 Revived
Play Need for Speed: Underground 2 (2004) in Chrome and Firefox with WebGPU graphics, Web Audio, up to 120 fps widescreen, controller support, free roam and Online races with friends. Fan project, not
Routes Minecraft client connections to backend servers based upon the requested server address
Translation pending
GamesGo · DockerfileGitHub @itzgSelf-installOwner unconfirmed
Paste into Claude or ChatGPT for installation instructions suited to your environment.
Have you tried it? One tap is enough. No sign-in needed.
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗Routes Minecraft Java Edition client connections to backend servers based upon the requested server address. This is quite helpful for home network deployments where any number of Minecraft servers can be multiplexed onto a single, public IP address and standard Minecraft port. It works great with itzg/minecraft-server, but can be used with any Minecraft server. Stand-alone binaries are also published for Linux, Mac, and Windows if not wanting to use containers at all.
Some other features included:
-api-binding host:port
The host:port bound for servicing API requests (env API_BINDING)
-auto-scale-allow-deny string
Path to config for server allowlists and denylists. If a global/server entry is specified, only players allowed to connect to the server will be able to trigger a scale up when -auto-scale-up is enabled or cancel active down scalers when -auto-scale-down is enabled (env AUTO_SCALE_ALLOW_DENY)
-auto-scale-asleep-motd string
MOTD to display when auto-scaled down servers are accessed; if empty, no status will be served (env AUTO_SCALE_ASLEEP_MOTD)
-auto-scale-down
Scale to zero after idle. For Kubernetes, decreases StatefulSet replicas from 1 to 0. For Docker, gracefully stops the container when there are no connections (env AUTO_SCALE_DOWN)
-auto-scale-down-after duration
Server scale down delay after there are no connections (env AUTO_SCALE_DOWN_AFTER) (default 10m0s)
-auto-scale-status-resets-down-timer
Reset the scale down timer when status requests are proxied; by default only player login connections reset it (env AUTO_SCALE_STATUS_RESETS_DOWN_TIMER)
-auto-scale-loading-motd string
MOTD to display while auto-scaled Docker servers are waking up; if empty, asleep status will be served (env AUTO_SCALE_LOADING_MOTD)
-auto-scale-up
Scale from zero on access. For Kubernetes, increases StatefulSet replicas from 0 to 1. For Docker, starts or unpauses the container when accessed (env AUTO_SCALE_UP)
-auto-scale-wake-kick-message string
If set, a player logging in to a scaled-down or still waking server is immediately disconnected with this message while the server keeps waking up, instead of holding the login until the server is ready (the Java client gives up after about 30 seconds) (env AUTO_SCALE_WAKE_KICK_MESSAGE)
-auto-scale-webhook-headers value
Zero or more 'key=value' headers added to scaler webhook requests, e.g. for authentication tokens (env AUTO_SCALE_WEBHOOK_HEADERS)
-auto-scale-webhook-timeout duration
Timeout for each scaler webhook request (env AUTO_SCALE_WEBHOOK_TIMEOUT) (default 30s)
-auto-scale-webhook-url string
If set, statically-configured backends are scaled up on access and down after idle by POSTing to this URL (env AUTO_SCALE_WEBHOOK_URL)
-auto-scale-webhook-wake-timeout duration
Maximum time to wait for the backend to become reachable after a scale-up webhook (env AUTO_SCALE_WEBHOOK_WAKE_TIMEOUT) (default 1m0s)
-backend-dial-timeout duration
Timeout for establishing the TCP connection to a backend. Bounds how long a dial to a scaled-to-zero server's Service (no endpoints) waits before the auto-scale asleep MOTD / scale-up fallback is served; operators with fast on-cluster backends can lower it so that fallback fires sooner (env BACKEND_DIAL_TIMEOUT) (default 2s)
-clients-to-allow value
Zero or more client IP addresses or CIDRs to allow. Takes precedence over deny. (env CLIENTS_TO_ALLOW)
-clients-to-deny value
Zero or more client IP addresses or CIDRs to deny. Ignored if any configured to allow (env CLIENTS_TO_DENY)
-connection-rate-limit int
Max number of connections to allow per second (env CONNECTION_RATE_LIMIT) (default 1)
-cpu-profile string
Enables CPU profiling and writes to given path (env CPU_PROFILE)
-debug
Enable debug logs (env DEBUG)
-default string
host:port of a default Minecraft server to use when mapping not found (env DEFAULT)
-docker-api-version string
Instead of auto-negotiating, use specific Docker API version (env DOCKER_API_VERSION)
-docker-network string
Default network for Docker containers attached to more than one network, matched like the mc-router.network label, which takes precedence (env DOCKER_NETWORK)
-docker-refresh-interval duration
Deprecated and ignored: Docker discovery is now event-driven (env DOCKER_REFRESH_INTERVAL)
-docker-socket string
Path to Docker socket to use (env DOCKER_SOCKET)
-docker-timeout duration
Timeout (as duration) for the Docker integrations (env DOCKER_TIMEOUT)
-dynamic-proxy-protocol
Accept connections with or without PROXY protocol. When a connection supplies a PROXY header, it is forwarded to the backend. Cannot be combined with -receive-proxy-protocol or -use-proxy-protocol (env DYNAMIC_PROXY_PROTOCOL)
-in-docker
Use Docker service discovery (env IN_DOCKER)
-in-docker-swarm
Use Docker Swarm service discovery (env IN_DOCKER_SWARM)
-in-kube-cluster
Use in-cluster Kubernetes config (env IN_KUBE_CLUSTER)
-kube-config string
The path to a Kubernetes configuration file (env KUBE_CONFIG)
-kube-namespace string
The namespace to watch or blank for all, which is the default (env KUBE_NAMESPACE)
-log-level value
Set a specific log filtering level, such as debug, info, warn, error
Ignored when --debug or --trace is used (env LOG_LEVEL) (default info)
-mapping value
Comma or newline delimited or repeated mappings of externalHostname=host:port (env MAPPING)
-metrics-backend string
Backend to use for metrics exposure/publishing: discard,expvar,influxdb,prometheus (env METRICS_BACKEND) (default "discard")
-metrics-backend-config-influxdb-addr string
(env METRICS_BACKEND_CONFIG_INFLUXDB_ADDR)
-metrics-backend-config-influxdb-database string
(env METRICS_BACKEND_CONFIG_INFLUXDB_DATABASE)
-metrics-backend-config-influxdb-interval duration
(env METRICS_BACKEND_CONFIG_INFLUXDB_INTERVAL) (default 1m0s)
-metrics-backend-config-influxdb-password string
(env METRICS_BACKEND_CONFIG_INFLUXDB_PASSWORD)
-metrics-backend-config-influxdb-retention-policy string
(env METRICS_BACKEND_CONFIG_INFLUXDB_RETENTION_POLICY)
-metrics-backend-config-influxdb-tags value
any extra tags to be included with all reported metrics (env METRICS_BACKEND_CONFIG_INFLUXDB_TAGS)
-metrics-backend-config-influxdb-username string
(env METRICS_BACKEND_CONFIG_INFLUXDB_USERNAME)
-metrics-rate-limit-period duration
The period at which the rate limit bucket's metrics are set: 0 to disable (default 1s) (env METRICS_RATE_LIMIT_PERIOD) (default 1s)
-ngrok-remote-addr string
If set, the TCP address to request for this edge (env NGROK_REMOTE_ADDR)
-ngrok-token string
If set, an ngrok tunnel will be established. It is HIGHLY recommended to pass as an environment variable. (env NGROK_TOKEN)
-port port
The port bound to listen for Minecraft client connections (env PORT) (default 25565)
-receive-proxy-protocol
Receive PROXY protocol from backend servers, by default trusts every proxy header that it receives, combine with -trusted-proxies to specify a list of trusted proxies (env RECEIVE_PROXY_PROTOCOL)
-record-logins
Log and generate metrics on player logins. Metrics only supported with influxdb or prometheus backend (env RECORD_LOGINS)
-routes-config path
Name or full path to routes config file (env ROUTES_CONFIG)
-routes-config-watch
Watch for config file changes (env ROUTES_CONFIG_WATCH)
-simplify-srv
Simplify fully qualified SRV records for mapping (env SIMPLIFY_SRV)
-trace
Enable trace logs (env TRACE)
-trusted-proxies value
Comma delimited list of CIDR notation IP blocks to trust when receiving PROXY protocol (env TRUSTED_PROXIES)
-use-proxy-protocol
Send PROXY protocol to backend servers (env USE_PROXY_PROTOCOL)
-version
Output version and exit (env VERSION)
-webhook-events value
Comma delimited list of events to send to the webhook. Valid values are connect, disconnect, route-added, route-removed, default-route-set, default-route-removed (env WEBHOOK_EVENTS) (default connect,disconnect)
-webhook-require-user
Indicates if the webhook will only be called if a user is connecting rather than just server list/ping (env WEBHOOK_REQUIRE_USER)
-webhook-timeout duration
Timeout for each connection status notification request (env WEBHOOK_TIMEOUT) (default 30s)
-webhook-url string
If set, a POST request that contains connection status notifications will be sent to this HTTP address (env WEBHOOK_URL)
mc-router can pass the real client IP address through to backend servers using the PROXY protocol. Without it, backend servers see every connection as coming from mc-router itself, which breaks IP-based banning, allow-listing, and similar protections. Three mutually exclusive modes are available, each with a matching environment variable:
-use-proxy-protocol (USE_PROXY_PROTOCOL): prepend a PROXY protocol v2 header carrying the real client address to the connection opened towards the backend. This is the common choice when mc-router fronts Minecraft servers directly.-receive-proxy-protocol (RECEIVE_PROXY_PROTOCOL): expect and consume a PROXY header sent by an upstream proxy or load balancer, so that mc-router itself sees the real client address. The header is not forwarded to the backend. By default, every received header is trusted; pass -trusted-proxies (TRUSTED_PROXIES), a comma-delimited list of CIDR blocks, to only honor headers sent from those upstream addresses.-dynamic-proxy-protocol (DYNAMIC_PROXY_PROTOCOL): accept connections with or without a PROXY header. When a trusted upstream supplies a header, it is forwarded to the backend. Cannot be combined with either of the other two flags.Backend servers must be configured to expect the header, otherwise the leading bytes are read as garbage and the connection fails. The setup differs per server "flavor":
proxy_protocol: true on the listener in config.ymlhaproxy-protocol = true under [advanced] in velocity.tomlproxies.proxy-protocol: true in config/paper-global.yml, and keep prevent-proxy-connections set to false in server.properties[!IMPORTANT] Only accept PROXY protocol on ports that are not directly reachable by untrusted clients: any client able to open a raw TCP connection can forge a header. When receiving with
-receive-proxy-protocolor-dynamic-proxy-protocol, restrict trust with-trusted-proxies.
The multi-architecture image published at Docker Hub supports amd64, arm64, and arm32v6 (i.e. RaspberryPi).
The image bundles tzdata so log timestamps can match your local timezone. Pick one:
TZ environment variable, e.g. TZ=America/New_York-v /etc/localtime:/etc/localtime:roIf neither is set, timestamps are in UTC.
The diagram below shows how this docker-compose.yml configures two Minecraft server services named vanilla and forge, which also become the internal network aliases. Notice those services don't need their ports exposed since the internal networking allows for the inter-container access.
services:
vanilla:
image: itzg/minecraft-server
environment:
EULA: "TRUE"
forge:
image: itzg/minecraft-server
environment:
EULA: "TRUE"
TYPE: FORGE
router:
image: ${MC_ROUTER_IMAGE:-itzg/mc-router}
depends_on:
- forge
- vanilla
environment:
MAPPING: |
vanilla.example.com=vanilla:25565
forge.example.com=forge:25565
ports:
- "25565:25565"
The router service is only one of the services that needs to exposed on the external network. The MAPPING declares how the hostname users will enter into their Minecraft client will map to the internal services.
To test out this example, add these two entries to my "hosts" file:
127.0.0.1 vanilla.example.com
127.0.0.1 forge.example.com
When running mc-router in a Docker environment you can pass the --in-docker or --in-docker-swarm command-line argument or set the environment variables IN_DOCKER or IN_DOCKER_SWARM to "true". With that, it will subscribe to the Docker event stream to react to container/service lifecycle changes (start, stop, pause, unpause, rename, network connect/disconnect for containers; create, update, remove for swarm services) and update routes immediately. An initial listing is performed on startup, and the stream is reconnected with exponential backoff on errors (e.g. daemon restart). To enable discovery, you have to set the mc-router.host label on the container.
When using in Docker, make sure to volume mount the Docker socket into the container, such as
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
[!IMPORTANT] If getting "Permission denied" errors for the Docker socket,
/var/run/docker.sock, then it is because the image now runs the container as a non-root user by default. The Docker socket is not readable or writable a regular user/group. Refer to one of the following notes for solutions.
[!NOTE] One solution is to add the group ID of the docker system user to match your system. Group ID 999 is the default for Docker's native installation on Linux. When using Docker Desktop it tends to assign the root group through as the socket owner.
Find the group ID by using
stat -c '%g' /var/run/docker.sockon your host.Typically on Linux, add the following to the container/service definition:
group_add: # docker system user's group ID - "999"or with Docker Desktop
group_add: # root user's group ID - "0"You can also simply "revert" to the previous behavior add add the following to the container/service definition:
user: rootto the container/service definition.
[!NOTE] If you would like to instead isolate the elevated access to the Docker socket, a socket proxy can be used such as this example compose file for Docker Swarm and this example Docker Compose file.
These are the labels on the containers are scanned for discovery and auto-scaling:
mc-router.host: Used to configure the hostname the Minecraft clients would use to connect to the server. The container/service endpoint will be used as the routed backend. You can use more than one hostname by splitting it with a comma or newline. Whitespace around commas is automatically trimmed. For example: "host1.com,host2.com", "host1.com, host2.com", or "host1.com\nhost2.com".mc-router.port: This value must be set to the port the Minecraft server is listening on. The default value is 25565.mc-router.default: Set this to a truthy value to make this server the default backend. Please note that mc-router.host is still required to be set.mc-router.network: Specify the network you are using for the router if multiple are present in the container/service. You can either use the network ID, it's full name or an alias. For Docker containers, -docker-network sets a default for all containers without this label.mc-router.auto-scale-up: Per-container/service override to enable/disable auto scale up for Docker/Swarm. When true (or left unspecified and the global -auto-scale-up flag is enabled), mc-router will start the container or scale up the Swarm service when a client connects.mc-router.auto-scale-down: Per-container/service override to enable/disable auto scale down for Docker/Swarm. When true (or left unspecified and the global -auto-scale-down flag is enabled), mc-router will stop the container or scale down the Swarm service to 0 after it has been idle.mc-router.auto-scale-asleep-motd: Per-container/service override for MOTD to show when scaled to zero. If empty or not set the host will appear unresponsive.mc-router.auto-scale-loading-motd: Per-container/service override for MOTD to show while waking up. Supports replacing the {duration} token with the remaining Swarm restart delay if the task is waiting to retry. If empty or not set, the global -auto-scale-loading-motd value is used.mc-router.auto-scale-wait-timeout: Configure the maximum duration the router waits for the container or Swarm task to become reachable after scaling up (e.g. "5m" or "300s"). Defaults to 60s. Note: Since the Minecraft Java client has a strict connection timeout of 30 seconds, configuring this value above 30s is not recommended for player join connections.mc-router.auto-scale-restart-delay-motd: MOTD to show while the service is in a temporary restart delay (e.g. "Server failed to start. Retrying in {duration}."). Supports the countdown token, which dynamically updates.To use scale-to-zero with Docker containers:
Start mc-router with Docker discovery and scaling enabled, for example:
docker run --rm \
-p 25565:25565 \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
itzg/mc-router \
-in-docker -auto-scale-up -auto-scale-down -auto-scale-down-after=10m
Label each Minecraft container with at least mc-router.host. You can also set per-container autoscale overrides using mc-router.auto-scale-up and mc-router.auto-scale-down labels.
For usage with docker compose refer to the examples/docker-autoscale/compose.yml or examples/docker-autoscale/compose-minimal.yml examples.
Behavior:
-auto-scale-loading-motd).-auto-scale-down-after), mc-router gracefully stops the container.-auto-scale-wake-kick-message set, a login to a stopped or still waking container is answered right away with a disconnect screen showing that message, while the container keeps starting in the background. Without it, the login is held until the container is reachable, which can exceed the Java client's ~30 second timeout. A server woken this way is scheduled to scale down like any other, even if the player never comes back.[!NOTE] Native Swarm service scaling via
-auto-scale-up/-auto-scale-downis supported in both VIP (Virtual IP) and DNSRR (DNS Round-Robin) modes. Bypassing the VIP (by routing directly to the task container IP) is automatically enabled whenreplicas == 1to prevent VIP routing delay. Note that DNSRR mode has not been actively tested.
Refer to this example compose file to see how to
configure two different Minecraft servers and a mc-router instance for use with Docker service discovery.
Refer to this example compose file to see how to
configure two different Minecraft servers and a mc-router instance for use with Docker Swarm service discovery.
Refer to the section above for information on how to configure the Docker socket access.
For statically-configured routes (those defined via -mapping, -default, or the routes config file
rather than discovered from Docker/Kubernetes), auto scaling can be driven by a webhook instead of mc-router managing the
backend directly. This is useful when a separate process owns the scaling — for example a control-plane container that
starts/stops the backend via its own API.
Compared to running a local command, the webhook approach keeps the scaling privilege outside the router process: mc-router needs no Docker socket, no Kubernetes credentials, and no shell, so it can run as a minimal/distroless, least-privilege container. The scaling authority lives in a separately-secured receiver.
Configure a single scaler endpoint; the request body's action field distinguishes scale-up from scale-down:
mc-router \
-default backend:25565 \
-auto-scale-webhook-url http://scaler:8080/scale \
-auto-scale-down-after 10m \
-auto-scale-webhook-headers "Authorization=Bearer <token>"
{"action":"up"} payload, then waits (up to
-auto-scale-webhook-wake-timeout, default 60s) for the backend to become reachable before proxying.-auto-scale-down-after), mc-router POSTs an
{"action":"down"} payload.-auto-scale-up/-auto-scale-down flags are not required for webhook scaling.The request body is JSON. serverAddress is the route the client requested (empty for the default route); backend is the
configured host:port. Both come from mc-router's own configuration — a client can only select an existing route, never
influence the outbound request:
{
"action": "up",
"serverAddress": "survival.example.com",
"backend": "10.0.0.5:25565"
}
A non-2xx response (or a transport error) is treated as a failure: a failed scale-up aborts the connection, and a failed
scale-down is retried the next time the idle timer elapses. The asleep/loading MOTDs (-auto-scale-asleep-motd,
-auto-scale-loading-motd) work the same as for Docker/Kubernetes.
The scale-up response may optionally return the backend's current address as JSON, which overrides the configured
backend for that wake:
{ "backend": "10.0.0.5:25565" }
This is useful when the backend's address is dynamic — for example a container whose IP changes on every start. The
receiver (which just started it) knows the live address and reports it, so mc-router connects there directly instead of
re-resolving the configured name against a possibly-stale DNS cache. The body is optional and lenient: an empty body, a
non-JSON body, or JSON without a backend field all leave the configured backend in effect. The scale-down response
body is ignored.
For a runnable local example — a small webhook receiver that starts/stops an itzg/minecraft-server container via Apple's
container CLI — see docs/webhook-scaler.
The routing configuration allows routing via a config file rather than a command.
You need to set -routes-config or ROUTES_CONFIG env variable.
The following shows a JSON file for routes config, where default-server can also be null or omitted:
{
"default-server": "vanilla:25565",
"mappings": {
"vanilla.example.com": "vanilla:25565",
"forge.example.com": "forge:25565"
}
}
Sending a SIGHUP signal will cause mc-router to reload the routes config from disk. The file can also be watched for changes by setting -routes-config-watch or the env variable ROUTES_CONFIG_WATCH to "true".
When a global webhook auto scaler is configured, it is attached to every route loaded from this file; the requested server address is sent in each scale request so the receiver can tell the routes apart.
The allow/deny list configuration allows limiting which players can scale up servers when using the -auto-scale-up option (AUTO_SCALE_UP env variable) and which players can cancel an active down scaler when using the -auto-scale-down option (AUTO_SCALE_DOWN env variable). Global allow/deny lists can be configured that apply to all backend servers, but server-specific lists can be added as well. There are a few important things to note about the configuration:
mc-router process will not automatically pick up changes to the config. If updates to the config are made, the router must be restarted.uuid or name. Both will be checked if specified, but otherwise a uuid will take priority over a name.An example configuration might look something like:
{
"global": {
"denylist": [
{"uuid": "<some player's uuid>", "name": "<some player's name>"}
]
},
"servers": {
"my.server.domain": {
"allowlist": [
{"uuid": "<some player's uuid>"}
]
},
"my.other-server.domain": {
"denylist": [
{"uuid": "<some player's uuid>"}
]
}
}
}
In the example, players in the my.server.domain allowlist will be able to scale up my.server.domain. Players in the global denylist and the my.other-server.domain denylist will not be able to scale up my.other-server.domain. Any servers not listed in the config will also be affected by the global allowlist. Note that if a global allowlist is specified, no denylists will have any effect as that global allowlist will affect all servers.
For more information on the allow/deny list configuration, see the json schema.
When running mc-router as a Kubernetes Pod and you pass the --in-kube-cluster command-line argument, then it will automatically watch for any services annotated with
mc-router.itzg.me/externalServerName : The value of the annotation will be registered as the external hostname Minecraft clients would used to connect to the routed service. The service is used as the routed backend. You can use more hostnames by splitting them with comma or newline. Whitespace around commas is automatically trimmed. For example: "host1.com,host2.com", "host1.com, host2.com", or multi-line values.mc-router.itzg.me/defaultServer : When set to "true", the service is used as the default if no other externalServiceName annotations applies.mc-router.itzg.me/proxyServerName : When using a proxy server like Velocity or BungeeCord, this annotation specifies the proxy's address to route traffic to. The Service endpoint is still used for auto-scaling operations, allowing mc-router to scale the backend StatefulSet while routing client connections to the proxy. See Using with Velocity/BungeeCord proxies for details.By default, the router will watch all namespaces for those services; however, a specific namespace can be specified using the KUBE_NAMESPACE environment variable. The pod's own namespace could be set using:
- name: KUBE_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
For example, start mc-router's container spec with
image: itzg/mc-router
name: mc-router
args: ["--in-kube-cluster"]
and configure the backend minecraft server's service with the annotation:
apiVersion: v1
kind: Service
metadata:
name: mc-forge
annotations:
"mc-router.itzg.me/externalServerName": "external.host.name"
you can use multiple host names:
apiVersion: v1
kind: Service
metadata:
name: mc-forge
annotations:
"mc-router.itzg.me/externalServerName": "external.host.name,other.host.name"
or with newlines and optional whitespace:
apiVersion: v1
kind: Service
metadata:
name: mc-forge
annotations:
"mc-router.itzg.me/externalServerName": |
external.host.name
other.host.name
The Role or ClusterRole bound to the service account should have the rules:
rules:
- apiGroups: [""]
resources: ["services"]
verbs: ["watch","list"]
and if using StatefulSet auto-scaling additionally
- apiGroups: ["apps"]
resources: ["statefulsets"]
verbs: ["watch","list","patch"]
- apiGroups: ["apps"]
resources: ["statefulsets/scale"]
verbs: ["get"]
[!NOTE] The
patchverb is preferred for scaling operations as it provides atomic updates and prevents concurrency conflicts. For backward compatibility, mc-router will automatically fall back to usingget+updateifpatchis not permitted, but this may result in occasional scaling conflicts in high-traffic scenarios.
To detrmine the endpoint mc-router will pick the host from spec.clusterIP by default, if the service is of type ExtenalName it will use spec.externalName instead.
For the port it will look in spec.ports for a port named mc-router, if not present minecraft or, if neither port names exist, it will use default minecraft port value 25565.
mc-router service that exposes a node port 25565--in-kube-cluster in the mc-router container arguments"mc-router.itzg.me/externalServerName" annotation that declares their external server name(s)kubectl apply -f https://raw.githubusercontent.com/itzg/mc-router/main/docs/k8s-example-auto.yaml
mc-stable and mc-snapshot--service-node-port-range=25000-32767
to /etc/kubernetes/manifests/kube-apiserver.yamlThe -auto-scale-up flag argument makes the router "wake up" any stopped backend servers by changing replicas: 0 to replicas: 1. The -auto-scale-down flag argument makes the router shut down any running backend servers with no active connections by changing replicas: 1 to replicas: 0. The scale down will occur after a configurable (using the -auto-scale-down-after argument) waiting period, such as 10m (10 minutes), 2h (2 hours), etc. If any players connect to the server during this period the scale down will be canceled. It is recommended to set this value high enough so a temporary player disconnect will not immediately shut down the server (1m or higher).
Both options require using kind: StatefulSet instead of kind: Service for the Minecraft backend servers.
They also require the ClusterRole to permit patch for statefulsets,
e.g. like this (or some equivalent more fine-grained one to only watch/list services+statefulsets, and patch statefulsets):
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: services-watcher
rules:
- apiGroups: [""]
resources: ["services"]
verbs: ["watch","list"]
- apiGroups: ["apps"]
resources: ["statefulsets"]
verbs: ["watch","list","patch"]
- apiGroups: ["apps"]
resources: ["statefulsets/scale"]
verbs: ["get"]
[!NOTE] The
patchverb is preferred for scaling operations as it provides atomic updates and prevents concurrency conflicts. For backward compatibility, mc-router will automatically fall back to usingget+updateifpatchis not permitted, but this may result in occasional scaling conflicts in high-traffic scenarios.
Make sure to set StatefulSet.metadata.name and StatefulSet.spec.serviceName to the same value;
otherwise, autoscaling will not trigger:
apiVersion: v1
kind: Service
metadata:
name: mc-forge
annotations:
"mc-router.itzg.me/defaultServer": "true"
"mc-router.itzg.me/externalServerName": "external.host.name"
spec:
type: ClusterIP
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mc-forge
spec:
serviceName: mc-forge
selector:
matchLabels:
app: mc-forge
template:
metadata:
labels:
app: mc-forge
spec:
containers:
- name: mc
You can also opt-out of auto-scaling per server by setting the following annotations on the Service object:
mc-router.itzg.me/autoScaleUp=falsemc-router.itzg.me/autoScaleDown=falseExample server with auto-scaling disabled explicitly:
apiVersion: v1
kind: Service
metadata:
name: mc-forge
annotations:
"mc-router.itzg.me/externalServerName": "external.host.name"
"mc-router.itzg.me/autoScaleUp": "false"
"mc-router.itzg.me/autoScaleDown": "false"
To override the MOTD shown when the server is scaled down or scaling up, you can use the following annotations on the Service object:
mc-router.itzg.me/autoScaleAsleepMOTDmc-router.itzg.me/autoScaleLoadingMOTDYou can also customize how long the router will wait for a scaling backend to become reachable (default: 60s):
mc-router.itzg.me/autoScaleWaitTimeout (e.g. 2m, 30s). Note: Since the Minecraft Java client has a strict connection timeout of 30 seconds, configuring this value above 30s is not recommended for player join connections.Example server with custom MOTD and timeout:
apiVersion: v1
kind: Service
metadata:
name: mc-forge
annotations:
"mc-router.itzg.me/externalServerName": "external.host.name"
"mc-router.itzg.me/autoScaleAsleepMOTD": "Server is sleeping"
"mc-router.itzg.me/autoScaleLoadingMOTD": "Server is loading"
"mc-router.itzg.me/autoScaleWaitTimeout": "90s"
When using a proxy server like Velocity or BungeeCord, you can use the mc-router.itzg.me/proxyServerName annotation to route client connections to the proxy while still allowing mc-router to auto-scale the backend StatefulSet. This is useful when you want to:
Example configuration:
# Velocity/BungeeCord proxy service (always running)
apiVersion: v1
kind: Service
metadata:
name: velocity-proxy
spec:
selector:
app: velocity
ports:
- name: minecraft
port: 25577
---
# Backend Minecraft server with auto-scaling
apiVersion: v1
kind: Service
metadata:
name: mc-survival
annotations:
# External hostname that clients connect to
"mc-router.itzg.me/externalServerName": "survival.example.com"
# Route traffic to the proxy instead of directly to this service
"mc-router.itzg.me/proxyServerName": "velocity-proxy:25577"
spec:
selector:
app: mc-survival
ports:
- name: minecraft
port: 25565
---
# Backend StatefulSet that can be scaled to zero
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mc-survival
spec:
serviceName: mc-survival
replicas: 0 # Can be scaled from 0 to 1 automatically
selector:
matchLabels:
app: mc-survival
template:
metadata:
labels:
app: mc-survival
spec:
containers:
- name: mc
image: itzg/minecraft-server
# ... container configuration
In this configuration:
survival.example.com are routed to velocity-proxy:25577mc-survival StatefulSet from 0 to 1 replicas[!NOTE] The proxy server must be configured to connect to the backend server at
mc-survival:25565(the Service endpoint) and handle the case where the backend may not be available immediately during scale-up.
First and foremost, enable debug logs on mc-router by setting the DEBUG environment variable to "true". With that, the logs will be fairly verbose with information about incoming connections, handshake processing, backend service discovery, and backend connection establishment and teardown.
If backend service discovery doesn't seem to be happening, then double-check the service annotations, described above, are applied to the backend Service objects and that those are selecting a Minecraft service deployment each.
If the client reports "Connection refused" check:
Service type is configured as NodePort (or LoadBalancer if applicable)kubectl describe service mc-router for the next few steps
NodePort value, usually in the 30000-32767 range.Endpoints field contains at least one entry referencing the cluster IP address of the mc-router Pod. If not, check that the Selector matches the labels on the mc-router Pod.GET /routes (with Accept: application/json)
Retrieves the currently configured routes
{
"serverAddress": {
"backend": "HOST:PORT", // The address the client is routed to
"scalingTarget": "HOST:PORT" // The address of the actual minecraft server (will differ from backend when a proxy is configured)
}
}
POST /routes (with Content-Type: application/json)
Registers a route given a JSON body structured like:
{
"serverAddress": "CLIENT REQUESTED SERVER ADDRESS",
"backend": "HOST:PORT"
}
POST /defaultRoute (with Content-Type: application/json)
Registers a default route to the given backend. JSON body is structured as:
{
"backend": "HOST:PORT"
}
DELETE /routes/{serverAddress}
Deletes an existing route for the given serverAddress
mc-router has built-in support to run as an ngrok agent. To enable this support, pass an ngrok authtoken to the command-line argument or environment variable, shown above.
Create/access an ngrok account and allocate an agent authtoken from the dashboard.
In a new directory, create a file called .env with the allocated token
NGROK_TOKEN=...
In the same directory, create the following compose file:
version: "3.8"
services:
mc:
image: itzg/minecraft-server
environment:
EULA: true
volumes:
- mc-data:/data
# No port mapping since mc-router connects over compose network
router:
image: itzg/mc-router
environment:
DEFAULT: mc:25565
NGROK_TOKEN: ${NGROK_TOKEN}
# No port mapping needed since it routes through ngrok tunnel
volumes:
mc-data: {}
Start the compose project:
docker compose up -d
Grab the mc-router logs using:
docker compose logs router
From those logs, locate the ngrokUrl parameter from the "Listening" info log message, such as tcp://8.tcp.ngrok.io:99999.
In the Minecraft client, the server address will be the part after the "tcp://" prefix, such as 8.tcp.ngrok.io:99999.
Refer to the usage section above for -webhook-* argument descriptions.
The following are sample payloads for the connect webhook events.
{
"event": "connect",
"timestamp": "2025-04-20T22:26:30.2568775-05:00",
"status": "success",
"client": {
"host": "127.0.0.1",
"port": 56860
},
"server": "localhost",
"player": {
"name": "itzg",
"uuid": "5cddfd26-fc86-4981-b52e-c42bb10bfdef"
},
"backend": "localhost:25566"
}
[!NOTE]
clientrefers to the machine where the Minecraft client is connecting from and is conveyed separately from theplayerstarting a session. As seen below, the player information may not always be present, such as when the client is pinging the server list.
[!NOTE] The
playeris absent in this payload since the Minecraft client does not send player information in the server ping request.
{
"event": "connect",
"timestamp": "2025-04-20T22:26:30.2568775-05:00",
"status": "success",
"client": {
"host": "127.0.0.1",
"port": 56396
},
"server": "localhost",
"backend": "localhost:25566"
}
In this the status is "missing-backend" since the requested server invalid.example.com does not have a configured/discovered backend entry.
{
"event": "connect",
"timestamp": "2025-04-20T22:26:30.2568775-05:00",
"status": "missing-backend",
"client": {
"host": "127.0.0.1",
"port": 56891
},
"server": "invalid.example.com",
"player": {
"name": "itzg",
"uuid": "5cddfd26-fc86-4981-b52e-c42bb10bfdef"
},
"error": "No backend found"
}
In this case the status is "failed-backend-connection" indicating that a backend server was located but a connection could not be established from mc-router.
{
"event": "connect",
"timestamp": "2025-04-20T22:26:30.2568775-05:00",
"status": "failed-backend-connection",
"client": {
"host": "127.0.0.1",
"port": 56905
},
"server": "localhost",
"player": {
"name": "itzg",
"uuid": "5cddfd26-fc86-4981-b52e-c42bb10bfdef"
},
"backend": "localhost:25566",
"error": "dial tcp [::1]:25566: connectex: No connection could be made because the target machine actively refused it."
}
[!IMPORTANT] This event type is disabled by default. Enable with
-webhook-events route-added
{
"event": "route-added",
"timestamp": "2025-04-20T22:26:30.2568775-05:00",
"server": "localhost",
"backend": "localhost:25566"
}
[!IMPORTANT] This event type is disabled by default. Enable with
-webhook-events route-removed
{
"event": "route-removed",
"timestamp": "2025-04-20T22:26:30.2568775-05:00",
"server": "localhost"
}
[!IMPORTANT] This event type is disabled by default. Enable with
-webhook-events default-route-set
{
"event": "default-route-set",
"timestamp": "2025-04-20T22:26:30.2568775-05:00",
"backend": "localhost:25566"
}
[!IMPORTANT] This event type is disabled by default. Enable with
-webhook-events default-route-removed
{
"event": "default-route-removed",
"timestamp": "2025-04-20T22:26:30.2568775-05:00"
}
Refer to DEVELOPMENT.md
{duration}mc-router.auto-scale-failed-motd: MOTD to show if the container/service fails to start permanently or Swarm exhausts its restart policy (e.g. "Server crashed and stopped retrying."). Does not support the countdown token.