Trackmania Forever dedicated server with the Trakman TypeScript controller, packaged as a Docker image and wired for a full stack (game server + PostgreSQL) via Docker Compose.
Coming from an XAseco server? Your players, records, karma votes and donations can be carried over, see migration/MIGRATION.md.
The controller in trakman/ started as Trakman 1.6 and has been significantly reworked since. It's versioned separately, starting at 2.0.0. Highlights:
- Docker deployment: dedicated server, Trakman and PostgreSQL via Docker Compose, configured from
.env, with a self-hosted UI icon server. - Configuration: JSON overrides in
config/that apply while the server runs, and plugins that can be enabled, disabled or restarted in-game with//plugin. Feature widgets now live in their own plugins. - New features:
//disabletimer///enabletimer, movable BestCps / BestFinishes / CpCounter widgets, a public API fallback for author nicknames and flags, and direct map writes (up to 5MB) with manual map loading. - Bug fixes: crashes on Highlands and other modded maps, juked maps playing twice, duplicate records, stale local ranks,
/listsearch on styled names, TMX screenshots, blank widget icons, and webservices errors without credentials. - XAseco migration: see
migration/MIGRATION.md.
The full list is in trakman/CHANGELOG.md, also shown in-game with /changes. Because of these changes, upstream releases can't be copied over directly, see Updating Trakman.
- Install Docker with the Compose plugin.
cp .env.example .envand fill in the required values.- Put your
.Challenge.Gbxfiles intracks/. docker compose up -d --build
To stop: docker compose down
| Path | Purpose |
|---|---|
trakman/ |
Vendored Trakman source, forked from 1.6 and reworked, see About this fork. Its config files (trakman/config/, trakman/plugins/**/*Config*.js) are the defaults. |
config/ |
Trakman config overrides as JSON, mounted into the container. See config/README.md. |
docker/entrypoint.sh |
Renders server config from env, then runs the dedicated server and Trakman together. If either exits the container exits and Compose restarts it. |
docker/templates/ |
dedicated_cfg and MatchSettings templates (@VAR@ placeholders). They're rendered into the image filesystem at start and never into a mounted volume. |
migration/ |
One-shot XAseco MySQL → PostgreSQL migration, see migration/MIGRATION.md. |
tracks/ |
.Challenge.Gbx files, mounted at GameData/Tracks/Challenges/Custom. Every map in it is played. Two example tracks are included. |
music/ |
.ogg files, mounted at GameData/Music. Not committed. |
icons/ |
Self-hosted copy of Trakman's UI icons, served by docker/icon-server.mjs (see below). |
The dedicated server binary is downloaded from Nadeo's official 2011-02-21 release at build time and checked against a pinned SHA-256.
| Service | Purpose |
|---|---|
postgres |
PostgreSQL 16, data in the trackmania_postgres_data volume. Not published to the host. |
tmuf-server |
Dedicated server + Trakman + UI icon server |
legacy-mysql, migrate |
Only started with --profile migrate, for the XAseco migration. |
Published ports: 2350 and 3450 (UDP and TCP), and 8080 (TCP) for UI icons. XML-RPC (5000) stays inside the container.
See .env.example for the full list. Required:
SERVER_LOGIN/SERVER_LOGIN_PASSWORD: a dedicated server account.SERVER_SA_PASSWORD: SuperAdmin password. Trakman logs in as SuperAdmin with it.MASTERADMIN_LOGIN: becomes the Trakman owner. Add other admins in-game with//addadmin <login>(stored in the database).DB_PASSWORD: PostgreSQL password (plus optionalDB_USER/DB_NAME).
Optional: server name, password and limits (SERVER_*, MAX_PLAYERS), game mode settings (GAMEMODE and the per-mode limits), CUSTOM_MUSIC_ENABLED, DEDIMANIA_PASSWORD (defaults to SERVER_LOGIN_PASSWORD), WEBSERVICES_LOGIN / WEBSERVICES_PASSWORD, ICONS_URL, LOG_LEVEL and TZ.
Copy .Challenge.Gbx files into tracks/ (subfolders work too) and restart the container with docker compose restart tmuf-server. On every start the container puts every map in tracks/ into the server's map list, so there's no playlist to maintain, and maps added in-game through TMX stay in rotation after a restart. To take a map out of rotation, remove its file. The two included tracks are only examples, remove them if you don't want them. If tracks/ has no maps, the server falls back to the Nations campaign.
Players' game clients download every widget icon over HTTP. Trakman normally points them at trakman.ptrk.eu, where downloads can fail partway and leave blank buttons, a different set for each player. The container serves its own copy from icons/ on port 8080. Set ICONS_URL to the address players can reach, e.g. ICONS_URL=http://your.server.host:8080/icons, and open TCP 8080 in the host firewall. When ICONS_URL is empty, Trakman uses trakman.ptrk.eu.
To use your own banner on the end-of-map screen, add an image (around 4:1) to icons/ and name it in config/plugins/donations/ui/BannerWidget.config.json, e.g. { "image": "MyBanner.png" }. A plain file name is loaded from the icon host, a full URL is used as is. The default is CrazyTrakman.png. Files in icons/ are copied into the image, so rebuild after adding one.
-
Copy
.ogg(or.mux) files intomusic/. Any file name works. -
Add one entry per song to
config/plugins/music/SongList.json(create it if it doesn't exist).urlisMusic/<file name>, a path the server serves fromGameData, so no web server is needed:[ { "name": "Song Title", "author": "Artist", "url": "Music/Artist - Song Title.ogg" }, { "name": "Another Song", "author": "Someone Else", "url": "Music/another-song.ogg" } ] -
The first time, set
CUSTOM_MUSIC_ENABLED=Truein.envand recreate the container withdocker compose up -d.
After that, edits to SongList.json apply while the server runs: the music plugin restarts with the new list. music/ is mounted, so new files don't need a rebuild either.
//addsong <name> <author> <url> adds a song in-game, but only until the next restart. Add songs you want to keep to SongList.json.
Other music settings go in config/plugins/music/Config.json (defaults in trakman/plugins/music/Config.js).
Plugins are registered in trakman/Plugins.ts. Whether one starts is set by isEnabled in config/plugins/<name>/Config.json (see config/README.md), and can be changed at runtime without a restart:
| Command | Effect |
|---|---|
/plugins |
Window listing every plugin with its status. Masteradmins get enable/disable and restart buttons. |
//plugin list |
Same list in chat. |
//plugin enable <name> / //plugin disable <name> |
Starts or stops a plugin. Disabling a plugin stops the plugins that depend on it, enabling it starts them again. The choice is saved in the database and overrides isEnabled until reset. |
//plugin restart <name> |
Restarts a plugin and the plugins depending on it. |
//plugin reset <name> |
Forgets the saved choice, so isEnabled from the config applies again. |
//reloadconfig |
Re-reads config/ and restarts the plugins whose config changed. Edits are also picked up automatically while the server runs. |
ui, vote and actions are required and can't be disabled. ui only holds the shared widgets and the building blocks for windows; feature widgets live in their plugin's ui/ folder (e.g. plugins/dedimania/ui/) and disappear when it's disabled. Skip, replay and time-extension votes and payments are handled by the vote plugin, with their limits, costs and commands in plugins/vote/MapVotes.config.js (override: config/plugins/vote/MapVotes.config.json). The privilege needed is plugins.privilege in config/config/Config.json (default 3, masteradmin).
Everything server-specific lives in data files rather than code, so you can keep your setup in git next to this repo without forking the code:
| What | Where |
|---|---|
| Trakman settings, song list, banner | config/**/*.json (see config/README.md) |
| Tracks | tracks/ |
| Banner and other images | icons/<your file> |
| Deployment | your CI file, e.g. .gitlab-ci.yml |
| Secrets | .env, never committed |
A clean way to manage this is one local clone with two branches and two remotes:
maintracks this repo and only ever gets code changes.liveismainplus your customization commits, pushed to a private remote that your server deploys from.
git remote add private <your private repo URL>
git switch -c live
# add your tracks, config/*.json, icons, CI file
git add -A && git commit -m "Server customizations"
git push -u private liveTo update, pull or make code changes on main, then git switch live && git merge main && git push. Because live only adds data files, merges from main don't conflict. Make changes that belong to everyone on main, never on live.
To make sure live can't be pushed to the public remote by accident, add a .git/hooks/pre-push hook:
#!/bin/sh
# Refuse to push the live branch anywhere except the private remote
[ "$1" = private ] && exit 0
while read -r local_ref _ _ _; do
if [ "$local_ref" = refs/heads/live ]; then
echo "pre-push: live only goes to the private remote" >&2
exit 1
fi
donePlugins here are restructured for runtime enable/disable (each entry file exports a plugin definition with start/stop, and src/plugins/ tracks what they register), so upstream releases can't be copied over wholesale. Port upstream changes file by file, keeping src/ConfigLoader.ts, src/plugins/, config/Titles.js and plugins/music/SongList.js. For new config files, run node scripts/wrap-trakman-configs.cjs so they read the overrides in config/. Check the upstream CHANGELOG.md for renamed config keys, since Trakman warns about overrides that no longer match. Then rebuild.
Any Docker host works: clone the repo, add your .env, tracks and music, and run docker compose up -d --build. Tracks added in-game through TMX are written to tracks/ on the host. If your deployment syncs the repo over the host's tracks/ and deletes extra files (e.g. rsync --delete), they'll be removed unless they're also committed.
See LICENSE. Trakman and the migration script keep their own licenses in trakman/LICENSE and migration/LICENSE.