Skip to content

Instantly share code, notes, and snippets.

@lightdiscord
Last active August 8, 2026 07:19
Show Gist options
  • Select an option

  • Save lightdiscord/cd26208b389a00740e0c84f06b8eef15 to your computer and use it in GitHub Desktop.

Select an option

Save lightdiscord/cd26208b389a00740e0c84f06b8eef15 to your computer and use it in GitHub Desktop.
Proof of concept to use HAProxy to route traffic to relevant Minecraft server (not thoroughly tested)

Proof of concept to use HAProxy to route traffic to relevant Minecraft server (not thoroughly tested)

  • Currently using static IP addresses in Map file, if using DNS name they need to be resolved
one.minecraft.local 172.19.0.3
three.minecraft.local 172.19.0.4
services:
haproxy:
image: haproxy:3.4
configs:
- source: haproxy_config
target: /usr/local/etc/haproxy/haproxy.cfg
volumes:
- ./minecraft.lua:/etc/haproxy/minecraft.lua
- ./backends.map:/data/backends.map
ports:
- "25565:25565"
minecraft_one:
build:
dockerfile: minecraft.Dockerfile
configs:
- source: minecraft_one_config
target: /app/server.properties
minecraft_two:
build:
dockerfile: minecraft.Dockerfile
configs:
- source: minecraft_two_config
target: /app/server.properties
minecraft_three:
build:
dockerfile: minecraft.Dockerfile
configs:
- source: minecraft_three_config
target: /app/server.properties
configs:
haproxy_config:
content: |
global
lua-load /etc/haproxy/minecraft.lua
frontend minecraft_proxy
bind :25565
mode tcp
log global
tcp-request inspect-delay 3s
tcp-request content lua.parse_mc_handshake if { req_len gt 5 }
tcp-request content set-var(txn.target_ip) var(txn.mc_server_name),lower,map(/data/backends.map)
use_backend dynamic_minecraft if { var(txn.target_ip) -m found }
backend dynamic_minecraft
mode tcp
tcp-request content set-dst var(txn.target_ip)
tcp-request content set-dst-port int(25565)
server dynamic_srv 0.0.0.0:0
minecraft_one_config:
content: |
accepts-transfers=false
allow-flight=false
broadcast-console-to-ops=true
broadcast-rcon-to-ops=true
bug-report-link=
chat-spam-threshold-seconds=10
command-spam-threshold-seconds=10
difficulty=easy
enable-code-of-conduct=false
enable-jmx-monitoring=false
enable-query=false
enable-rcon=false
enable-status=true
enforce-secure-profile=true
enforce-whitelist=false
entity-broadcast-range-percentage=100
force-gamemode=false
function-permission-level=2
gamemode=survival
generate-structures=true
generator-settings={}
hardcore=false
hide-online-players=false
initial-disabled-packs=
initial-enabled-packs=vanilla
level-name=world
level-seed=
level-type=minecraft\:normal
log-ips=true
management-server-allowed-origins=
management-server-enabled=false
management-server-host=localhost
management-server-port=0
management-server-secret=d9MEMEdjywnNDKvzmNkbrJGaiSSzJ93rimCyq2gK
management-server-tls-enabled=true
management-server-tls-keystore=
management-server-tls-keystore-password=
max-chained-neighbor-updates=1000000
max-players=20
max-tick-time=60000
max-world-size=29999984
motd=Minecraft Server One
network-compression-threshold=256
online-mode=true
op-permission-level=4
pause-when-empty-seconds=60
player-idle-timeout=0
prevent-proxy-connections=false
query.port=25565
rate-limit=0
rcon.password=
rcon.port=25575
region-file-compression=deflate
require-resource-pack=false
resource-pack=
resource-pack-id=
resource-pack-prompt=
resource-pack-sha1=
server-ip=
server-port=25565
simulation-distance=10
spawn-protection=16
status-heartbeat-interval=0
sync-chunk-writes=true
text-filtering-config=
text-filtering-version=0
use-native-transport=true
view-distance=10
white-list=false
minecraft_two_config:
content: |
accepts-transfers=false
allow-flight=false
broadcast-console-to-ops=true
broadcast-rcon-to-ops=true
bug-report-link=
chat-spam-threshold-seconds=10
command-spam-threshold-seconds=10
difficulty=easy
enable-code-of-conduct=false
enable-jmx-monitoring=false
enable-query=false
enable-rcon=false
enable-status=true
enforce-secure-profile=true
enforce-whitelist=false
entity-broadcast-range-percentage=100
force-gamemode=false
function-permission-level=2
gamemode=survival
generate-structures=true
generator-settings={}
hardcore=false
hide-online-players=false
initial-disabled-packs=
initial-enabled-packs=vanilla
level-name=world
level-seed=
level-type=minecraft\:normal
log-ips=true
management-server-allowed-origins=
management-server-enabled=false
management-server-host=localhost
management-server-port=0
management-server-secret=d9MEMEdjywnNDKvzmNkbrJGaiSSzJ93rimCyq2gK
management-server-tls-enabled=true
management-server-tls-keystore=
management-server-tls-keystore-password=
max-chained-neighbor-updates=1000000
max-players=20
max-tick-time=60000
max-world-size=29999984
motd=Minecraft Server Two
network-compression-threshold=256
online-mode=true
op-permission-level=4
pause-when-empty-seconds=60
player-idle-timeout=0
prevent-proxy-connections=false
query.port=25565
rate-limit=0
rcon.password=
rcon.port=25575
region-file-compression=deflate
require-resource-pack=false
resource-pack=
resource-pack-id=
resource-pack-prompt=
resource-pack-sha1=
server-ip=
server-port=25565
simulation-distance=10
spawn-protection=16
status-heartbeat-interval=0
sync-chunk-writes=true
text-filtering-config=
text-filtering-version=0
use-native-transport=true
view-distance=10
white-list=false
minecraft_three_config:
content: |
accepts-transfers=false
allow-flight=false
broadcast-console-to-ops=true
broadcast-rcon-to-ops=true
bug-report-link=
chat-spam-threshold-seconds=10
command-spam-threshold-seconds=10
difficulty=easy
enable-code-of-conduct=false
enable-jmx-monitoring=false
enable-query=false
enable-rcon=false
enable-status=true
enforce-secure-profile=true
enforce-whitelist=false
entity-broadcast-range-percentage=100
force-gamemode=false
function-permission-level=2
gamemode=survival
generate-structures=true
generator-settings={}
hardcore=false
hide-online-players=false
initial-disabled-packs=
initial-enabled-packs=vanilla
level-name=world
level-seed=
level-type=minecraft\:normal
log-ips=true
management-server-allowed-origins=
management-server-enabled=false
management-server-host=localhost
management-server-port=0
management-server-secret=d9MEMEdjywnNDKvzmNkbrJGaiSSzJ93rimCyq2gK
management-server-tls-enabled=true
management-server-tls-keystore=
management-server-tls-keystore-password=
max-chained-neighbor-updates=1000000
max-players=20
max-tick-time=60000
max-world-size=29999984
motd=Minecraft Server Three
network-compression-threshold=256
online-mode=true
op-permission-level=4
pause-when-empty-seconds=60
player-idle-timeout=0
prevent-proxy-connections=false
query.port=25565
rate-limit=0
rcon.password=
rcon.port=25575
region-file-compression=deflate
require-resource-pack=false
resource-pack=
resource-pack-id=
resource-pack-prompt=
resource-pack-sha1=
server-ip=
server-port=25565
simulation-distance=10
spawn-protection=16
status-heartbeat-interval=0
sync-chunk-writes=true
text-filtering-config=
text-filtering-version=0
use-native-transport=true
view-distance=10
white-list=false
FROM openjdk:28-ea
WORKDIR /app
ADD https://piston-data.mojang.com/v1/objects/823e2250d24b3ddac457a60c92a6a941943fcd6a/server.jar /app/server.jar
RUN java -Xmx4G -Xms4G -jar /app/server.jar nogui
RUN echo eula=true > /app/eula.txt
CMD ["java", "-Xmx4G", "-Xms4G", "-jar", "/app/server.jar", "nogui"]
local function read_varint(payload, offset)
local value = 0
local bit_shift = 0
local bytes_read = 0
while true do
if offset + bytes_read >= #payload then return nil, nil end
local b = string.byte(payload, offset + bytes_read + 1)
value = value + ((b & 0x7F) * (2 ^ bit_shift))
bit_shift = bit_shift + 7
bytes_read = bytes_read + 1
if (b & 0x80) == 0 then break end
if bytes_read >= 5 then return nil, nil end
end
return value, bytes_read
end
local function extract_mc_hostname(txn)
local payload = txn.req:data()
if not payload or #payload < 5 then
return
end
local current_offset = 0
local packet_len, read1 = read_varint(payload, current_offset)
if not packet_len then return end
current_offset = current_offset + read1
if #payload < (packet_len + read1) then
return
end
local packet_id, read2 = read_varint(payload, current_offset)
if not packet_id or packet_id ~= 0x00 then return end
current_offset = current_offset + read2
local proto_version, read3 = read_varint(payload, current_offset)
if not proto_version then return end
current_offset = current_offset + read3
local string_len, read4 = read_varint(payload, current_offset)
if not string_len or string_len <= 0 then return end
current_offset = current_offset + read4
if current_offset + string_len > #payload then return end
local hostname = string.sub(payload, current_offset + 1, current_offset + string_len)
txn:set_var("txn.mc_server_name", hostname)
end
core.register_action("parse_mc_handshake", { "tcp-req" }, extract_mc_hostname)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment