Status: first full run works (2026-10-13), guide still being cleaned up. This is the record of setting up a clean public server on a fresh DigitalOcean droplet and connecting to it from a separate Windows PC. Every command here was run for real; problems found along the way are listed under Findings. It will become the Linux guide once the first full run works, and a Windows guide follows from it.
Test machine: DigitalOcean s-2vcpu-4gb-amd (2 vCPU, 4 GB RAM, 80 GB disk), Ubuntu 24.04.5 LTS,
branch forever at 669c8de4ec.
A password login works for a person, but a key is needed for scripted work. On the Windows PC (Git Bash or PowerShell, OpenSSH is built into Windows 10+):
ssh-keygen -t ed25519 -C "forever-droplet" -f ~/.ssh/forever_droplet
Install the public key on the server (asks for the root password once):
Get-Content $env:USERPROFILE\.ssh\forever_droplet.pub | ssh root@SERVER_IP "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys"Then: ssh -i ~/.ssh/forever_droplet root@SERVER_IP.
Long jobs (the build takes hours) must survive a dropped SSH connection. All commands below were run in one shared, logged screen session:
screen -dmS forever -L -Logfile /root/forever-screen.log
screen -x forever # attach (several people can watch at once)
Detach with Ctrl+A then D. Do not press Ctrl+C or type exit inside it - that stops the
running job. Scroll back: Ctrl+A, Esc, arrows/PgUp, Esc to return.
GCC needs more than 4 GB on the largest files with -j2. Without swap the build gets killed.
fallocate -l 8G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo "/swapfile none swap sw 0 0" >> /etc/fstab
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y git clang cmake make gcc g++ libmysqlclient-dev libssl-dev libbz2-dev \
libreadline-dev libncurses-dev libboost-all-dev mysql-server p7zip-full
Versions installed: CMake 3.28.3 (needs 3.24+), GCC 13.3.0, Boost 1.83 (needs 1.78+), MySQL 8.0.46.
cd /root
git clone --branch forever --single-branch --depth 1 https://github.com/advocaite/TrinityCore.git TrinityCore
(~560 MB download, 4.9 GB on disk.)
mkdir -p /root/TrinityCore/build && cd /root/TrinityCore/build
cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/root/server -DTOOLS=0 -DWITH_WARNINGS=0
make -k -j2 2>&1 | tee /root/build.log
Release: debug info makes linking much heavier on a 4 GB machine.TOOLS=0: the map extractors run on Windows next to the client, not on the server.-j2on 2 vCPU / 4 GB + swap.-kkeeps going after a compile error so one run shows all errors.- Build time on this droplet: 48 minutes, no compile errors with GCC 13.3.
Then make install (puts bin/, etc/*.conf.dist and lib/ under /root/server).
Extracted on the Windows PC with the client (see README "Game data"), then copied to the server.
Needed: dbc, gt, maps, vmaps, cameras. No mmaps (1.6 GB, not needed; mmap.enablePathFinding = 0).
tar -cf - -C "PATH/TO/DATADIR" dbc gt maps vmaps cameras | ssh root@SERVER_IP "mkdir -p /root/server/data && tar -xf - -C /root/server/data"
(~680 MB.)
All four databases are created by worldserver's auto-setup on the first start (section 10). world + hotfixes come from the Forever database release: download it into the bin folder and unpack it there.
cd /root/server/bin
wget https://github.com/advocaite/TrinityCore/releases/download/forever-tdb-2026-10-14/TDB_full_forever_2026_10_14.7z
7z x TDB_full_forever_2026_10_14.7z
(On the first test run, before that release existed, world + hotfixes were a mysqldump of the
dev database imported with zcat forever_db.sql.gz | mysql. That dev DB turned out to hold
leftovers of older SQL file versions and test edits; the release is built from source instead,
see "Forever database release" below.)
MySQL user (password generated and kept in a root-only file; MySQL on Ubuntu already listens on 127.0.0.1 only, which is what we want):
P=$(openssl rand -hex 16); umask 077; echo "$P" > /root/.forever_db_pass
mysql -e "CREATE USER trinity@localhost IDENTIFIED BY '$P';
GRANT ALL PRIVILEGES ON world.* TO trinity@localhost; GRANT ALL PRIVILEGES ON hotfixes.* TO trinity@localhost;
GRANT ALL PRIVILEGES ON auth.* TO trinity@localhost; GRANT ALL PRIVILEGES ON characters.* TO trinity@localhost;"
- Build world + hotfixes from source into empty databases: TrinityCore's TDB 1210.26091 files in
the working folder, a worldserver config pointing at the new database names with
Updates.EnableDatabases = 12(world + hotfixes only), thenworldserver -u(update and exit). Took 6 minutes on the droplet. - Compare with the dev database table by table (CHECKSUM TABLE, then
EXCEPTrow counts). - Copy the website's generated
armory_icon/armory_item/armory_nametables from dev. - Dump each database on its own (no
CREATE DATABASE/USE, so renamed databases work) withoutupdates_apply_check:mysqldump --single-transaction --hex-blob --set-gtid-purged=OFF --no-tablespaces --ignore-table=DB.updates_apply_check DB - Test: a new setup from the two files alone must import them and then apply nothing.
- Name them
TDB_full_{world,hotfixes}_forever_<date>.sql, set the names inrevision_data.h.in.cmake, publish as a GitHub release.
Configs from the .dist files with only the needed keys changed:
sh contrib/linux/configure_server.sh /root/server SERVER_IP "$(cat /root/.forever_db_pass)"
Certificate: the server certificate chain + key for trinity.actual.battle.net (NOT the root CA
key, that never leaves the PC that made it):
scp trinity_forever.cert.pem root@SERVER_IP:/root/server/bin/forever.cert.pem
scp trinity_forever.key.pem root@SERVER_IP:/root/server/bin/forever.key.pem
chmod 600 /root/server/bin/forever.key.pem
The server must resolve the login hostname itself (Finding 2):
echo "SERVER_IP trinity.actual.battle.net" >> /etc/hosts
Firewall (SSH first, so you do not lock yourself out):
ufw allow OpenSSH
ufw allow 1119/tcp # bnetserver
ufw allow 8081/tcp # login REST (LoginREST.Port)
ufw allow 8085/tcp # worldserver
ufw --force enable
MySQL (3306), Remote Access (3443) and SOAP (7878) stay closed.
worldserver, in its own screen window, from the bin folder:
cd /root/server/bin && ./worldserver -c /root/server/etc/worldserver.conf
Answer yes to create auth and characters (Finding 7). It then applies all updates.
"World initialized in 2 minutes 38 seconds" on the droplet.
Point the realm at the public IP (Finding 4; localAddress stays 127.0.0.1):
mysql -e "UPDATE auth.realmlist SET address='SERVER_IP' WHERE id=70"
bnetserver, in another screen window, from the bin folder (Finding 10):
cd /root/server/bin && ./bnetserver -c /root/server/etc/bnetserver.conf
Log shows Added realm "Trinity" at SERVER_IP 127.0.0.1 (port 8085). ss -ltnp shows 1119, 8081
(bnetserver) and 8085 (worldserver) on 0.0.0.0.
Check from the client PC before starting the game (both must say Verify return code: 0 (ok)):
openssl s_client -connect SERVER_IP:8081 -servername trinity.actual.battle.net -CAfile root_ca.cert.pem
openssl s_client -connect SERVER_IP:1119 -servername trinity.actual.battle.net -CAfile root_ca.cert.pem
PowerShell: Test-NetConnection SERVER_IP -Port 8085 must say TcpTestSucceeded : True.
Account (worldserver console, i.e. the screen window running worldserver):
bnetaccount create EMAIL PASSWORD
account set gmlevel 1#1 3 -1 # optional, GM for the first game account
On the client PC, the only change is the hosts entry (edit as Administrator; keep a copy first):
SERVER_IP trinity.actual.battle.net
then ipconfig /flushdns, and start the game with the Forever Launcher as usual. The PC must
trust the root CA that signed the server certificate (same as for a local server).
Result 2026-10-13: works. Logged in from a home PC, created a character, entered the world (server saw the home IP, not 127.0.0.1).
Memory on the 4 GB droplet with one player in the world: 3.6 GB RAM used plus 1.7 GB swap. One realm fits but is tight; more realms or players need 8 GB.
Problems that explain why connecting to an outside server fails, and anything that differed from the Windows dev setup.
- The repack is local-only. Its
bnetserver.confandworldserver.confboth setBindIP = "127.0.0.1", so the servers do not accept connections from any other machine. Hosting the repack for others needsBindIP = "0.0.0.0"in both (plus items 2-4). - The server itself needs the hosts entry.
LoginREST.ExternalAddressandLoginREST.LocalAddressare not bind addresses (that isBindIP); they are the hostnames sent back to clients. But bnetserver resolves both at startup and refuses to start if either does not resolve ("Could not resolve LoginREST.ExternalAddress", LoginRESTService.cpp).trinity.actual.battle.netdoes not exist in public DNS (verified on the droplet), so the server needsSERVER_PUBLIC_IP trinity.actual.battle.netin its own/etc/hosts(Windows:C:\Windows\System32\drivers\etc\hosts). LoginREST.ExternalAddressmust stay the hostname, not the IP: the client opens TLS to that name and the certificate only coverstrinity.actual.battle.net/*.actual.battle.net.- realmlist address:
auth.realmlist.addressmust be the public IP, or clients are sent to 127.0.0.1 after login. To verify. Network.EnterEncryptedModeRegionGroupis not inworldserver.conf.dist. It defaults to 0; the client needs 8. Only the README mentions it.contrib/linux/configure_server.shadds it.- SQL file hashes differed between Windows and Linux (fixed in the updater). The world
baselines (
2026_09_27_00_world_forever_baseline_01/02.sql) contain CRLF bytes inside text strings in their git copy.UpdateFetcher::ReadSQLUpdateread files in text mode, which turns CRLF into LF only on Windows, so Windows stored hash2AB2CF9and Linux computed6FDF0A5. A database made on Windows (dev, repack, our dump) therefore made a Linux worldserver re-run the 65 MB baseline: it failed witherror 1136 ... Column count doesn't match value countafter already changing rows (creature 79422 -> 77149), and if it had succeeded it would have re-run every later custom file too. First fix (#52) hashed CRLF->LF everywhere, which then broke databases built on Linux (they stored the raw hash) the same way. Final fix: the updater accepts either hash (CRLF->LF or raw) as "already applied" and only re-hashes, and the baselines are never applied again at all (a changed baseline is logged and re-hashed), because they are not safe to re-run on a finished database. Tested on the droplet with a Windows-made DB, a Linux-made DB, a changed baseline and a changed custom file. After a failed run like this, rebuild or re-import the world database before starting again. - First start asks questions. With
Updates.AutoSetup = 1, worldserver asksDatabase "auth" does not exist, do you want to create it?(and the same forcharacters). Run the first start in the foreground (screen) and answeryes. - worldserver needs the vmaps to start. Without
vmaps/GameObjectModels.dtreeit stops with "Unable to load gameobject models ... server shutting down". Upload all game data before the first start (the database setup part of that first start still completed). AlsoClassic.Hardcoreis missing fromworldserver.conf.dist(only a warning, default 0 is right for a Normal realm). - About a million DB warnings on every start. With the
.distlogger setting (Logger.sql.sql = 5,Console DBErrors) they all scroll through the console and make it look broken. They are the same on the dev server (retail rows pointing at creature displays, factions, items etc. that the Classic client data does not have; the server skips them).configure_server.shsetsLogger.sql.sql = 5,DBErrors(file only), like the dev and repack configs. - Every repack has its own certificate authority. The repack's Setup.bat creates a root CA
for that one PC (and deletes its key), so a friend's PC does not trust the host's login
certificate unless the host's
server\forever_root_ca.crtis imported there. This test only worked without that step because the client PC already trusted the CA that signed the server certificate. The repack'ssetup\host-for-others.batnow builds a friend kit that does it. - Certificate paths are relative to the working directory.
CertificatesFile = "./forever.cert.pem"means bnetserver must be started from/root/server/bin.