Running a game server at home is easy. Letting friends connect from outside your network without exposing your home IP or fighting your ISP's NAT is the actual hard part. Here are two approaches that cover almost every case, and a subtle bug that can make a tunnel look perfectly healthy while being completely broken.
For the vast majority of game servers — anything that listens on one TCP or UDP port — a tunneling service like playit.gg is the simplest option. Install a small agent on the same machine as the game server, claim a tunnel through their dashboard, and you get a public address that forwards straight to your local port. No port forwarding, no static IP needed, free tier covers most personal use.
host:port string into
a single field throws a generic, unhelpful "not correctly formatted" error that has nothing to do with the
server itself.
Some games need far more than one port — dozens of UDP ports for game-world sharding, a separate port for an internal message queue, etc. That doesn't fit the single-port tunnel model any tunneling service offers. For this, a cheap VPS running frp (Fast Reverse Proxy) works well — you get full control over exactly which ports are exposed.
If the game runs inside Docker on its own custom bridge network (common when a game ships with a sidecar
container, or you've put it behind its own internal network for isolation), running the tunnel client
directly on the host with localIP = 127.0.0.1 won't reach it — the game process is only
reachable via the container's own IP or Docker's internal DNS, from inside that network. The fix is
to run the tunnel client itself as another container joined to the same Docker network, and point it at the
game container by name (Docker's embedded DNS resolves container names automatically) instead of an IP:
frpc:
image: alpine:latest
container_name: frpc
restart: unless-stopped
networks:
- gamenet
volumes:
- /opt/frp:/opt/frp
entrypoint: ["/opt/frp/frpc", "-c", "/opt/frp/frpc.toml"]
Then in frpc.toml, set localIP to the game container's service name (e.g.
"enshrouded") instead of an IP address — it resolves correctly because both containers share the
same Docker network.
If people on your own network also want to connect, route them directly to the local server instead of round-tripping out through the internet and back in through the tunnel. Set up split-horizon DNS: public resolvers get the VPS's IP, but your local router/DNS returns the game server's LAN IP for the same hostname. Verify both directions actually resolve differently before trusting it.
localIP set to 127.0.0.1 — while the actual game server process bound to the
machine's real LAN IP, not localhost. One unrelated companion service happened to be configured correctly
(bound to localhost for real), which is exactly why that one tested fine and gave false confidence
the whole tunnel was healthy.
Lesson: don't treat "proxy registered" as proof of connectivity. Before trusting a tunnel
is actually working, check what the service you're tunneling to is actually bound to
(netstat -ulnp / ss -ulnp), and confirm your tunnel client's local target actually
matches that — don't just assume 127.0.0.1 because it's the common default. Ultimately, verify
with a real client connection from outside your network, not just clean-looking logs.
Getting a new port working through a VPS+frp setup means checking (and opening) the same thing in three separate places — missing any one of them looks identical from the client's side ("proxy registered, still can't connect"), so check them in this order:
frpc.toml on the game host — the client-side proxy definition (local IP/port,
remote port, protocol).frps.toml's allowPorts allowlist on the VPS — frp's server
rejects any proxy for a remote port not explicitly listed here, even though the client's control
connection (auth, login) succeeds fine. The failure only shows up per-proxy:
new proxy [myserver] type [udp] error: acquire port 25566 error: port not allowed
ufw, or equivalent) — a completely separate
layer from frp's own allowlist. Even with the first two correct, a default-deny firewall with no explicit
rule for the new port will silently drop every packet before it ever reaches frps. This one is
the sneakiest, because frps's own logs show the proxy registering successfully — the firewall
operates one layer below frp entirely, so frp has no idea traffic is being dropped:
ufw allow 25566/tcp comment 'my game server'
Confirm a port is actually reachable end-to-end with a raw connection test from outside, not just a
registration log line:
timeout 5 bash -c "echo > /dev/tcp/<vps-ip>/25566" && echo OPEN || echo CLOSED
Fix: only trust a "confirmed working" report if it was tested from genuinely outside your LAN — cellular data, a VPN, or asking someone off-network to try. Do this before decommissioning any old tunnel or fallback path, not after.
It's reasonable to leave an old tunneling agent stopped-but-installed as a fallback while you validate a
replacement. Once that replacement has been confirmed working (from genuinely outside the LAN, per above)
for long enough that you trust it, go back and actually uninstall the old agent rather than leaving it
disabled indefinitely — a disabled-but-present agent is one stray systemctl enable or container
restart away from silently coming back and fighting with your new setup for the same local port.