A Helm chart for CMaNGOS, a World of Warcraft server. A release runs one of three game versions: Classic (1.12.1), The Burning Crusade (2.4.3), or Wrath of the Lich King (3.3.5a).
The chart runs MySQL, realmd (the login server), and mangosd (the world server). Two Jobs prepare the data: one installs and migrates the databases, and one extracts the map data from your game client.
CMaNGOS publishes no container images, so this repository builds them from source. The images include the playerbots module and the auction house bot. Both are off by default, and you can turn them on in the chart values.
You need a Kubernetes cluster, Helm 3.8 or later, and a game client of the expansion that you want to run.
-
Write a values file. It tells the chart the expansion, where the client is, and which account to create. This example reads the client from a PVC. For the other options, see Client data.
# values.local.yaml expansion: classic # classic, tbc or wotlk clientData: extract: clientVolume: persistentVolumeClaim: claimName: wow-client # holds the folder that contains Data/ readOnly: true dbInit: accounts: - username: admin password: change-me gmlevel: 3
-
Install the chart:
helm install cmangos oci://ghcr.io/christianjacobsen/charts/cmangos \ -n cmangos --create-namespace -f values.local.yaml kubectl -n cmangos get pods -w
-
Wait for the two Jobs to complete. Then
mangosdstarts. -
Make sure that the chart works:
helm test cmangos -n cmangos --logsThe test connects to both servers. If
dbInit.accountshas an account, the test also logs in with it, and the realm must be online. -
In the client folder, set
realmlist.wtfto the address of therealmdservice. Then log in asadmin.
On an 8-core machine, the first Classic install took about 17 minutes, and the mmaps (navigation meshes) took 14 of them. With 4 mmap threads on a 10-core machine, the extraction took 25 minutes for TBC and 38 minutes for WotLK. Slower nodes take hours for the mmaps.
The chart values pin the images that the CI of this repository publishes to ghcr.io/christianjacobsen. To build your own images, see CONTRIBUTING.md.
The expansion value selects the game version:
expansion |
Game version | Client build | World database |
|---|---|---|---|
classic (default) |
Classic 1.12.1 | 5875 | classic-db |
tbc |
The Burning Crusade 2.4.3 | 8606 | tbc-db |
wotlk |
Wrath of the Lich King 3.3.5a | 12340 | wotlk-db |
Each expansion has its own images, for example cmangos-tbc-server and cmangos-tbc-db. The expansion also sets the default database names, for example tbcmangos and tbcrealmd.
To run more than one expansion, install one release for each expansion. If you change expansion on an existing release, the client-data Job stops with an error, because the data volume holds the data of the old expansion.
| Component | Kind | Purpose |
|---|---|---|
mysql |
StatefulSet | MySQL 8.4 with the four CMaNGOS databases (optional, see Database) |
db-init |
Job | Creates the databases, installs the world content, applies the SQL updates, and creates accounts |
client-data |
Job | Fills the data volume with dbc, maps, vmaps, and mmaps |
realmd |
Deployment and Service | The login server (port 3724) |
mangosd |
Deployment and Service | The world server (port 8085) |
The server pods wait until the Jobs of their release revision are complete.
mangosd needs four kinds of data from the game client:
- dbc files: game tables such as spells and items.
- maps: terrain height.
- vmaps: buildings and other models, for line of sight.
- mmaps: navigation meshes, for creature path finding.
CMaNGOS has no download of this data, so the chart extracts it from your client. Two volumes take part: the client volume, which the Job reads, and the data volume, which the Job fills and mangosd reads.
The clientData.source value selects how the Job fills the data volume:
source |
What the Job does |
|---|---|
extract (default) |
Runs the extractors against your client (clientVolume or clientUrl) |
download |
Downloads and unpacks an archive of data that you extracted before (clientData.download.url) |
none |
No Job. The data volume holds the data already |
The Job reads the folder that contains Data/. Set one of these two values:
clientData.extract.clientVolume: any Kubernetes volume source, for example a PVC, an NFS share, or a hostPath folder on the node.clientSubPathselects a folder inside the volume.clientData.extract.clientUrl: an archive of the client folder (.tar,.tar.gz,.tgz,.tar.xz,.tar.bz2, or.zip). If the Job must extract, it downloads the archive intoscratchVolumeand deletes it afterwards. Keep the URL private, because the client is copyrighted.
clientData:
extract:
clientVolume:
nfs:
server: nas.example.com
path: /export/games
clientSubPath: "World of Warcraft 1.12.1"
mmapThreads: 4 # 0 = one thread per CPUIf your cluster has no shared storage and you cannot use hostPath, copy the client into a PVC once. The PVC uses the default storage class:
kubectl -n cmangos apply -f - <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: wow-client
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 8Gi
---
apiVersion: v1
kind: Pod
metadata:
name: wow-client-upload
spec:
containers:
- name: upload
image: busybox:1.37.0
command: ["sleep", "3600"]
volumeMounts:
- name: client
mountPath: /client
volumes:
- name: client
persistentVolumeClaim:
claimName: wow-client
EOF
kubectl -n cmangos wait --for=condition=Ready pod/wow-client-upload
tar -C "/path/to/World of Warcraft 1.12.1" -cf - Data \
| kubectl -n cmangos exec -i wow-client-upload -- tar -xf - -C /client
kubectl -n cmangos delete pod wow-client-uploadThe chart selects the data volume in this order:
clientData.volume: any Kubernetes volume source, for example NFS.clientData.existingClaim: a PVC that you manage.- A PVC that the chart creates from
clientData.storage(10 GiB, ReadWriteOnce, the default storage class).
The extracted data uses about 2.3 GB for Classic, 3.1 GB for TBC, and 3.2 GB for WotLK. Most of it is mmaps. The mmap generator uses about 0.5 to 1 GiB of memory per thread, so set mmapThreads to fit your node.
The Job records each finished step on the data volume. If the Job restarts, it continues after the last finished step. If you turn on a step later, for example mmaps: true, the next upgrade runs only that step. Then restart mangosd, so that it loads the new data. To extract everything again, set clientData.force=true for one upgrade.
By default, the chart deploys MySQL 8.4 with a 10 GiB volume. It generates the root password and the password of the mangos user, and it keeps both in a Secret.
To use your own MySQL or MariaDB server:
mysql:
enabled: false
externalDatabase:
host: mysql.example.com
port: 3306
adminUser: root # optional, see below
database:
user: mangos
existingSecret: cmangos-db # keys: password, admin-passwordIf you set adminUser, the db-init Job creates the four databases and the mangos user, and it grants the access. If you do not set it, the databases must exist, and database.user must be able to create tables in them.
Do not use ; in the password. CMaNGOS uses it to separate the fields of its connection strings.
The bundled MySQL runs with a few extra arguments, and mysql.extraArgs in values.yaml gives the reasons. Only MySQL 8.4 is tested.
The db-init Job runs on every install and upgrade, and it is safe to run many times. It creates the databases, installs the world content from the world database of the expansion, and applies the SQL updates. Then it sets the realm in the realm list and creates your accounts.
The characters, realmd, and logs databases hold player data. The Job never drops them.
The world database holds game content only. If the image carries new world content, the Job installs the world database again. That install discards manual changes, for example spawns that a GM added with .npc add. Keep such changes in dbInit.world.extraSql. The Job applies these files after every world install:
dbInit:
world:
extraSql:
10-custom-spawns.sql: |
INSERT INTO creature ...;To install the world database only once, set dbInit.world.reinstall=never.
The base SQL of CMaNGOS contains four accounts: ADMINISTRATOR, GAMEMASTER, MODERATOR, and PLAYER. The password of each account is its name. On a fresh database, the Job deletes them. To keep them, set dbInit.removeDefaultAccounts=false.
The Job creates the accounts in dbInit.accounts:
dbInit:
accounts:
- username: admin
password: change-me # stored in a Secret by the chart
gmlevel: 3 # 0 player, 1 moderator, 2 game master, 3 administrator
- username: friend
existingSecret: my-accounts
passwordKey: friend-passwordIf an account does not exist, the Job creates it. The Job never changes the password of an existing account, because players can change their password in the game. It sets gmlevel on every run. It also sets the expansion level of the account: 0 for Classic, 1 for TBC, and 2 for WotLK. An account with a lower level cannot create the races and classes of the expansion. Names and passwords have 16 characters at most, and they are not case-sensitive.
You can also use the mangosd console:
kubectl -n cmangos attach -it deploy/cmangos-mangosd -c mangosd
account create <user> <password> <level>
account set gmlevel <user> 3<level> is the expansion level. Without it, the core creates a Classic account. To change the level of an existing account, use account set addon <user> <level>.
To detach, press ctrl-p ctrl-q. Do not press ctrl-c, because it stops mangosd.
Warning: do not type passwords in the attached console. The console echoes your input, and the container log keeps it. Use dbInit.accounts, or the remote consoles (see Remote consoles).
You can set any key from mangosd.conf.dist (mangosd.config), realmd.conf.dist (realmd.config), aiplayerbot.conf.dist (playerbots.config), and ahbot.conf.dist (ahbot.config). Use the key exactly as the file writes it:
mangosd:
config:
Rate.XP.Kill: 3
Rate.Drop.Money: 2
GameType: 1 # PvP realm
realmd:
config:
WrongPass.MaxCount: 5The chart sets the database connections, the data folder, the ports, and the remote consoles. For environment variables without a configuration key, use mangosd.extraEnv and realmd.extraEnv.
The servers log to stdout only. To keep a log file, set its key, for example mangosd.config.LogFile: Server.log. At the default LogLevel = 1, mangosd logs an "Avg Diff" line about every 3 seconds. To make the log quieter, set mangosd.config.LogLevel: 0.
playerbots:
enabled: true
config:
AiPlayerbot.MinRandomBots: 100
AiPlayerbot.MaxRandomBots: 100
mangosd:
resources:
limits:
memory: 6Gi
ahbot:
enabled: true
config:
AuctionHouseBot.Chance.Sell: 10
AuctionHouseBot.Chance.Buy: 10The chart lowers the number of random bots from 1000 to 50. More bots need more memory for mangosd. In a test with 10 bots, mangosd used about 2.1 GiB.
On the first start with playerbots, the module builds an equipment cache in the characters database. That start took about 2 minutes. If the startup probe stops mangosd during the build, the cache stays incomplete. To build it again, stop mangosd and empty the table ai_playerbot_equip_cache.
mangosd has two remote consoles: SOAP (mangosd.soap.enabled) and a telnet console (mangosd.remoteAccess.enabled). The chart exposes them only inside the cluster, on the Service <release>-mangosd-admin. Both need a GM account with gmlevel 3:
kubectl -n cmangos port-forward svc/cmangos-mangosd-admin 3443:3443
telnet 127.0.0.1 3443 # log in with the GM account, then: account create <user> <password> <level>The game protocols are raw TCP. Ingress routes HTTP only, so the chart does not offer an Ingress.
The client connects to two addresses:
- The address in
realmlist.wtf, which is therealmdservice. The client uses port 3724. - The realm address and port from the realm list, which is the
mangosdservice.dbInit.realm.addressanddbInit.realm.portset them.
Both Services are of type LoadBalancer by default, so they listen on the standard ports. On clusters without a load balancer, use NodePort services with fixed ports:
mangosd:
service:
type: NodePort
nodePort: 30085
dbInit:
realm:
address: 192.168.1.20 # a node address that the clients can reachIf dbInit.realm.port is empty, the chart uses the nodePort of the mangosd service (for NodePort) or its port. Clients expect the auth port 3724, so give realmd port 3724 on the address in realmlist.wtf. realmd reads the realm list every 20 seconds, so a new realm address takes effect without a restart.
To test both addresses without a game client, run the login test on your machine with Python 3:
python3 build/scripts/auth-check.py --expansion classic --host <realmd address> \
--user admin --password change-me --check-worldA Gateway controller that supports TCPRoute can route the two ports. TCPRoute is in the experimental channel of the Gateway API, so the cluster needs the experimental CRDs. A TCP listener cannot tell two routes apart, so each game port needs its own listener on the Gateway:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: games
namespace: gateway-system
spec:
gatewayClassName: <your gateway class>
listeners:
- name: wow-auth
protocol: TCP
port: 3724
allowedRoutes:
namespaces:
from: All
- name: wow-world
protocol: TCP
port: 8085
allowedRoutes:
namespaces:
from: AllThe chart values attach one TCPRoute to each listener:
gateway:
enabled: true
parentRefs:
realmd:
- name: games
namespace: gateway-system
sectionName: wow-auth # a TCP listener on port 3724
mangosd:
- name: games
namespace: gateway-system
sectionName: wow-world # a TCP listener on port 8085
realmd:
service:
type: ClusterIP
mangosd:
service:
type: ClusterIP
dbInit:
realm:
address: <Gateway address>helm upgrade runs both Jobs again. The db-init Job applies the new SQL updates. The client-data Job finds its finished steps and completes in seconds. Every upgrade restarts both servers.
Chart 0.2.0 moved images.server and images.db to images.<expansion>.server and images.<expansion>.db. If your values set them, move them before you upgrade.
helm uninstall cmangos -n cmangosHelm keeps three things: the MySQL volume (data-cmangos-mysql-0), the data volume (cmangos-client-data), and the database Secret (cmangos-db). A reinstall with the same release name uses them again. To delete everything, delete them by hand:
kubectl -n cmangos delete pvc data-cmangos-mysql-0 cmangos-client-data
kubectl -n cmangos delete secret cmangos-dbEach chart version and image carries a keyless cosign signature from this repository. To make sure that a chart comes from here, run:
cosign verify ghcr.io/christianjacobsen/charts/cmangos:<version> \
--certificate-identity-regexp '^https://github\.com/ChristianJacobsen/cmangos-helm/\.github/workflows/' \
--certificate-oidc-issuer https://token.actions.githubusercontent.comThe same command works for the images. Charts 0.1.0 and 0.1.1 and their images have no signatures.
- One realm per release, and one replica of each server.
- The chart has no backup job yet. Use
mysqldumpagainst the MySQL pod. - The data volume uses ReadWriteOnce by default. That works on one node. On clusters with more nodes, use a ReadWriteMany volume through
clientData.existingClaim, or keep the Job andmangosdon one node.
The chart uses the GPL-2.0-or-later license, the same as CMaNGOS. See LICENSE.