Forwarding traffic to a game server running on Pterodactyl

Unsure if this is the correct place for issues like this, however I’ve been trying to set up a game server running on pterodactyl over the last week and I’m on the very last hurdle.

I’ve successfully set up a game server running on my local network, and users are able to connect to it via my public IP + port. My end goal is to be able to connect with just a domain such as minecraft.mydomain.com, however I understand that not all games support SRV records so I may still need a port. I’m currently just testing on Minecraft Java edition, as this only uses (from my understanding) one TCP port, which in my case is set to 27000.

Anyway, here is my main issue. I am able to connect to my server on my LAN, and users are able to connect via public IP + port as stated above. I have also port forwarded to the server running traefik (which is running the pterodactyl panel but not the game server itself), so traefik is somehow directing the correct traffic to my game server when connecting via my public IP, but not when connecting via Minecraft.mydomain.com:27000.

My domain is 100% pointing towards my public IP, so I’m unsure why this is happening. I’m doing all this with docker compose, and I have one particular error in traefik logs that may solve my issue, however everything I’ve found online has not helped so far.

Here’s the error in my traefik logs:
2024-07-21T10:11:51+01:00 DBG github.com/traefik/traefik/v3/pkg/tcp/proxy.go:104 > Error while terminating TCP connection error="close tcp 192.168.2.7:49364->192.168.1.104:27000: use of closed network connection"

The above appears every time my friend attempts to connect to the server. I’ll also attach my compose files for Traefik along with the file I’m using to try and direct traffic.

Traefik Compose File:

services:

Traefik 3 - Reverse Proxy

traefik:
container_name: traefik
image: traefik:3.0
security_opt:
- no-new-privileges:true
restart: unless-stopped
# profiles: ["core", "all"]
networks:
t3_proxy:
ipv4_address: 192.168.90.254 # You can specify a static IP
socket_proxy:
command: # CLI arguments
- --global.checkNewVersion=true
- --global.sendAnonymousUsage=true
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --entrypoints.traefik.address=:8080
- --entrypoints.websecure.http.tls=true
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entrypoints.web.http.redirections.entrypoint.scheme=https
- --entrypoints.web.http.redirections.entrypoint.permanent=true
- --api=true
- --api.dashboard=true
# - --api.insecure=true
# - --serversTransport.insecureSkipVerify=true
- --entrypoints.websecure.forwardedHeaders.trustedIPs=$CLOUDFLARE_IPS,$LOCAL_IPS
- --log=true
- --log.filePath=/logs/traefik.log
- --log.level=DEBUG # (Default: error) DEBUG, INFO, WARN, ERROR, FATAL, PANIC
- --accessLog=true
- --accessLog.filePath=/logs/access.log
- --accessLog.bufferingSize=100 # Configuring a buffer of 100 lines
- --accessLog.filters.statusCodes=204-299,400-499,500-599
- --providers.docker=true
# - --providers.docker.endpoint=unix:///var/run/docker.sock # Disable for Socket Proxy. Enable otherwise.
- --providers.docker.endpoint=tcp://socket-proxy:2375 # Enable for Socket Proxy. Disable otherwise.
- --providers.docker.exposedByDefault=false
- --providers.docker.network=t3_proxy
# - --providers.docker.swarmMode=false # Traefik v2 Swarm
# - --providers.swarm.endpoint=tcp://127.0.0.1:2377 # Traefik v3 Swarm
- --entrypoints.websecure.http.tls.options=tls-opts@file
# Add dns-cloudflare as default certresolver for all services. Also enables TLS and no need to specify on individual services
- --entrypoints.websecure.http.tls.certresolver=dns-cloudflare
- --entrypoints.websecure.http.tls.domains[0].main=$DOMAINNAME_1
- --entrypoints.websecure.http.tls.domains[0].sans=.$DOMAINNAME_1
- --entrypoints.websecure.http.tls.domains[1].main=$DOMAINNAME_2 # Pulls main cert for second domain
- --entrypoints.websecure.http.tls.domains[1].sans=
.$DOMAINNAME_2 # Pulls wildcard cert for second domain
- --providers.file.directory=/rules # Load dynamic configuration from one or more .toml or .yml files in a directory
- --providers.file.watch=true # Only works on top level files in the rules folder
# - --certificatesResolvers.dns-cloudflare.acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory # LetsEncrypt Staging Server - uncomment when testing
- --certificatesResolvers.dns-cloudflare.acme.storage=/acme.json
- --certificatesResolvers.dns-cloudflare.acme.dnsChallenge.provider=cloudflare
- --certificatesResolvers.dns-cloudflare.acme.dnsChallenge.resolvers=1.1.1.1:53,1.0.0.1:53
- --certificatesResolvers.dns-cloudflare.acme.dnsChallenge.delayBeforeCheck=90 # To delay DNS check and reduce LE hitrate

  # GAME SERVER CONFIG
  - --entryPoints.minecraftserverTCP.address=:27000/tcp
  
ports:
  - target: 80
    published: 80
    protocol: tcp
    mode: host
  - target: 443
    published: 443
    protocol: tcp
    mode: host
  # Game Server Ports
  - target: 27000
    published: 27000
    protocol: tcp
    mode: host



  # - target: 8080 # need to enable --api.insecure=true
  #  published: 8085
  #  protocol: tcp
  #  mode: host
volumes:
  - $DOCKERDIR/traefik3/rules/$HOSTNAME:/rules # Dynamic File Provider directory
  # - /var/run/docker.sock:/var/run/docker.sock:ro # Enable if not using Socket Proxy
  - $DOCKERDIR/traefik3/acme/acme.json:/acme.json # Certs File 
  - /home/william/docker/logs/HomeServer/traefik:/logs # Traefik logs
environment:
  - TZ=$TZ
  - CF_DNS_API_TOKEN_FILE=/run/secrets/cf_dns_api_token    
  - HTPASSWD_FILE=/run/secrets/basic_auth_credentials # HTTP Basic Auth Credentials
  - DOMAINNAME_1 # Passing the domain name to traefik container to be able to use the variable in rules.
  - DOMAINNAME_2 
secrets:
  - cf_dns_api_token
  - basic_auth_credentials
labels:
  - "traefik.enable=true"
  # HTTP Routers
  - "traefik.http.routers.traefik-rtr.entrypoints=websecure"
  - "traefik.http.routers.traefik-rtr.rule=Host(`traefik.$DOMAINNAME_1`)"
  # Services - API
  - "traefik.http.routers.traefik-rtr.service=api@internal"
  # Middlewares
  - "traefik.http.routers.traefik-rtr.middlewares=chain-oauth@file" # Adds Google Oauth Authentication

And finally, the dynamic configuration file:

tcp:
routers:
game_minecraft:
entryPoints:
- minecraftserverTCP
rule: "HostSNI(*)"
service: game_minecraft
tls: false
services:
game_minecraft:
loadBalancer:
servers:
- address: "192.168.1.104:27000"

Apologies for the huge post, there just seems to be a lack of others with the same config as me, and I’m not entirely ruling out that this could be an issue with pterodactyl, so please let me know if you would like to see my pterodactyl compose file too.

Any help is hugely appreciated, as I said this is the last hurdle for this and I would be extremely happy to get this working.

Did you ever find a fix??

Sadly I never found an exact solution and I'm going to go back to this at some point.

I discovered that my issue was proxying through cloudflare, as cloudflare cannot handle TCP/UDP traffic for game servers. They do offer a service to proxy this kind of traffic, but it's hugely expensive so I assume is enterprise use only.

I assume that somewhere out there is a service that'll proxy this kind of traffic so you don't have to expose your public IP, however I also found a few community threads that suggested you should just expose your IP anyway as that's the most direct and therefore the fastest connection to your server.

Personally, for now I am exposing my IP, it's not perfect but the general consensus seems to be that this is fine for game servers.

Hope this helps! Didn't think anyone would find this thread after a few months!

I've just started using Traefik to proxy my Minecraft server. I'm still in the process of getting a pterodactyl server running but I was able to successfully test proxying Minecraft through Traefik directly.
I read in a forum post somewhere that Minecraft traffic doesnt include HostSNI information so that rule wont work. Instead I used ClientIP to allow all IP's and that worked!
You could probably lock down the IP for security instead of using all IP's. But this was just a test for my network.

Snippet from my config.yaml:


tcp:
routers:
tcpmc_rtr:
entryPoints:
- "tcpmc"
rule: "ClientIP(0.0.0.0/0)"
service: "tcpmc_svc"

services:
tcpmc_svc:
loadBalancer:
servers:
- address: ":25565"

udp:
routers:
udpmc_rtr:
entryPoints:
- "udpmc"
service: "udpmc_svc"

services:
udpmc_svc:
loadBalancer:
servers:
- address: ":25565"

Use 3 backticks before and after code/config to preserve spacing in post, which is important for yaml files.

I’m just getting to this step with my set-up, so just throwing ideas out there. One of the videos I was following a few months ago while initially getting traefik, docker, authentik, and proxmox to all talk to eachother had mentioned cloudflare cache-ing and the free tier accounts.

You mentioned you had found the issue was narrowed down to your services are proxies through cloudflare. Cloudflare doesn’t allow cache-ing on their free tier. The YouTube-er mentioned they keep any of their services that need a rule to skip cache-ing with a specific prefix so they can make a single wildcard rule (Cloudflare only gives so many on the free tier). Something similar to “proxradarr.domain.tld”, so their wildcard rule involves no cache for anything prox***.domain.tld.

Anyways, if you get to it before I do, let me know if that was helpful. Otherwise, if I can try it out, I’ll let you know how it goes.
The guide I’m referring to is here.

It is almost correct! (Almost in that u underestimate how convenient traefik is, it does most the work for you)

But traefik routes the port for you to the application.. So remove mentions about ports! And add this in your docker compose for the server:

labels:

  • "traefik.enable=true"
  • "traefik.http.routers.minecraft.rule=Host(minecraft.domain.com)"
  • "traefik.http.routers.minecraft.entrypoints=web"

And in the bottom:

networks:

  • minecraft-net

#volumes: #this is for persistent storage, u probably dont need it.
#data_minecraft:

networks:
minecraft-net:
external: true

And for traefik's docker compose.yml i copied the hole ting and edited for your use, with cloudflared which u also need:

"
networks:
traefik-net: # for cloudflared and traefik
external: true
minecraft-net: # ← To reach the docker container with minecraft
external: true

services:
traefik:
image: traefik:v3
restart: unless-stopped
ports:

  • "443:443"
    environment:
  • CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}
    volumes:

- /var/run/docker.sock:/var/run/docker.sock:ro #u might need, probably not.

networks:

  • traefik-net
  • minecraft-net # ← So server can reach the reverse proxy

cloudflared: # To reach cloudflare
image: cloudflare/cloudflared:latest
restart: unless-stopped
CF_DNS_API_TOKEN=YOUR TOKEN FROM CLOUDFLARE
CF_TUNNEL_TOKEN=TOKEN FOR REVERSE PROXY TUNNEL
volumes:

  • ./cloudflared:/etc/cloudflared
    networks:
  • traefik-net
    "

U also need to set up the domain with *domain.com (wildcard if u havent so traefik can hand out subdomains on behalf of your domain)

Let me know if u cant get it to work!

Give this settings to an ai and then the settings u have, and ask it to make a setting similar to this that work for your docker image!(It may have som enviromental variables u also can put in the docker compose file to get it to work..

But traefik if u use subdomains, u can point each subdomain to a resource, and it doesnt use ports..

minecraft java isnt http, so http routers and those traefik.http.routers labels will never match it. thats why the label version does nothing. you need a tcp entrypoint and a tcp router with HostSNI(*), and since vanilla mc has no tls there is no sni to route on, so its one entrypoint per game server.

on traefik: --entrypoints.minecraft.address=:27000/tcp and publish 27000:27000 on the traefik container, plus the port forward on your router.

on the game server container: traefik.enable=true, traefik.tcp.routers.mc.rule=HostSNI(*), traefik.tcp.routers.mc.entrypoints=minecraft, traefik.tcp.services.mc.loadbalancer.server.port=25565, and make sure it sits on the same docker network as traefik.

the other thing biting you is cloudflare. the orange cloud only proxies http/https ports, 27000 is not one of them, so minecraft.mydomain.com:27000 through the proxy never reaches your box. set that record to dns only (grey cloud). a cloudflared tunnel doesnt carry plain tcp on the free plan either.

if you dont want people typing a port, add an SRV record (service minecraft, protocol tcp) pointing at the host and port, the java client honours it.