Relay Deployment Guide (Oracle Cloud Free Tier)
Self-host a coturn relay on an Oracle Cloud Free Tier VPS for NetCrafter Relay.
There's no dedicated game server to deploy with NetCrafter Relay — HostActionRouter.cs compiles into the same client build as everyone else. But two small pieces of infrastructure do need to run somewhere with a real, public, non-CGNAT IP address: the coturn relay itself, and a small rendezvous service that resolves join codes to a host's relayed address (coturn only relays traffic — it doesn't do address-book lookups). This guide stands both up on the same Oracle Cloud Always Free tier compute instance — a public IP, indefinitely free, not a trial — since there's no reason to provision two servers for a small relay (2–4 players typical, up to 8–10).
Both are fully self-hosted by you. Pivec Labs does not run a shared coturn or rendezvous service for buyers to point at — every value in Relay Settings needs to come from a server you control.
Once you've created the instance (Section 01) and connected to it (Section 02), Section 03 installs and configures both pieces in one automated pass via install-relay-server.sh — this is the recommended path. Sections 04-09 walk through exactly the same steps by hand, kept as a reference for anyone who wants to understand or customize what the script does, or who'd rather not run it at all.
00 — Before you start#
You'll need:
- An Oracle Cloud account (oracle.com/cloud/free) — identity verification requires a card, but Always Free resources are never charged.
- An SSH key pair to log into the instance (
ssh-keygenlocally if you don't have one). - About 20 minutes.
01 — Create the compute instance#
01.1 — Start instance creation
- Go to Oracle Cloud Console → Compute → Instances → Create Instance
- Name it:
netcrafter-relay
01.2 — Choose the image
- Under Image, click Change Image
- Select: Canonical Ubuntu 22.04 (not 20.04, not Minimal, not aarch64)
- Confirm it shows "Always Free eligible"
01.3 — Choose the shape
This step causes the most problems — read carefully.
Two Always Free shapes are available:
Option A — VM.Standard.A1.Flex (Ampere ARM)
- Up to 4 OCPU / 24 GB RAM free
- Recommended: 2 OCPU / 12 GB RAM
- Better performance for coturn
- Frequently shows "Out of capacity" error — see Troubleshooting below
Option B — VM.Standard.E2.1.Micro (AMD x86)
- 1 OCPU / 1 GB RAM
- Always available — no capacity issues
- Sufficient for NetCrafter Relay (2–4 players typical, up to 8–10)
- Recommended if A1 is unavailable
To find these shapes:
- Click Shape series → Specialty and previous generation
- Look for shapes marked Always Free-eligible
- Select VM.Standard.A1.Flex OR VM.Standard.E2.1.Micro
01.4 — Configure networking
Do not skip any step:
- Under Primary VNIC → Virtual cloud network, select Create new virtual cloud network
- Enter a name:
netcrafter-vcn - Under Subnet, select Create new public subnet
- Enter a name:
public-subnet - Enter CIDR block:
10.0.0.0/24 - Under Private IPv4 address, select Automatically assign private IPv4 address
- Under Public IPv4 address, select Automatically assign public IPv4 address
The Public IPv4 checkbox will be greyed out until you enter a valid CIDR block in step 5. Enter 10.0.0.0/24 and click elsewhere — the checkbox will become available.
01.4a — Create Internet Gateway and Route Table
This step is critical and easy to miss. Without an Internet Gateway, your instance will have a public IP but will be completely unreachable from the internet — SSH will time out even with correct security list rules.
After creating your VCN:
- Go to Networking → Virtual Cloud Networks → netcrafter-vcn
- In the left menu click Internet Gateways
- Click Create Internet Gateway
- Name:
netcrafter-igw - Compartment: your root compartment
- Click Create Internet Gateway
Then add the route rule:
- In the left menu click Route Tables
- Click Default Route Table for netcrafter-vcn
- Click Add Route Rules
- Fill in:
- Target Type: Internet Gateway
- Destination CIDR Block:
0.0.0.0/0 - Target Internet Gateway:
netcrafter-igw
- Click Add Route Rules
Without this route rule, outbound traffic from your instance cannot reach the internet and inbound connections will time out.
01.5 — Add SSH keys
- Under Add SSH keys, select Generate a key pair for me
- Click Download private key — saves as
ssh-key-YYYY-MM-DD.key - Click Download public key — saves as
ssh-key-YYYY-MM-DD.key.pub - Store both files safely:
C:\Users\YourName\.ssh\
You cannot download the private key again after this step. If you lose it you cannot connect to the instance.
The public key field will be automatically populated after downloading.
Open required ports in the Security List
While you have the Security List open, add ingress rules for coturn:
Go to: Networking → Virtual Cloud Networks → netcrafter-vcn → public-subnet → Security Lists → Default Security List → Add Ingress Rules
Add these rules (one at a time):
| Source CIDR | Protocol | Port | Purpose |
|---|---|---|---|
0.0.0.0/0 |
TCP | 22 |
SSH |
0.0.0.0/0 |
TCP | 3478 |
TURN/STUN |
0.0.0.0/0 |
UDP | 3478 |
TURN/STUN |
0.0.0.0/0 |
TCP | 5349 |
TURN over TLS |
0.0.0.0/0 |
UDP | 5349 |
TURN over TLS |
0.0.0.0/0 |
UDP | 49152-65535 |
coturn relay ports |
Port 22 may already exist by default. Check before adding to avoid duplicates.
01.6 — Boot volume
Leave all boot volume settings as default:
- Do not check custom boot volume size
- Do not check in-transit encryption
- Do not attach block volumes
01.7 — Review and create
Confirm:
- Image: Canonical Ubuntu 22.04
- Shape: VM.Standard.A1.Flex or VM.Standard.E2.1.Micro
- Public IPv4 address: Yes
- SSH keys: present
Click Create.
Wait 1-2 minutes for status to change from Provisioning to Running.
Note the Public IP address shown on the instance details page.
Troubleshooting — Out of Capacity
If you see:
"Out of capacity for shape VM.Standard.A1.Flex in availability domain AD-1"
Try these in order:
- Change availability domain — try AD-2 or AD-3 instead of AD-1
- Remove fault domain — set to "Let Oracle choose the best fault domain"
- Switch to E2.1.Micro — under Shape, select Specialty and previous generation, then pick VM.Standard.E2.1.Micro. Always available and sufficient for NetCrafter Relay.
- Retry later — Oracle releases A1 capacity randomly. Try again in 30-60 minutes or the following morning.
Note: You cannot change region after starting instance creation. If your region is consistently full, start over and select a different region from the Oracle Cloud Console home page.
Troubleshooting — SSH Connection Timed Out
SSH times out even though port 22 is in the Security List. This is almost always caused by a missing Internet Gateway or route rule.
Check this in order:
- Go to Networking → Virtual Cloud Networks → netcrafter-vcn
- Click Internet Gateways in the left menu
- If NONE exists → create one named
netcrafter-igw(see Section 01.4a)
- If NONE exists → create one named
- Click Route Tables → Default Route Table
- Confirm there is a rule with: Destination
0.0.0.0/0, Target Internet Gateway - If missing → Add Route Rules with these values
- Confirm there is a rule with: Destination
The Security List controls which ports are open. The Internet Gateway + Route Table controls whether any traffic can reach your instance at all. Both are required.
02 — Connect#
ssh -i /path/to/your_private_key ubuntu@<PUBLIC_IP>
03 — Automated setup (recommended)#
Sections 04-09 below install and configure coturn and the rendezvous service by hand, one SSH command at a time — that's still exactly what happens, and it's worth reading if you want to understand or customize any of it. But you don't have to type it all in yourself: install-relay-server.sh, shipped alongside the other server files, runs the entire sequence in one pass and ends with both services running, enabled on boot, and verified.
It does not change the self-hosted architecture — you're still running your own server, on your own instance. It only automates the setup.
03.1 — Copy the files onto the instance
Three files ship together at Assets/PivecLabs/NetCrafterRelay/ServerTools/:
install-relay-server.sh
rendezvous_server.py
netcrafter-relay-rendezvous.service
Copy all three (adjust the source path to wherever your Unity project lives):
scp -i /path/to/your_private_key \
"Assets/PivecLabs/NetCrafterRelay/ServerTools/install-relay-server.sh" \
"Assets/PivecLabs/NetCrafterRelay/ServerTools/rendezvous_server.py" \
"Assets/PivecLabs/NetCrafterRelay/ServerTools/netcrafter-relay-rendezvous.service" \
ubuntu@<PUBLIC_IP>:~/
03.2 — Run it
sudo bash ~/install-relay-server.sh
With no flags, it prompts for each value it can't detect itself (public/private IP are auto-detected via cloud metadata and ifconfig.me/interface routing, with a chance to confirm or override; the TURN shared secret defaults to a freshly generated random one). Pass -y/--yes to accept every detected/default value without prompting, or supply any value directly as a flag — --public-ip, --private-ip, --realm, --turn-username, --shared-secret, --rendezvous-port, --min-port, --max-port. Run --help for the full list.
Want to see exactly what it would do first, with no root and no changes to the real system? bash install-relay-server.sh --dry-run writes every generated file (turnserver.conf, the systemd unit, etc.) under a local ./netcrafter-relay-dry-run/ folder and echoes every privileged command instead of running it.
Step by step, a real (non-dry-run) run:
- Installs coturn via
apt. - Detects (or accepts as flags) public IP, private IP, realm, TURN username, and TURN shared secret — confirming each with you unless
--yesis passed. - Writes
/etc/turnserver.conffrom those values —external-ip,lt-cred-mech,realm,user=— matching Section 06 below exactly. - Enables and starts the coturn systemd service.
- Installs
rendezvous_server.pyto/opt/netcrafter-relay/and the systemd unit to/etc/systemd/system/, creating thenetcrafter-relaysystem user and/var/lib/netcrafter-relay-rendezvousdata directory to match what the unit file expects (verified — no mismatch between the two). - Enables and starts the rendezvous systemd service.
- Opens both services' ports via
iptables(+netfilter-persistent save), then detects whether this looks like an Oracle Cloud instance and prints an explicit reminder either way: cloud providers commonly enforce a separate, network-level firewall (Oracle's Security List, AWS Security Groups, etc.) thatiptablescannot reach or open — that part is always manual, in your provider's console. - Verifies: curls the rendezvous service's endpoints (including a full host/join/peers round trip), checks coturn's systemd status, and runs a local
turnutils_uclientcredential smoke test if it's available — then prints a plain summary of what's running, on what ports, and the exact values to paste into Unity's Relay Settings.
That final summary looks like this (values will differ per instance):
========================================================
NetCrafter Relay server setup complete.
Running:
coturn : UDP/TCP 3478, relay ports 49160-49200
netcrafter-relay-rendezvous : TCP 8787
Paste these into Unity -> NetCrafter Relay window -> Settings -> Relay Settings:
Relay Host : 192.0.2.10
Relay Port : 3478
Rendezvous URL : http://192.0.2.10:8787
Credential Mode : StaticSecret
TURN Username : netcrafter-relay
Shared Secret : <your generated or provided secret>
NOTE: Rendezvous URL includes the http:// prefix, shown above -- Relay Host
does NOT take one (TURN doesn't use a scheme). Paste Rendezvous URL exactly
as printed, scheme included.
IMPORTANT -- cloud firewall (this script cannot reach this):
This looks like an Oracle Cloud instance. The iptables rules above only
cover this machine's own firewall -- Oracle's Security List is a SEPARATE
network-level firewall that also must allow this traffic in, and this
script has no way to reach or modify it. Open it yourself:
Console -> Networking -> Virtual Cloud Networks -> your VCN ->
Security Lists -> Add Ingress Rules:
0.0.0.0/0 UDP+TCP 3478 TURN/STUN
0.0.0.0/0 UDP 49160-49200 Relay ports
0.0.0.0/0 TCP 8787 Rendezvous service
========================================================
Note: Rendezvous URL is printed with the http:// prefix, unlike Relay Host directly above it — copy it exactly as shown, scheme included. Entering it without the scheme (e.g. 192.0.2.10:8787) is auto-corrected at runtime, but pasting it as printed avoids relying on that.
A copy of the credential values is also saved to /root/netcrafter-relay-credentials.txt (mode 600).
The script's local turnutils_uclient check is a best-effort smoke test, not the authoritative one — Section 08's browser-based check (from a real remote browser, over the actual internet path peers will use) is still the one to trust if the two ever disagree.
Safe to re-run: useradd tolerates an existing user, config/service files are overwritten with the same content, and systemctl enable --now is idempotent — running it again against an already-configured instance re-confirms everything and exits the same way.
04 — Open the coturn ports (both layers)#
Sections 04-09 are the manual, step-by-step version of what install-relay-server.sh (Section 03) does in one automated pass. If you've already run the script, everything below is already done — read on only if you want to understand, verify, or customize any of it by hand.
Oracle Cloud has two firewall layers, and both must allow the traffic — this is the most common thing people miss. (The rendezvous service gets its own port later, in Section 09 — same two-layer gotcha applies there too.)
(a) Security List (cloud-level, stateless)
Console → Networking → Virtual Cloud Networks → your VCN → Security Lists → the default security list → Add Ingress Rules:
| Source CIDR | Protocol | Destination Port Range | Purpose |
|---|---|---|---|
0.0.0.0/0 |
UDP | 3478 |
STUN/TURN listener |
0.0.0.0/0 |
TCP | 3478 |
TURN over TCP (fallback) |
0.0.0.0/0 |
UDP | 49160-49200 |
Relay port range (must match min-port/max-port below) |
A narrow relay port range (here, 41 ports) is enough for a handful of concurrent sessions (2–4 players typical, up to 8–10) and keeps the open-port surface small — widen it only if you expect many simultaneous sessions.
(b) The instance's own firewall
Oracle's Ubuntu images ship with iptables rules (via netfilter-persistent/oracle-cloud-agent) that block everything not explicitly allowed, on top of the Security List above. Open the same ports locally:
sudo iptables -I INPUT -p udp --dport 3478 -j ACCEPT
sudo iptables -I INPUT -p tcp --dport 3478 -j ACCEPT
sudo iptables -I INPUT -p udp --dport 49160:49200 -j ACCEPT
sudo apt install -y iptables-persistent # if not already installed
sudo netfilter-persistent save
05 — Install coturn#
sudo apt update
sudo apt install -y coturn
06 — Configure turnserver.conf#
Edit /etc/turnserver.conf:
listening-port=3478
fingerprint
lt-cred-mech
realm=netcrafter.relay
user=netcrafter-relay:YOUR_SHARED_SECRET_HERE
min-port=49160
max-port=49200
external-ip=<PUBLIC_IP>/<PRIVATE_IP>
no-tls
no-dtls
log-file=/var/log/turnserver.log
simple-log
no-multicast-peers
user=netcrafter-relay:YOUR_SHARED_SECRET_HERE— this is the StaticSecret long-term credential: a single username/password pair every peer uses. Pick a real random secret, not a password you use elsewhere.external-ip=<PUBLIC_IP>/<PRIVATE_IP>— Oracle instances have a private IP (find it withip addr show, usually onens3/eth0) behind a separately NAT'd public IP. This tells coturn to advertise the public IP in relayed-address attributes while binding to the private interface. Without this, peers receive an unreachable private IP and allocation succeeds but relaying silently fails.no-tls/no-dtls— keeps the MVP setup simple (plain UDP relay, matchingStunTurnClient.cs, which doesn't do DTLS). Fine for a small co-op relay (2–4 players typical, up to 8–10); add TLS later if you want encrypted TURN control traffic.
07 — Enable and start coturn#
sudo nano /etc/default/coturn # set TURNSERVER_ENABLED=1
sudo systemctl enable --now coturn
sudo systemctl status coturn # confirm "active (running)"
sudo tail -f /var/log/turnserver.log # watch for startup errors
08 — Test the relay server#
The quickest way to verify coturn is working is using a free browser-based TURN checker — no extra software needed (no Linux-only tools like turnutils_uclient required).
- Open this URL in your browser: webrtc.github.io trickle-ice sample
- Scroll down to the ICE servers section
- Remove any existing servers and add yours:
URI: turn:YOUR_PUBLIC_IP:3478 (e.g. turn:92.5.181.120:3478)
Username: netcrafter-relay
Password: <your shared secret from turnserver.conf>
- Click Add Server
- Click Gather candidates
- Wait 10-15 seconds
✅ SUCCESS — you should see one or more candidates with type: relay appear in the list. This confirms coturn is correctly configured end to end.
FAILURE — if no relay candidates appear:
- Check coturn is running:
sudo systemctl status coturn - Check the log for errors:
sudo tail -f /var/log/turnserver.log - Confirm ports 3478 TCP and UDP are open in both the Oracle Security List AND the instance iptables rules
- Confirm
external-ipin turnserver.conf has the correct public/private IP format:external-ip=PUBLIC_IP/PRIVATE_IP
09 — Install the rendezvous service#
Join codes need a small lookup service so a host's relayed address can be found by a joiner (and vice versa) — coturn only relays traffic, it doesn't do address-book lookups. This section installs a minimal one on the same instance, listening on its own port, so you don't need to provision a second server.
This is a small, hobby-project-grade service — no auth beyond knowing the join code, no HTTPS by default, no clustering. That matches the same security posture already accepted for the StaticSecret TURN credential above: fine for a small self-hosted relay (2–4 players typical, up to 8–10), not something to expose as shared production infrastructure for strangers.
09.1 — Open its port (both layers)
Pick a port not already used by coturn — this guide uses 8787.
(a) Security List:
Console → Networking → Virtual Cloud Networks → your VCN → Security Lists → the default security list → Add Ingress Rules:
| Source CIDR | Protocol | Destination Port Range | Purpose |
|---|---|---|---|
0.0.0.0/0 |
TCP | 8787 |
Rendezvous HTTP service |
(b) The instance's own firewall:
sudo iptables -I INPUT -p tcp --dport 8787 -j ACCEPT
sudo netfilter-persistent save
09.2 — Copy the rendezvous script onto the instance
This is the manual, by-hand version of what install-relay-server.sh (Section 03) does automatically. Skip to Section 03 if you just want it running.
The service is a single Python 3 file with no pip installs required — Ubuntu 22.04 ships Python 3 by default, and the script only uses the standard library (http.server, sqlite3).
It ships with NetCrafter Relay at:
Assets/PivecLabs/NetCrafterRelay/ServerTools/rendezvous_server.py
Assets/PivecLabs/NetCrafterRelay/ServerTools/netcrafter-relay-rendezvous.service
Copy both to the instance (from your local machine — adjust the source path to wherever your Unity project lives):
scp -i /path/to/your_private_key \
"Assets/PivecLabs/NetCrafterRelay/ServerTools/rendezvous_server.py" \
"Assets/PivecLabs/NetCrafterRelay/ServerTools/netcrafter-relay-rendezvous.service" \
ubuntu@<PUBLIC_IP>:~/
09.3 — Install and start it as a systemd service
sudo mkdir -p /opt/netcrafter-relay
sudo mv ~/rendezvous_server.py /opt/netcrafter-relay/
sudo useradd --system --no-create-home --shell /usr/sbin/nologin netcrafter-relay || true
sudo mkdir -p /var/lib/netcrafter-relay-rendezvous
sudo chown -R netcrafter-relay:netcrafter-relay /var/lib/netcrafter-relay-rendezvous
sudo mv ~/netcrafter-relay-rendezvous.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now netcrafter-relay-rendezvous
sudo systemctl status netcrafter-relay-rendezvous # confirm "active (running)"
If useradd reports "user already exists," that's fine — the || true just makes this step safe to re-run.
09.4 — Test it
curl http://<PUBLIC_IP>:8787/
✅ SUCCESS — you should see {"status": "ok", "service": "netcrafter-relay-rendezvous"}.
FAILURE:
- Check the service is running:
sudo systemctl status netcrafter-relay-rendezvous - Check its logs:
sudo journalctl -u netcrafter-relay-rendezvous -f - Confirm port 8787 TCP is open in both the Oracle Security List AND the instance's iptables rules (same two-layer gotcha as coturn — see Section 04)
Then confirm the actual join-code contract end to end (this is the same sequence NetCrafter Relay's generated code drives automatically — see RelayConnectionManager.tpl's HostSession()/JoinSession()/PollForPeers()):
# Simulate a host registering:
curl -X POST http://<PUBLIC_IP>:8787/host -d '{"relayedAddress":"1.2.3.4:5000"}'
# -> {"joinCode": "AB3F9", "userId": 0}
# Simulate a joiner resolving that code (use the joinCode from above):
curl -X POST http://<PUBLIC_IP>:8787/join/AB3F9 -d '{"relayedAddress":"5.6.7.8:6000"}'
# -> {"relayedAddress": "1.2.3.4:5000"}
# Simulate the host polling for peers:
curl http://<PUBLIC_IP>:8787/host/AB3F9/peers
# -> {"peers": ["5.6.7.8:6000"]}
✅ SUCCESS — the join code from step 1 resolves to the host's address in step 2, and shows up in the peer list in step 3. This is exactly the flow a real host + joiner exercise automatically.
10 — Point NetCrafter Relay at both#
In the NetCrafter Relay editor window → Settings → Relay Settings:
| Field | Value |
|---|---|
| Relay Host | <PUBLIC_IP> (or a DNS name pointed at it) — no scheme |
| Relay Port | 3478 |
| Rendezvous URL | http://<PUBLIC_IP>:8787 — requires the http:// prefix |
| Credential Mode | StaticSecret |
| TURN Username | netcrafter-relay (must match user= in turnserver.conf) |
| Shared Secret | the same secret from user=name:secret |
Rendezvous URL and Relay Host sit right next to each other but expect different formats — Relay Host is a bare host with no scheme (TURN doesn't use one), while Rendezvous URL is a plain HTTP endpoint and needs the http:// (or https://) prefix. Entering <PUBLIC_IP>:8787 instead of http://<PUBLIC_IP>:8787 is auto-corrected at runtime, but it used to fail with a generic "Cannot connect to destination host" and no hint why — copy it exactly as shown above to avoid depending on that fallback.
Rendezvous URL has no working default — the field is required and ships blank. Leaving it blank fails the join-code flow at runtime with a clear error rather than a silent hang; there is no Pivec Labs-hosted fallback to fall back to.
Click Save Settings. The generated RelayConnectionManager reads these values live from this same RelaySettings asset at runtime (via its connectionSettings field, auto-wired by Prefab Generator) — there's no need to regenerate code or recreate the prefab just because a value here changed.
11 — Security notes#
- The shared secret lives only in the
RelaySettings.assetScriptableObject (Definitions/{Name}/RelaySettings.asset) — it is read live viaconnectionSettings.sharedSecret, never baked into generated C# source. Treat it as a low-value credential appropriate for a hobby/small-studio relay regardless — not a production secret, never one reused from elsewhere — but it won't end up committed to source control just because you committed generated code. - Anyone who gets a copy of your
RelaySettings.asset(or theRelayConnectionManagerprefab referencing it) can see this secret and, in principle, allocate through your relay. For small co-op sessions (2–4 players typical, up to 8–10) the blast radius is small, but it's worth knowing. A production-grade setup would use RestApi credential mode — time-limited HMAC credentials minted by a rendezvous service — which is reserved for a future release and not yet implemented. - The rendezvous service has no authentication beyond knowing the join code — anyone who guesses or brute-forces a 5-character code within its ~15 minute lifetime could poll its peer list. At 2-8 players and short-lived codes, this is a low-value target, matching the same posture already accepted for the TURN shared secret above.
- The rendezvous service's database is a single SQLite file at
/var/lib/netcrafter-relay-rendezvous/rendezvous.db. It's designed to be disposable — only ever holds short-lived join codes — so there's no need to back it up. - Always Free has an outbound data transfer allowance (10TB/month at time of writing) — comfortably enough for casual co-op traffic; keep an eye on it if you expect heavy use.
- Keep the instance patched:
sudo apt update && sudo apt upgrade -yperiodically.
12 — What this guide does not cover#
- RestApi credential mode — time-limited HMAC-signed TURN credentials minted by a server-side service, instead of the single long-term StaticSecret used throughout this guide. Reserved for a future release; not yet implemented.
- TLS/HTTPS for either coturn (control channel) or the rendezvous HTTP service — both run plain UDP/HTTP here, matching the MVP posture described above. Add TLS yourself if you need encrypted control traffic for your use case.
- Scaling beyond a single instance — this guide is sized for a handful of concurrent sessions (2–4 players typical, up to 8–10) on one Always Free box, not a matchmaking service for many simultaneous games.
- Provisioning the instance itself, or your cloud provider's network-level firewall —
install-relay-server.sh(Section 03) automates everything that happens on the instance over SSH. Creating the instance (Section 01) and opening ports in your provider's own console (Oracle's Security List or equivalent) both happen outside SSH, so no script — this one included — can reach or automate them.