Skip to main content

Deployment

This page covers running the FigNet game server in Docker, from a local quick-start to a full production rollout on a Hetzner Cloud VPS.

Docker

This guide covers running the FigNet game server in Docker - locally for testing and on a Linux VPS for production. The image bundles the .NET game/admin server and the static admin dashboard in a single container; Caddy fronts it for TLS.

Game UDP traffic (ENet on 5559/udp) connects directly to the server. Game TCP/WSS (port 9000) and the admin panel (port 8080) are reverse-proxied by Caddy on admin.<your-domain> and game.<your-domain> for TLS.

Quick start (local, no TLS)

For testing the image on your laptop without setting up DNS or certs:

git clone <your-fork> && cd FigNetServer
cp .env.example .env
# Edit .env: set ADMIN_DOMAIN=localhost, GAME_DOMAIN=localhost,
# CADDY_EMAIL=internal@local, CLUSTER_SECRET=$(openssl rand -base64 32)

docker build -t fignet-server:dev .
# Or pull from GHCR once a release is published:
# docker pull ghcr.io/<owner>/fignet-server:latest

# Run the server alone (skip Caddy locally - it can't issue Let's Encrypt for localhost):
docker run --rm -it \
--network host \
-v entangle-data:/app/data \
-e FIGNET_ROLE=Master \
fignet-server:dev

Open http://localhost:8080, log in with admin / changeme, change credentials in the Config page.

Master VPS deployment

Prerequisites

  • A Linux VPS (Ubuntu 22.04 / Debian 12 tested) with docker + docker compose.
  • A domain you control. Add two A records pointing at the VPS public IP:
    • admin.yourgame.com -> admin panel
    • game.yourgame.com -> game WSS transport

Steps

# 1. One-time host tuning (sysctls, FD limits, ufw rules).
sudo bash scripts/setup-vps.sh

# 2. Configure .env (DOMAINS, EMAIL, CLUSTER_SECRET).
cp .env.example .env
nano .env

# 3. Pull the image and start.
docker compose pull
docker compose up -d

# 4. Watch logs while Caddy fetches certs from Let's Encrypt.
docker compose logs -f caddy

When you see certificate obtained successfully for both domains, browse to https://admin.yourgame.com, log in admin / changeme, and immediately:

  1. Change the admin password (Config -> Admin section -> Save).
  2. Confirm the JWT secret was randomized at first boot (entrypoint logs Generated random JwtSecret); rotate again from the Config page if you want a fresh one.
  3. Set a real ClusterSharedSecret (via env or Config UI).

Game clients connect to:

  • wss://game.yourgame.com/ - game WebSocket transport (TCP)
  • udp://<vps-ip>:5559 - ENet (direct, not proxied)

Why two domains?

Caddy fronts both the admin and the game WSS endpoints, but they're different upstreams (8080 and 9000) with different latency requirements. Keeping them on separate subdomains lets us tune each block independently - admin gets gzip and 4 h keepalive; game gets no compression and 24 h keepalive plus tighter buffer sizes.

Adding a node VPS

Spin up a second VPS to scale horizontally. Same image, different env block.

Prerequisites

  • Second Linux VPS.
  • DNS A record for the node, e.g. game-node1.yourgame.com -> node IP.
  • The master's CLUSTER_SECRET (from its .env).

Steps

sudo bash scripts/setup-vps.sh

cp .env.example .env
# Set:
# GAME_DOMAIN=game-node1.yourgame.com
# CADDY_EMAIL=ops@yourgame.com
# MASTER_ADDRESS=wss://admin.yourgame.com/ws/cluster
# CLUSTER_SECRET=<same as master>

docker compose -f docker-compose.node.yml pull
docker compose -f docker-compose.node.yml up -d

Within ~1 second, the node appears on the master's Cluster page. Per-node stats stream to the master's Dashboard via the node selector.

Game clients targeting the new node use:

  • wss://game-node1.yourgame.com/
  • udp://<node-ip>:5559

The master's UI handles all cluster operations (kick user, delete room) for both master and node - nodes don't expose their own admin panel.

Updating

docker compose pull
docker compose up -d

The entangle-data volume preserves config, AppKeys, and audit log:

  • No re-login required (AppKeys.xml unchanged -> JWTs stay valid).
  • Custom config edited via the admin panel survives the redeploy.
  • Caddy's cert state lives in caddy-data - no re-issuance.

Backup and restore

# Backup
docker run --rm \
-v entangle-data:/data:ro \
-v "$PWD":/backup \
alpine tar czf /backup/entangle-data.tgz /data

# Restore
docker run --rm \
-v entangle-data:/data \
-v "$PWD":/backup \
alpine sh -c 'cd /data && tar xzf /backup/entangle-data.tgz --strip-components=1'

Include caddy-data in the backup if you want to skip a Let's Encrypt re-issue.

Troubleshooting

SymptomCheck
Dashboard 502 / cert errorsdocker logs fignet-caddy - Let's Encrypt rate limits, DNS, port 80 reachable from internet
Server not respondingdocker logs fignet, curl http://127.0.0.1:8080/healthz
Game UDP packets lostss -ulnp | grep 5559 (must show EntangleServer, not docker-proxy); confirm network_mode: host
Buffers truncatedcat /proc/sys/net/core/rmem_max should return 134217728 after setup-vps.sh
Node not appearing on mastermatch CLUSTER_SECRET exactly; check MASTER_ADDRESS reachable from node (curl https://admin.../healthz)
Logs missinglogs land in entangle-data:/logs/; inspect via docker run --rm -v entangle-data:/data alpine ls /data/logs

Why host networking?

Port-mapping UDP through Docker's userland proxy adds 20-40 microseconds of latency per packet and silently drops bursts under load - unacceptable for a 128 fps game loop. network_mode: host bypasses NAT entirely; the EntangleServer process binds the host's NIC directly. This is also why we set kernel buffers in setup-vps.sh rather than per-container - host networking shares the host's kernel namespace.

Note: do not consolidate by routing UDP through Caddy. Caddy is HTTP-only, and ENet's reliable-UDP framing has no notion of a reverse proxy.

Default credentials checklist

After first boot:

  • Admin password changed from changeme
  • ClusterSharedSecret set to a real value (Config UI or FIGNET_CLUSTER_SECRET env)
  • JwtSecret is no longer CHANGE_THIS_SECRET_IN_PRODUCTION_... (entrypoint randomizes on first run, but verify in Config UI)
  • Caddy logs show successful cert issuance for both domains
  • Health check is green: curl https://admin.yourgame.com/healthz returns {"status":"ok"}

Image size and build notes

  • Final image: ~210 MB (runtime-deps + self-contained .NET + libenet + static dashboard).
  • PublishSingleFile=false is mandatory - FigNet loads modules at runtime from AppContext.BaseDirectory. Do not "fix" it.
  • Multi-stage build: web (node:20-alpine) -> dotnet (sdk:8.0) -> runtime (runtime-deps:8.0).
  • CI publishes on git tags (v*.*.*) with cache-from: gha. First build is slow; subsequent are minutes.

Hetzner

A concrete, top-to-bottom recipe for deploying the FigNet master (Entangle + FnVoice) to a freshly provisioned Hetzner Cloud VPS. Follow this top to bottom on a clean Ubuntu box and you should reach a working production deploy with TLS in roughly 60 minutes.

This guide layers on top of the generic Docker section above - that section explains what the compose stack does; this one is the how for a Hetzner-specific cold start.

What's covered:

  • Hetzner Cloud Firewall + SSH hardening
  • DNS for three subdomains (admin, entangle game WSS, FnVoice WSS)
  • Building and pushing the unified image from a dev machine to Docker Hub
  • Host tuning in SSH-safe order (no lockout risk)
  • Post-boot verification + a "first 5 minutes" hardening checklist
  • Update / rollback / backup workflows
  • A troubleshooting appendix covering every failure mode hit during the bring-up

Assumptions baked in:

  • Single master VPS (nodes are a separate guide - start with docker-compose.node.yml)
  • Image is public on Docker Hub (mahmed310/fignet-server by default) - no docker login required on the VPS
  • Manual publish from your dev machine via Docker Desktop / CLI; the optional CI workflow at .github/workflows/release.yml is a fallback you can wire up later

Prerequisites

Before you start, have these in hand:

  • A Hetzner Cloud account with billing set up
  • A domain you control (you'll add three A records)
  • Docker Desktop on your dev machine, signed into Docker Hub (avatar visible top-right of Desktop = the docker CLI is also authenticated - no separate docker login needed)
  • The repo cloned locally on a branch that ships the unified Dockerfile (feature/unity6_message_struct at time of writing)

Verify on your dev machine:

docker --version
docker info | findstr Username # should show your Docker Hub username

Provision the Hetzner VPS

In the Hetzner Cloud console:

  1. Add Server -> Location: pick the region closest to your players
  2. Image: Ubuntu 24.04 LTS (or current LTS)
  3. Type: at least CX31 (4 vCPU / 8 GB / 160 GB) for a master running both Entangle and FnVoice
  4. SSH keys: add your public key (~/.ssh/id_ed25519.pub from the dev box). Don't use root password auth.
  5. Create the server. Note the public IPv4 (and IPv6 if you want) - you'll use it when setting up DNS and connecting over SSH.

Why CX31 minimum? The unified image bundles the .NET server, Next.js admin SPA, and runs both Entangle and FnVoice transports. CX21 (2 vCPU / 4 GB) works for low-population testing but starves at 100+ concurrent peers.

Hetzner Cloud firewall

This protects the VPS at the network edge, before packets reach the VM. Set this up before your first SSH so the box is never exposed unfiltered to the public internet.

In the Hetzner Cloud console: Firewalls -> Create firewall named e.g. fignet-master.

Inbound rules:

ProtocolPortSourcePurpose
TCP22Any IPv4 + IPv6SSH (we'll tighten this to your dev IP only later, see Tighten SSH source IP below)
TCP80AnyCaddy HTTP / Let's Encrypt ACME challenge
TCP443AnyCaddy HTTPS - admin + Entangle WSS + FnVoice WSS
UDP5559AnyEntangle ENet (direct, no proxy)
UDP5558AnyFnVoice ENet (direct, no proxy)

Outbound: leave default (allow all).

Apply the firewall to your VPS (Servers -> your server -> Networking -> Firewalls).

Why both Hetzner FW and ufw later? Defense in depth. Hetzner's FW protects the host network interface; ufw protects against any in-VM process bypassing it from inside.

DNS

At your domain registrar, add three A records pointing at the VPS public IPv4:

SubdomainTargetWhat it fronts
admin.<your-domain><vps-ip>Admin panel (port 8080)
entangle.<your-domain><vps-ip>Entangle game WSS (port 9000)
voice.<your-domain><vps-ip>FnVoice WSS (port 9001)

(You can use game.<your-domain> instead of entangle.<your-domain> - just be consistent with what you put in GAME_DOMAIN later.)

Optionally add AAAA records for IPv6.

Verify propagation before continuing - Caddy's first cert request will fail if DNS isn't live yet:

dig +short admin.yourgame.com
dig +short entangle.yourgame.com
dig +short voice.yourgame.com

All three should return the VPS IP. Usually under 5 minutes after creation.

SSH hardening on the VPS

You're currently root@<vps-ip> with key auth. Lock that down before doing anything else.

Create a non-root deploy user

adduser deploy
usermod -aG sudo deploy
mkdir -p /home/deploy/.ssh
nano /home/deploy/.ssh/authorized_keys # paste your public key
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys
chown -R deploy:deploy /home/deploy/.ssh

Open a second terminal and verify ssh deploy@<vps-ip> works before locking root out. This is your safety net.

Lock down SSH

sudo nano /etc/ssh/sshd_config
# Change these lines:
# PermitRootLogin no
# PasswordAuthentication no
sudo systemctl restart ssh

From this point: ssh deploy@<vps-ip> then sudo for privileged work.

Install Docker

Use Docker's official apt repo, not Ubuntu's older docker.io:

sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg ufw

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list

sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin

sudo usermod -aG docker deploy # log out / back in for group to apply
docker --version && docker compose version

Build and push the image from your dev machine

This is independent of VPS work - you can do this step on your dev machine while the VPS provisioning and hardening steps above are still in progress.

From the repo root, on the branch that contains the unified Dockerfile:

git checkout feature/unity6_message_struct
git pull
docker build `
-t mahmed310/fignet-server:0.1.1 `
-t mahmed310/fignet-server:latest `
--platform linux/amd64 `
.
docker push mahmed310/fignet-server:0.1.1
docker push mahmed310/fignet-server:latest

Pick a fresh version tag every release - never reuse one. :latest rolls; the version pin (0.1.1 here) is your immutable rollback target.

Why --platform linux/amd64? Hetzner VPSes are x86_64. Docker Desktop on Windows builds amd64 by default, but the explicit flag prevents accidental multi-arch builds when buildx is configured.

Verify at https://hub.docker.com/r/mahmed310/fignet-server:

  • Both tags appear under Tags (~210 MB each)
  • Settings -> Visibility = Public (if it defaulted to Private, switch it now)

Anonymous pull test from a logged-out shell - proves the VPS will be able to pull without auth:

docker logout
docker pull mahmed310/fignet-server:0.1.1

Get deploy files onto the VPS

You only need four files: docker-compose.yml, docker/Caddyfile, scripts/setup-vps.sh, .env.example. Easiest is just clone the repo:

ssh deploy@<vps-ip>
cd ~
git clone https://github.com/<owner>/FigNetServer.git
cd FigNetServer

Private repo? Use a deploy key, GitHub CLI auth, or scp the four files manually. The image itself comes from Docker Hub - git only ships the compose/config bits.

Apply host tuning (SSH-safe order)

This is the one section where order matters. scripts/setup-vps.sh only adds ufw allow rules for 80/443/5559/5558 and sets kernel sysctls - it does not allow port 22, and it does not call ufw enable. The lockout risk is the next step (ufw enable) if port 22 isn't already allowed.

Pre-flight (read-only)

sudo ufw status verbose # if "Status: inactive", we're fine
ss -tnp | grep :22 # confirm your SSH is on 22

Open a second SSH session in another terminal NOW as a safety net. Hetzner's web console (Hetzner Cloud -> your server -> Console) is a third fallback that works even if SSH is fully blocked.

Apply rules in safe order

# 1. Allow SSH FIRST - before anything could deny it
sudo ufw allow 22/tcp comment 'SSH'

# 2. Run the kernel/FD/ufw-rules script (no enable, no SSH change)
sudo bash scripts/setup-vps.sh

# 3. Confirm port 22 is in the rule list before activating
sudo ufw show added | grep -E '22/tcp|allow 22'

# 4. NOW enable. If step 3 didn't show port 22, STOP and add it
sudo ufw enable # answer 'y' at the prompt
sudo ufw status numbered # confirm 22, 80, 443, 5559, 5558 listed

If you do get disconnected (very unlikely): use the Hetzner web console to run sudo ufw disable, then reconnect.

Verify sysctls applied

sysctl net.core.rmem_max # expect 134217728
sysctl net.core.somaxconn # expect 4096

Do NOT add a sysctls: block to the fignet service in docker-compose.yml. network_mode: host shares the host network namespace and Docker refuses to set those sysctls from a container - docker compose up -d will fail with sysctl ... not allowed in host network namespace. The host already has them set by setup-vps.sh; that's where they belong.

Configure .env

cp .env.example .env
nano .env

Required values:

IMAGE_OWNER=mahmed310
IMAGE_TAG=0.1.1 # pin a version; never re-use a tag
ADMIN_DOMAIN=admin.yourgame.com
GAME_DOMAIN=entangle.yourgame.com
VOICE_DOMAIN=voice.yourgame.com
CADDY_EMAIL=ops@yourgame.com
CLUSTER_SECRET=<paste output of: openssl rand -base64 32>

Reusing a box that runs other services? Stop anything binding ports 80, 443, 9000, 9001, 5559, or 5558 first (docker ps, then docker stop <name>). Caddy will crash-loop on bind: address already in use otherwise. Single-purpose VPS is strongly recommended.

Pull and start

docker compose pull
docker compose up -d
docker compose ps # both fignet and fignet-caddy "Up"
docker compose logs -f caddy

Watch the Caddy log for three certificate obtained successfully lines (one per domain). Ctrl-C the follow once all three appear.

Then watch the server boot:

docker compose logs -f fignet

Look for these lines in order:

  1. Listening port=5559 (Entangle ENet)
  2. Listening port=5558 (FnVoice ENet)
  3. Listening side=server port=9000 (Entangle WSS)
  4. Listening side=server port=9001 (FnVoice WSS)
  5. Module 'Entangle' (version 1.0.0) loaded successfully
  6. Module 'FnVoiceServer' (version 1.0.0) loaded successfully - critical
  7. Admin Web Server listening url=http://*:8080

Post-boot verification

# Admin healthcheck via Caddy
curl -fsS https://admin.yourgame.com/healthz # expect {"status":"ok"}

# UDP listeners - must be the EntangleServer process, NOT docker-proxy
ss -ulnp | grep 5559
ss -ulnp | grep 5558

# FnVoice module loaded cleanly
docker compose logs fignet | grep -i fnvoice

The FnVoice line is the critical regression check. If you see:

[ERR] Service not registered type=FigNet.Core.IServer.
[ERR] Failed to load module name=FnVoiceServer. exception=NullReferenceException

...the image was built before FnVoiceServer/ServerConnectionListener.cs was fixed to lazy-resolve IServer. Rebuild from a branch that includes the fix and bump IMAGE_TAG in .env.

Open https://admin.yourgame.com in a browser -> log in admin / changeme -> continue to the hardening checklist below immediately.

First 5 minutes hardening checklist

Defaults are not safe for production. Before you let real clients connect:

  • Rotate the admin password (Settings -> Account in the admin UI)
  • Rotate AppSecretKey in ServerConfig.yaml from 123 to a real per-app secret. Update the Unity client's AppSecretKey to match.
  • Confirm ClusterSharedSecret in ServerConfig.yaml matches .env CLUSTER_SECRET.
  • Confirm JwtSecret was randomized on first boot:
    docker compose logs fignet | grep -i 'Generated random JwtSecret'
    If you see CHANGE_THIS_SECRET_IN_PRODUCTION still inside the persisted yaml, the entrypoint randomizer didn't fire - replace it manually.
  • Verify FnVoiceServer module is uncommented in the persisted ServerConfig.yaml. The persisted file lives at:
    /var/lib/docker/volumes/fignetserver_entangle-data/_data/ServerConfig.yaml
    The Modules: block must include:
    - AssemblyName: FnVoiceServer
    Type: FnVoiceServer.Modules.Lobby.FNELobby

The persisted yaml is what's loaded on every restart - the image-bundled defaults seed it once on first boot, then the volume wins.

Unity client connection settings (reference)

Mirror these in your Unity EntangleConfig and VoiceServerConfig:

ModuleServer IP / HostPortTransportTLS/WSS
Entangle (UDP, default)<VPS public IP>5559ENetoff
Entangle (WSS fallback)entangle.yourgame.com443WebSocketon
FnVoice (UDP, default)<VPS public IP>5558ENetoff
FnVoice (WSS fallback)voice.yourgame.com443WebSocketon

Don't include the wss:// prefix in the Server IP / host field. The Unity transport at ClientSocketWSNative.cs prepends it automatically based on the TLS toggle. Leaving it in produces wss://wss://entangle.yourgame.com and the connection fails silently.

Update workflow (recurring releases)

On dev machine

Bump the version, build, push:

git checkout feature/unity6_message_struct
git pull
docker build `
-t mahmed310/fignet-server:0.1.2 `
-t mahmed310/fignet-server:latest `
--platform linux/amd64 `
.
docker push mahmed310/fignet-server:0.1.2
docker push mahmed310/fignet-server:latest

On VPS

cd ~/FigNetServer
git pull # picks up any compose/Caddyfile updates
nano .env # bump IMAGE_TAG=0.1.2
docker compose pull
docker compose up -d # recreates the fignet container
docker compose logs -f fignet | head -50

The entangle-data volume preserves the persisted ServerConfig.yaml, AppKeys.xml, and audit log. The caddy-data volume preserves issued certs - no re-issue, no Let's Encrypt rate-limit risk.

Rollback

Every version tag you pushed is still on Docker Hub. Roll back by pointing .env at the previous tag:

nano .env # IMAGE_TAG=0.1.1
docker compose pull && docker compose up -d

Don't delete old tags from Docker Hub - keep at least the last few releases as rollback targets.

Backups

The entangle-data volume holds operational state. Add a daily cron job:

sudo crontab -e
# Append:
0 3 * * * docker run --rm -v entangle-data:/data:ro -v /root/backups:/b alpine tar czf /b/entangle-$(date +\%F).tgz /data

Sync /root/backups/ off-box (Hetzner Storage Box, S3, rsync to your laptop). Local-only backups don't survive VPS loss.

Tighten SSH source IP

Once your dev IP is stable, edit the Hetzner Cloud Firewall's port 22 rule -> restrict source to your dev IP only. Hetzner's web console works even if SSH is fully blocked, so it's a safe fallback if your IP changes.

Troubleshooting appendix

Every failure mode hit during this stack's bring-up, with the fix:

SymptomCauseFix
sysctl net.core.rmem_max not allowed in host network namespace on docker compose upA sysctls: block under the fignet service combined with network_mode: hostRemove the sysctls: block. Sysctls are set on the host by setup-vps.sh; that's the only place they belong.
Caddy crash loop with listen tcp :443: bind: address already in useAnother container or systemd service is holding 80/443 (Traefik, Apache, nginx, Photoprism stack...)docker ps and ss -tlnp | grep -E ':80|:443' to find the holder, then stop it. Strongly recommend a single-purpose VPS.
FnVoice load error: Service not registered type=FigNet.Core.IServer followed by NullReferenceExceptionImage built before FnVoiceServer/ServerConnectionListener.cs was changed to lazy-resolve IServer (it used to resolve in the constructor, before Program.cs had bound the service)Rebuild + push image from a branch that includes the fix. Bump IMAGE_TAG and docker compose pull && up -d.
FnVoice peer connects, registers, then logs Refused send messageId=60000 ... before secure session establishment and times outThe FnVoiceServer module is commented out in the persisted ServerConfig.yaml - the transport is up but no lobby handler exists for room opsEdit /var/lib/docker/volumes/fignetserver_entangle-data/_data/ServerConfig.yaml, uncomment the FnVoiceServer / FnVoiceServer.Modules.Lobby.FNELobby entry under Modules:, then docker compose restart fignet.
Unity WS error showing wss://wss://... URLServer IP field includes the wss:// prefixRemove the prefix - the transport prepends it based on the TLS toggle.
.env invisible in WinSCPHidden files are off by defaultCtrl+Alt+H to toggle hidden files.
Caddy stuck on cert issuance, no certificate obtained log lineDNS hasn't propagated, or A record points elsewheredig +short <each-subdomain> should return the VPS IP. Wait, then docker compose restart caddy.
pull access denied when running docker compose pull on VPSDocker Hub repo is set to PrivateDocker Hub -> repo Settings -> Visibility -> Public. Anonymous pull test from logged-out shell should succeed.
FD limit warnings (Too many open files) under loadContainer's nofile limit not raisedAlready set in docker-compose.yml (ulimits.nofile: 65536). If you removed it, put it back.

Critical files

  • docker-compose.yml - master profile with Caddy + host networking
  • docker/Caddyfile - TLS termination for admin + entangle + voice subdomains
  • .env.example - variable schema (template for your .env)
  • scripts/setup-vps.sh - kernel sysctls + FD limits + ufw rules
  • scripts/docker-entrypoint.sh - first-boot seeding + JwtSecret randomization
  • Dockerfile - multi-stage build (Node 20 -> SDK 9.0 -> runtime-deps 8.0)
  • .github/workflows/release.yml - optional CI publish (idle until you add DOCKERHUB_USERNAME / DOCKERHUB_TOKEN secrets)
  • Docker - generic deployment / volume / persistence reference
  • docker-compose.node.yml - node-VPS profile (separate-VPS topology, future)

End-to-end verification checklist

  • docker build -t mahmed310/fignet-server:<v> . succeeds locally
  • docker push mahmed310/fignet-server:<v> (and :latest) succeeds via Docker Desktop login
  • Image visible on Docker Hub, repo is Public
  • Anonymous docker pull works from a logged-out shell
  • Hetzner Cloud Firewall: 22/80/443 TCP + 5559/5558 UDP allowed
  • DNS A records for admin., entangle., voice. all resolve to the VPS IP
  • deploy user created, root SSH disabled, password auth disabled
  • setup-vps.sh ran; sysctl net.core.rmem_max reports 134217728
  • ufw status shows ports 22/80/443/5559/5558
  • docker compose ps shows fignet and fignet-caddy Up
  • Caddy logs show three certificate obtained successfully lines (admin, entangle, voice)
  • https://admin.yourgame.com/healthz returns {"status":"ok"}
  • ss -ulnp | grep 5559 shows EntangleServer (not docker-proxy)
  • docker compose logs fignet | grep -i fnvoice shows Module 'FnVoiceServer' ... loaded successfully with no Service not registered error after it
  • Admin password rotated, JWT randomized, ClusterSharedSecret matches .env, AppSecretKey rotated
  • Daily cron backup of entangle-data confirmed (next morning)
  • SSH source IP tightened in Hetzner Cloud Firewall