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.

1 Like

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.