PACKYARDStart your server

Permanent Minecraft server addresses with a WireGuard edge and mc-router

Published

The problem

On most game servers, the address players save is the machine's own IP and port. That works until the server has to move: the machine is being retired, it's full, or the server needs a bigger one. Then the address changes, and everyone who saved it, plus every server list, Discord pin and bookmark, points at nothing.

We wanted the address to belong to the server, not the machine. Then a server can move between machines and nobody has to update anything.

The setup

Players never connect to the machine their server runs on. They connect to one edge: a small OVH VPS in Hillsboro, Oregon. The game servers run on larger shared-vCPU VPSes at Contabo in Seattle. Each node has a WireGuard tunnel to the edge. Round trip from edge to node is 5.3 / 5.6 / 5.8 ms (min/avg/max, 20 pings).

player ── TCP 25565 ──▶ edge (OVH Hillsboro)
                         mc-router: read hostname from the handshake
                         └── WireGuard ──▶ node (Contabo Seattle)
                                            └── server container, bound to the node's tunnel address only

Minecraft Java makes this easy. The first packet a client sends, the handshake, contains the hostname the player typed. mc-router reads that hostname and proxies the TCP connection to whatever backend it maps to. So every server gets a name like yourserver.play.packyard.ca on the default port, one wildcard DNS record (*.play.packyard.ca) points at the edge, and there is no per-server DNS or SRV record to manage. A hostname with no route gets its connection closed.

Our orchestrator owns the mapping and pushes it to mc-router's HTTP API, which listens only on the edge's localhost:

POST /routes
{"serverAddress": "yourserver.play.packyard.ca", "backend": "<node tunnel address>:<port>"}

Moving a server means changing that backend. The name players saved doesn't change.

On the node, game ports are published by Docker on the WireGuard address only. That matters because Docker's published ports skip ufw: if a container port is published on all interfaces, the firewall doesn't save you. The node's firewall takes WireGuard and SSH from a short allowlist and nothing else.

Valheim, Terraria and Project Zomboid are harder, because their protocols carry no hostname, so there is nothing to route on. Each of those servers gets its own port on the edge instead, and nftables on the edge DNATs it over WireGuard to the same port on the node. Ports come from one pool for the whole fleet, not per node, so a server keeps its port when it moves. The address is name.games.packyard.ca:port. That's a separate wildcard, so UDP games can move to a different edge IP later without anyone's address changing.

Two things came with that:

What this does and doesn't cover: all game traffic and Steam listings go through the edge. The control panel (Pelican) talks to its daemon on each node through an outbound Cloudflare Tunnel, so the browser console doesn't connect to the node either. SFTP goes through the edge too: you connect to the edge, never to the node. None of that makes a node's IP secret. It's in our provider's records, and the node still makes some outbound connections of its own, like OS updates. What we can say is that nothing we hand to a customer contains it.

What broke

The tradeoffs

Closing

This runs Packyard, a small game host at $1.50 per GB of RAM a month ($5 minimum): packyard.ca

Example configs: https://github.com/Himtome1/permanent-address-edge

Update 27 October 2026: customer SFTP is live, routed through the edge.

Update 31 October 2026: we added a second node on 27 October. No server has moved between them yet.