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 (port9000) and the admin panel (port8080) are reverse-proxied by Caddy onadmin.<your-domain>andgame.<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 panelgame.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:
- Change the admin password (Config -> Admin section -> Save).
- 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. - 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
| Symptom | Check |
|---|---|
| Dashboard 502 / cert errors | docker logs fignet-caddy - Let's Encrypt rate limits, DNS, port 80 reachable from internet |
| Server not responding | docker logs fignet, curl http://127.0.0.1:8080/healthz |
| Game UDP packets lost | ss -ulnp | grep 5559 (must show EntangleServer, not docker-proxy); confirm network_mode: host |
| Buffers truncated | cat /proc/sys/net/core/rmem_max should return 134217728 after setup-vps.sh |
| Node not appearing on master | match CLUSTER_SECRET exactly; check MASTER_ADDRESS reachable from node (curl https://admin.../healthz) |
| Logs missing | logs 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 -
ClusterSharedSecretset to a real value (Config UI orFIGNET_CLUSTER_SECRETenv) -
JwtSecretis no longerCHANGE_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/healthzreturns{"status":"ok"}
Image size and build notes
- Final image: ~210 MB (runtime-deps + self-contained .NET + libenet + static dashboard).
PublishSingleFile=falseis mandatory - FigNet loads modules at runtime fromAppContext.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*.*.*) withcache-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-serverby default) - nodocker loginrequired on the VPS - Manual publish from your dev machine via Docker Desktop / CLI; the optional CI workflow at
.github/workflows/release.ymlis 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
dockerCLI is also authenticated - no separatedocker loginneeded) - The repo cloned locally on a branch that ships the unified
Dockerfile(feature/unity6_message_structat 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:
- Add Server -> Location: pick the region closest to your players
- Image: Ubuntu 24.04 LTS (or current LTS)
- Type: at least CX31 (4 vCPU / 8 GB / 160 GB) for a master running both Entangle and FnVoice
- SSH keys: add your public key (
~/.ssh/id_ed25519.pubfrom the dev box). Don't use root password auth. - 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:
| Protocol | Port | Source | Purpose |
|---|---|---|---|
| TCP | 22 | Any IPv4 + IPv6 | SSH (we'll tighten this to your dev IP only later, see Tighten SSH source IP below) |
| TCP | 80 | Any | Caddy HTTP / Let's Encrypt ACME challenge |
| TCP | 443 | Any | Caddy HTTPS - admin + Entangle WSS + FnVoice WSS |
| UDP | 5559 | Any | Entangle ENet (direct, no proxy) |
| UDP | 5558 | Any | FnVoice 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
ufwlater? Defense in depth. Hetzner's FW protects the host network interface;ufwprotects against any in-VM process bypassing it from inside.
DNS
At your domain registrar, add three A records pointing at the VPS public IPv4:
| Subdomain | Target | What 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
scpthe 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 thefignetservice indocker-compose.yml.network_mode: hostshares the host network namespace and Docker refuses to set those sysctls from a container -docker compose up -dwill fail withsysctl ... not allowed in host network namespace. The host already has them set bysetup-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, thendocker stop <name>). Caddy will crash-loop onbind: address already in useotherwise. 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:
Listening port=5559(Entangle ENet)Listening port=5558(FnVoice ENet)Listening side=server port=9000(Entangle WSS)Listening side=server port=9001(FnVoice WSS)Module 'Entangle' (version 1.0.0) loaded successfullyModule 'FnVoiceServer' (version 1.0.0) loaded successfully- criticalAdmin 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
AppSecretKeyinServerConfig.yamlfrom123to a real per-app secret. Update the Unity client'sAppSecretKeyto match. - Confirm
ClusterSharedSecretinServerConfig.yamlmatches.envCLUSTER_SECRET. - Confirm JwtSecret was randomized on first boot:
If you seedocker compose logs fignet | grep -i 'Generated random JwtSecret'
CHANGE_THIS_SECRET_IN_PRODUCTIONstill 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:The/var/lib/docker/volumes/fignetserver_entangle-data/_data/ServerConfig.yamlModules:block must include:- AssemblyName: FnVoiceServerType: 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:
| Module | Server IP / Host | Port | Transport | TLS/WSS |
|---|---|---|---|---|
| Entangle (UDP, default) | <VPS public IP> | 5559 | ENet | off |
| Entangle (WSS fallback) | entangle.yourgame.com | 443 | WebSocket | on |
| FnVoice (UDP, default) | <VPS public IP> | 5558 | ENet | off |
| FnVoice (WSS fallback) | voice.yourgame.com | 443 | WebSocket | on |
Don't include the
wss://prefix in the Server IP / host field. The Unity transport atClientSocketWSNative.csprepends it automatically based on the TLS toggle. Leaving it in produceswss://wss://entangle.yourgame.comand 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:
| Symptom | Cause | Fix |
|---|---|---|
sysctl net.core.rmem_max not allowed in host network namespace on docker compose up | A sysctls: block under the fignet service combined with network_mode: host | Remove 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 use | Another 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 NullReferenceException | Image 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 out | The FnVoiceServer module is commented out in the persisted ServerConfig.yaml - the transport is up but no lobby handler exists for room ops | Edit /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://... URL | Server IP field includes the wss:// prefix | Remove the prefix - the transport prepends it based on the TLS toggle. |
.env invisible in WinSCP | Hidden files are off by default | Ctrl+Alt+H to toggle hidden files. |
Caddy stuck on cert issuance, no certificate obtained log line | DNS hasn't propagated, or A record points elsewhere | dig +short <each-subdomain> should return the VPS IP. Wait, then docker compose restart caddy. |
pull access denied when running docker compose pull on VPS | Docker Hub repo is set to Private | Docker Hub -> repo Settings -> Visibility -> Public. Anonymous pull test from logged-out shell should succeed. |
FD limit warnings (Too many open files) under load | Container's nofile limit not raised | Already 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 networkingdocker/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 rulesscripts/docker-entrypoint.sh- first-boot seeding + JwtSecret randomizationDockerfile- multi-stage build (Node 20 -> SDK 9.0 -> runtime-deps 8.0).github/workflows/release.yml- optional CI publish (idle until you addDOCKERHUB_USERNAME/DOCKERHUB_TOKENsecrets)- 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 pullworks 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 -
deployuser created, root SSH disabled, password auth disabled -
setup-vps.shran;sysctl net.core.rmem_maxreports134217728 -
ufw statusshows ports 22/80/443/5559/5558 -
docker compose psshowsfignetandfignet-caddyUp - Caddy logs show three
certificate obtained successfullylines (admin, entangle, voice) -
https://admin.yourgame.com/healthzreturns{"status":"ok"} -
ss -ulnp | grep 5559showsEntangleServer(notdocker-proxy) -
docker compose logs fignet | grep -i fnvoiceshowsModule 'FnVoiceServer' ... loaded successfullywith noService not registerederror after it - Admin password rotated, JWT randomized,
ClusterSharedSecretmatches.env,AppSecretKeyrotated - Daily cron backup of
entangle-dataconfirmed (next morning) - SSH source IP tightened in Hetzner Cloud Firewall