WoW Forever beta

Server in Docker inside an LXC container, your own certificates, launcher and port forwarding.

Updated Oct 28, 2026#wow #docker #proxmox Based on the jibay442/forever_docker repository

The WoW Forever server (beta, client 1.60.1.70450) runs in Docker inside an LXC container on a home Proxmox host. Players connect to [server address] through the launcher.

It is based on the jibay442/forever_docker repository with the forever branch of TrinityCore. The Proxmox container has the address [local IP] on the internal network.

Docker containerWhat it doesPublic port
bnetserverBattle.net login over TLS, REST for the launcher1119, 8081
worldservergame world8085
mysqldatabases auth, characters, world, hotfixesnone
webregistration site and launcher download127.0.0.1:8088 only

Requirements#

The install needs about 80 GB of disk and 8 GB of RAM. Once the client is removed, the server takes about 28 GB.

Build
8 GB RAM, 4+ cores
per the repository README
Running
4 GB RAM, 2 cores
Disk for the install
client ~48 GB + server ~28 GB
Disk after the install
~28 GB
the client is removed
Client
build 1.60.1.70450
World of Warcraft folder with .build.info, Data and _classic_beta_, product wow_classic_beta
Open ports (TCP)
1119, 8081, 8085
Domain
[server address]
A record pointing to your external IP
Player's PC
Windows
launcher, administrator rights for the first setup

In our case the container got 8 cores, 8 GB of RAM and 98 GB of disk.

1

LXC in Proxmox#

Docker won't start inside LXC without nesting enabled, so first set up the container on the Proxmox host.

Check from inside the container that it is LXC (and not a full VM):

shell
systemd-detect-virt

On the Proxmox host (replace <CTID> with your container number), enable nesting, set 8 GB of swap and reboot the container:

shell
pct set <CTID> --features nesting=1,keyctl=1 --swap 8192 && pct reboot <CTID>

This is the only way to set swap in LXC: a swapfile can't be created inside the container. After the build you can reduce cores and memory; in LXC this applies without a reboot:

shell
pct set <CTID> --cores 2 --memory 4096
2

SSH keys#

Debian forbids root password login by default, so SSH needs a key. If a correct password is reported as wrong, this is why.

On the PC (PowerShell), create a separate key pair without touching your existing keys, and print the public part:

powershell
ssh-keygen -t ed25519 -f $env:USERPROFILE\.ssh\forever_vps -C "forever-vps"
Get-Content $env:USERPROFILE\.ssh\forever_vps.pub

On the server, in the container console from Proxmox, add the public key (replace PASTE_KEY with the whole line):

shell
mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo 'PASTE_KEY' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys

Check the login from the PC:

powershell
ssh -i $env:USERPROFILE\.ssh\forever_vps root@[local IP]

The private file forever_vps stays on the PC only.

3

Docker and the repository#

One command installs Docker with the official script, installs tmux, clones the repository and creates a starter .env. Everything runs as root.

shell
apt update && apt install -y git curl tmux && curl -fsSL https://get.docker.com | sh && git clone https://github.com/jibay442/forever_docker ~/forever && cd ~/forever && cp .env.example .env

Check that Docker works inside the container (an error about runc here means nesting isn't enabled):

shell
docker run --rm hello-world

Run long commands inside tmux so a dropped SSH session doesn't interrupt them. Detach: Ctrl+B, then D. Come back with:

shell
tmux attach -t forever
4

Client to the server#

The extractor needs the whole client root (about 48 GB), so copy the entire folder. Over a gigabit network this took about 13 minutes.

In PowerShell on the PC (change the path to your own; don't create /root/wow on the server beforehand, scp creates it):

powershell
scp -i $env:USERPROFILE\.ssh\forever_vps -r "D:\Games\test" root@[local IP]:/root/wow

If the network is Wi-Fi or unstable, use WinSCP: it can resume, while scp starts over after a dropped connection.

Check on the server: the root must contain .build.info, Data and _classic_beta_, and the size must match the folder on the PC:

shell
ls -la /root/wow && ls /root/wow/_classic_beta_ | head && du -sh /root/wow

.build.info must contain version 1.60.1.70450 and product wow_classic_beta:

shell
grep -a -E "70450|wow_classic_beta" /root/wow/.build.info | head -n 2
5

Configure .env#

One command generates three passwords and sets the address, the client path and the build options. Replace [server address] and [local IP] with your own values. The address in ADDR ends up in the certificates and in the launcher, so it's best not to change it after gen-certs.sh.

shell
ADDR=[server address] && P(){ tr -dc 'a-zA-Z0-9' </dev/urandom | head -c 24; } && cd ~/forever && sed -i "s|^MYSQL_ROOT_PASSWORD=.*|MYSQL_ROOT_PASSWORD=$(P)|" .env && sed -i "s|^DB_PASSWORD=.*|DB_PASSWORD=$(P)|" .env && sed -i "s|^SOAP_PASSWORD=.*|SOAP_PASSWORD=$(P)|" .env && sed -i "s|^PUBLIC_ADDRESS=.*|PUBLIC_ADDRESS=$ADDR|" .env && sed -i "s|^REALM_LOCAL_ADDRESS=.*|REALM_LOCAL_ADDRESS=[local IP]|" .env && sed -i "s|^BUILD_JOBS=.*|BUILD_JOBS=3|" .env && sed -i "s|^WOW_DIR=.*|WOW_DIR=/root/wow|" .env && sed -i "s|^HOST_UID=.*|HOST_UID=0|" .env && sed -i "s|^HOST_GID=.*|HOST_GID=0|" .env && sed -i "s|^TZ=.*|TZ=Europe/London|" .env && grep -vE '^#|^$' .env
OptionValueWhy
PUBLIC_ADDRESS[server address]the address the server gives to players
REALM_LOCAL_ADDRESS[local IP]players on the home network get the local address
BUILD_JOBS3the README says about 2 GB of RAM per job, so 3 is safe with 8 GB
WOW_DIR/root/wowthe folder with Data and .build.info
HOST_UID, HOST_GID0everything runs as root; with 1000 the container can't read /root
EXTRACT_MMAPS, MMAP_PATHFINDING0mmaps take a long time to generate and can be added later

Don't copy the passwords from the command output into chats or documents.

6

Fixes that were needed#

For us, two things would have broken the build and the start without intervention. Make these changes before step 7.

Patches. docker/patches/ holds fixes on top of the forever branch; the build fails on purpose if one of them no longer applies. For us 0002-classic-surname-invite-and-names (group invites and names with surnames) and 0004-classic-360-action-buttons (360 action bar slots for gamepads) didn't apply. We moved them to a backup:

shell
mkdir -p ~/patches-backup && mv ~/forever/docker/patches/0002-classic-surname-invite-and-names.patch ~/forever/docker/patches/0004-classic-360-action-buttons.patch ~/patches-backup/

If the build fails on another patch, the error has the line applying /patches/<name> and patch does not apply: move it the same way. The server starts without these patches, but the related features work as they do in the branch without the fixes.

extra_hosts. Docker accepts only IPs in extra_hosts, but docker-compose.yml puts PUBLIC_ADDRESS there, which is a domain. The start fails with invalid IP address in add-host. Change this line to REALM_LOCAL_ADDRESS, backing up the file first:

shell
cd ~/forever && cp docker-compose.yml ~/docker-compose.yml.bak && sed -i 's|trinity.actual.battle.net:${PUBLIC_ADDRESS:-127.0.0.1}|trinity.actual.battle.net:${REALM_LOCAL_ADDRESS:-127.0.0.1}|' docker-compose.yml && grep -n 'trinity.actual.battle.net' docker-compose.yml

The output must show ${REALM_LOCAL_ADDRESS:-127.0.0.1} after the colon. This entry is only needed so that bnetserver can resolve the name trinity.actual.battle.net at startup. After the change the file differs from the repository, so git pull may conflict: in that case restore the backup and run the command again.

7

Certificates, build, data, start#

The whole chain runs as one command inside tmux. The longest part, compiling TrinityCore, took us about 20 minutes with three jobs.

shell
tmux new -s forever

Inside tmux:

shell · inside tmux
cd ~/forever && ./scripts/gen-certs.sh && docker compose build && docker compose run --rm extractor && docker compose up -d && docker compose logs -f worldserver

What happens, in order:

  1. gen-certs.sh creates your root CA and the server certificate in ~/forever/certs.
  2. docker compose build builds the forever-trinitycore and forever-web images.
  3. extractor reads the client and puts the data in ~/forever/data; the output must end with all done and the sizes of dbc, maps, vmaps.
  4. docker compose up -d starts the containers; on first start worldserver fills the databases (a few minutes).

Ready means the worldserver log has the line World initialized, then ready.... Leave the logs with Ctrl+C; the containers keep running. Check:

shell
cd ~/forever && docker compose ps

All four containers must be Up, and mysql and web also healthy.

Log noise to ignore: TransmogOutfitSlotOption.db2: skipping ID ... and Received not handled opcode .... DBErrors.log may weigh about 150 MB after the first start.

8

Account and cleanup#

The game login is a Battle.net account, so its name must look like an email. A real address isn't needed; the server sends no mail.

Enter the worldserver console:

shell
cd ~/forever && docker compose attach worldserver

In the console (change the email and the password; a password without quotes and spaces is best):

worldserver console
bnetaccount create mail@example.com Password

Leave the console with Ctrl+P, then Ctrl+Q. Not Ctrl+C: that stops worldserver. You can create the same kind of account on the registration site, which listens on 127.0.0.1:8088.

Once the data has been extracted, the client on the server is no longer needed. Make sure the data is in place first, then delete:

shell
ls ~/forever/data && du -sh ~/forever/data && rm -rf /root/wow && df -h /

data must contain cameras, dbc, gt, maps, vmaps (about 716 MB for us). Mmaps weren't created, so mobs move without terrain navigation. You can add them later, but the client has to be copied to the server again for that.

9

DNS and router ports#

Players from the internet need two things: a DNS record for your external IP and three TCP ports forwarded to the container.

Port (TCP)ServicePurpose
1119bnetserverBattle.net login over TLS
8081bnetserverREST: launcher login, game ticket
8085worldserverthe game itself
  1. In DNS, create an A record for [server address] pointing to your external IP (if your provider changes the IP, use DDNS).
  2. On the router, forward ports 1119, 8081 and 8085 to [local IP], with the same external and internal ports.
  3. Pin this address to the container in the router's DHCP, otherwise forwarding breaks when the address changes.

Don't open 3306 (MySQL), 7878 (SOAP) or 3443 (remote access) to the outside.

Check from the PC (PowerShell):

powershell
nslookup [server address]
Test-NetConnection [server address] -Port 1119
Test-NetConnection [server address] -Port 8081
Test-NetConnection [server address] -Port 8085

All three ports must show TcpTestSucceeded : True. Many routers have no NAT loopback, so from the home network the check by domain may fail even though it works for everyone else. From inside, use the launcher's Home network mode with the address [local IP].

10

Launcher and connecting#

The client connects to the name trinity.actual.battle.net over TLS, so it can't reach the server without the launcher or manual PC setup, and there's no realmlist to edit.

What our custom launcher does when you press Play:

  1. Writes the portal trinity.actual.battle.net:1119 to a separate game config and leaves the main Config.wtf alone.
  2. Resolves the domain from the "Server" field and writes a marked line <IP> trinity.actual.battle.net to hosts.
  3. After you agree, installs your root CA as trusted (it shows the SHA-256 and the name Forever Private Root CA).
  4. Starts _classic_beta_\WowB.exe and swaps the server key in the process memory; game files on disk don't change.

Modes: Internet (address from the domain), Home network (local [local IP], for playing from your own network) and Auto. Administrator rights are requested separately, only to write to hosts and the certificate store.

Before agreeing to install the CA, compare the SHA-256 in the window with the real one on the server:

shell
openssl x509 -in ~/forever/certs/ca/forever_root_ca.crt -noout -fingerprint -sha256

The launcher only needs the public forever_root_ca.crt. The private keys forever_root_ca.key and forever.key.pem never leave the server. To get the file on the PC:

powershell
scp -i $env:USERPROFILE\.ssh\forever_vps root@[local IP]:/root/forever/certs/ca/forever_root_ca.crt $env:USERPROFILE\Downloads\forever_root_ca.crt

The Remove changes button removes the hosts entry and the installed certificate. Check after the rollback (the commands must print nothing):

powershell
Select-String -Path C:\Windows\System32\drivers\etc\hosts -Pattern "trinity.actual.battle.net"
Get-ChildItem Cert:\CurrentUser\Root, Cert:\LocalMachine\Root | Where-Object { $_.Subject -match "Forever" }

If you regenerate the certificates or change PUBLIC_ADDRESS, the launcher has to be rebuilt with the new forever_root_ca.crt. The forever_docker repository also has a ready path: a fork on GitHub, the variables FOREVER_CA_CERT and FOREVER_SERVER_ADDRESS, and a build through Actions (the Publish the launcher section of its README).

Maintenance#

All commands run in ~/forever on the server.

TaskCommand
Statusdocker compose ps
Live world and login logsdocker compose logs -f worldserver bnetserver
Stop everythingdocker compose stop
Start againdocker compose up -d
Restart one servicedocker compose restart worldserver
World consoledocker compose attach worldserver (leave with Ctrl+P, Ctrl+Q)

Update (per the repository README; make sure beforehand that the patches from step 6 still apply, otherwise the build stops on them):

shell
cd ~/forever && docker compose build --no-cache && docker compose up -d

Database backup. The MySQL root password comes from the container environment, so it isn't in the command:

shell
cd ~/forever && docker compose exec -T mysql sh -c 'mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --all-databases' > ~/forever-backup-$(date +%F).sql

Separately save the ~/forever/certs folder (certificates and keys) and the ~/forever/.env file. Without the CA keys you'd have to regenerate the certificates and rebuild the launcher.

Disk space. The Docker build cache can be cleared, but the next rebuild then starts from scratch:

shell
docker builder prune -f && df -h /

DBErrors.log can grow to hundreds of megabytes. To empty it:

shell
cd ~/forever && docker compose exec worldserver sh -c ': > /opt/trinitycore/logs/DBErrors.log'

Mmaps later. Copy the client to the server again, set EXTRACT_MMAPS=1 in .env, run docker compose run --rm extractor, then set MMAP_PATHFINDING=1 and restart the world.

Security#

The main risk here is a single one: a leaked CA private key lets someone issue certificates trusted by the PCs of everyone who installed your root certificate.

  • forever_root_ca.key and forever.key.pem live only on the server. Don't copy them to a PC or paste them into chats and repositories.
  • Find out whether the CA is limited by names. If there's no output, it isn't limited, and it's better not to hand such a CA out to friends.
shell
openssl x509 -in ~/forever/certs/ca/forever_root_ca.crt -noout -ext nameConstraints
  • ~/forever/.env contains the database and SOAP passwords. Don't send it to anyone.
  • Only 1119, 8081 and 8085 are open to the outside. Don't forward 3306, 7878 and 3443.
  • Lines like SSL Handshake failed http request from unknown addresses in the bnetserver log mean scanners are probing the open ports. It's noise, but it confirms the ports are visible from the internet.
  • Keep SSH key-only on the server; for a VPS open to the internet, add fail2ban.
  • When you're not playing, press Remove changes in the launcher: the extra root certificate is only needed on the PC while you play.

Blizzard's rules for the beta client and the possible consequences of swapping the key in memory for your main Battle.net account are yours to assess: this can't be checked in advance.

Troubleshooting#

Most problems from this install fit the table below: the first column is what you see, the second is the cause, the third is what to do.

SymptomCauseFix
SSH says Permission denied with the correct passwordRoot password login is forbidden by defaultLog in with a key (step 2)
Docker fails with an error about runcNesting isn't enabled in LXCpct set <CTID> --features nesting=1,keyctl=1 and a restart (step 1)
swapfile can't be created in the containerSwap in LXC is set by the hostpct set <CTID> --swap 8192 (step 1)
The build fails: patch does not applyThe branch moved on and the patch no longer fitsMove this patch out of docker/patches/ (step 6)
The build stops with no message, the log has KilledOut of memorySet BUILD_JOBS=2 in .env and repeat the build
invalid IP address in add-hostA domain got into extra_hosts instead of an IPFix the line in docker-compose.yml (step 6)
Could not resolve LoginREST.ExternalAddress in bnetservertrinity.actual.battle.net doesn't resolve inside the containerCheck extra_hosts in the compose file
Launcher: nothing in the bnetserver logThe client didn't reach the serverCheck hosts, port 1119, the network mode (Internet or Home network)
SSL Handshake failed from your address in the bnetserver logThe client doesn't trust the certificateCheck that the CA is installed, compare the fingerprint
Login works, then "You have been disconnected"Failure after loginWatch the worldserver and bnetserver logs during the attempt, check realmlist and port 8085
Can't connect from home by domain, works for friendsThe router has no NAT loopbackHome network mode and the address [local IP]

Check the realm address in the database:

shell
cd ~/forever && docker compose exec -T mysql sh -c 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SELECT id,name,address,localAddress,localSubnetMask,port FROM auth.realmlist"'

Logs from the container's disk:

shell
cd ~/forever && docker compose exec worldserver sh -c 'tail -n 40 /opt/trinitycore/logs/Bnet.log; echo ======; tail -n 60 /opt/trinitycore/logs/Server.log'

Log files are in /opt/trinitycore/logs: Bnet.log, Server.log, DBErrors.log, GM.log.