Basalt
Getting started

Run a tunnel server

Stand up the frp server your instances tunnel through.

Basalt tunnels work by having each instance dial outbound to a tunnel server, which accepts players on a public port and forwards them back. Basalt does not host that server for you: you run it, on a machine you control, which is what makes tunneling work without paying anyone or handing a third party your traffic.

You only need one, no matter how many nodes or instances you have. It is a small, stateless service: the whole thing is one container and a config file.

Do you need this?

Only if your game nodes aren't publicly reachable. If you control the router and can forward ports, or your node already has a public IP, players can connect directly and you can skip this entirely.

What the machine needs

  • A public IP. This is the one piece of your setup that has to be reachable from the internet.
  • Docker with the Compose plugin.
  • Very little else. It forwards packets; it doesn't run game servers.

A small VPS is the usual choice. It does not need to be near your nodes, but latency between players and this machine is added to their connection, so closer to your players is better.

1. Fetch the bundle

curl -L https://github.com/basalt-host/basalt/archive/refs/heads/main.tar.gz \
  | tar xz --strip-components=3 "basalt-main/deploy/tunnel-server"
cp .env.example .env

2. Fill in .env

Three things matter:

# Generate with: openssl rand -hex 32
FRPS_TOKEN=

# The public port range players will connect on. One port per tunnel.
FRPS_PORT_MIN=30000
FRPS_PORT_MAX=30100

# Generate with: openssl rand -base64 24
FRPS_DASHBOARD_PASSWORD=

The range is your ceiling on simultaneous tunnels: one port is held for the life of each tunnel. Size it for the instances you expect to tunnel plus headroom. Avoid making it enormous "just in case": Docker publishes each port in a range with its own proxy process, so a range of tens of thousands makes startup slow and memory-hungry. A few hundred is unremarkable. If you genuinely need a huge range, the bundle's README explains how to switch the service to host networking instead.

Keep the range clear of the control port (7000), the dashboard (7500), and anything else already listening on the machine.

3. Start it

docker compose up -d
docker compose logs frps

You want to see frps started successfully.

Open the right ports, and only those

Your firewall or cloud security group needs to allow the control port from your nodes, and the player range from anywhere, on both TCP and UDP. The dashboard is deliberately published on loopback only; reach it with ssh -L 7500:127.0.0.1:7500 <the machine> rather than opening it up.

4. Point Basalt at it

In the panel, Settings → Tunneling:

FieldValue
frps server addressThis machine's public IP or hostname
frps server portFRPS_BIND_PORT (7000 by default)
frp tokenFRPS_TOKEN, byte for byte
Remote port min / maxFRPS_PORT_MIN / FRPS_PORT_MAX, exactly

The token and the range are the only coupling between the two sides. A token mismatch shows up in the panel as a rejected token; a range wider in the panel than on the server shows up as tunnels that allocate a port and then never register.

Keeping it updated

The frp version is pinned in .env on purpose, so an unrelated docker compose pull never changes your tunnel control plane underneath you. Before bumping it, read the release notes and the security advisories:

Subscribe to that advisory feed. This is an internet-facing service holding a credential that grants proxy creation across your whole fleet.

docker compose pull
docker compose up -d

Tunnels drop while it restarts and re-register on their own within a few seconds. Basalt reports the gap and recovers without intervention.

Next steps

On this page