Windrose dedicated server for Linux using Docker, SteamCMD and Wine, with persistent saves, backups, diagnostics and optional Discord/Gotify notifications.
Self-hosted and production-friendly setup with first-time setup helper, world switching, health checks and 24/7 operation support.
No port forwarding required — players join via Invite Code from
ServerDescription.json.
- Features
- Requirements
- First-time setup (recommended)
- Quick start
- Configuration
- Volumes
- Multiple worlds
- How players join
- In-game visibility (official)
- Useful commands
- Quick diagnostics
- Activity notifications: Discord, Gotify, or both
- Save transfer and world selection
- Backup saves
- Directory structure
- Troubleshooting
- Image versions
- Technical notes
- FAQ
- Issues and suggestions
- Support
- License
Additional documents:
- TROUBLESHOOTING.md — full symptom table, diagnostics playbooks, network debugging
- DEVELOPMENT.md — local builds, image channels, CI workflows
- Dockerized Windrose dedicated server on Linux (Wine + Xvfb, headless)
- Automatic game install/update via SteamCMD with optional
UPDATE_ON_STARTtoggle - Persistent data by default (
./data,./steam-home) for saves, config, and Steam/Wine state - Simple operator-first configuration through
.envand optional JSON auto-patching - Stable helper commands for start/stop/restart/logs/diagnostics and world management
- Save transfer workflow with explicit
WorldIslandIdmapping and versioned world paths - Built-in backup tooling (
./windrose backup, cron installer, retention controls) - Optional Discord/Gotify activity notifications (or both at once) plus notifier test command
- Multiple image channels (
stable,latest,staging,debug) for operations and troubleshooting - Production-friendly defaults: host networking, restart policy, healthcheck, and log rotation
| Component | Minimum |
|---|---|
| OS | Ubuntu 22.04+ / Debian 12+ (Linux host) |
| Docker | 24.x+ |
| Docker Compose | v2.x (docker compose) |
| RAM | 8 GB (2 players) · 12 GB (4 players) · 16 GB (10 players) |
| Disk | 35 GB SSD |
If this is your first run, use the interactive helper first. It creates .env, asks for key settings, optionally configures backup cron, and can start the server immediately.
# 1. Clone and enter the repository
git clone https://github.com/UberDudePL/windrose-dedicated-server-docker.git
cd windrose-dedicated-server-docker
# 2. Make helper scripts executable
chmod +x ./windrose ./serverctl.sh
# 3. Run interactive setup
./windrose setupWhat ./windrose setup asks:
- Start automatically after setup (
Y/n) - Server name
- Invite code (optional, alphanumeric, minimum 6 chars)
- Optional server password
- Max players
- Enable automatic backup cron (
y/N) - Backup cron schedule (default:
0 6 * * *, daily at 06:00) - Backup format (
tar.gzorzip) - Backup scope (
full,save,both) - Discord upload for save backups (
y/N) - Discord webhook URL (only if upload is enabled)
If invite code is left empty, the server generates it automatically on first successful start.
When setup starts the server automatically, it tries to show the generated code.
When setup does not start the server, check the generated code later in data/R5/ServerDescription.json.
Behavior and safety notes:
- Setup is one-off by design: if
.envalready exists, setup exits with a clear message. - Setup runs a host precheck before questions: Docker in PATH, Docker Compose v2, RAM >= 8 GB, free disk >= 8 GB.
PUIDandPGIDare auto-detected from the current host user.- If backup upload is enabled and scope is
full, scope is adjusted toboth. - If
crontabis missing, setup continues and warns instead of failing. - Before auto-start, setup runs preflight checks (
docker compose config) and warns ifPORTorQUERYPORTare already in use.
After setup, use:
./windrose status
./windrose logsProduction mode uses the published GHCR image by default. Most users only need this mode and can ignore the development override file.
If this is your first run, prefer First-time setup (recommended).
# 1. Clone the repository
git clone https://github.com/UberDudePL/windrose-dedicated-server-docker.git
cd windrose-dedicated-server-docker
# 2. Copy the example environment file
cp .env.example .env
# 3. Edit basic values if needed
nano .env
# 4. Pull the published image
docker compose pull
# 5. Start the server (downloads game files on first run ~3 GB)
docker compose up -d
# 6. Follow logs
docker compose logs -f windroseRecommended image tags:
Stable: ghcr.io/uberdudepl/windrose-dedicated-server-docker:v1.6.4
Latest: ghcr.io/uberdudepl/windrose-dedicated-server-docker:latest
Staging fallback: ghcr.io/uberdudepl/windrose-dedicated-server-docker:staging
Debug tools: ghcr.io/uberdudepl/windrose-dedicated-server-docker:debug
Set the image version in .env with:
IMAGE_REPOSITORY=ghcr.io/uberdudepl/windrose-dedicated-server-docker
IMAGE_TAG=v1.6.4latest/ version tags: stable Wine build for normal use.staging: fallback image using Wine Staging pluswinetricksprewarm (win10,vcrun2022) for host-specific Wine issues.debug: stable Wine build plus extra diagnostic tools (dnsutils,file,iproute2,lsof,strace) and more verbose Wine logging.
Use the stable channel unless you are actively diagnosing host-specific startup problems.
For local development, builds, and CI workflows, see DEVELOPMENT.md.
You can set the most common values directly in .env:
SERVER_NAME=My Windrose Server
SERVER_NOTE=Friendly co-op server
SERVER_PASSWORD=
MAX_PLAYERS=4
INVITE_CODE=If you prefer manual editing, stop the server first and edit data/R5/ServerDescription.json directly.
Important: edit JSON files only while the server is stopped, or your changes may be overwritten.
Copy .env.example to .env and adjust to your needs. Use .env.dev.example for local development and notifier testing.
PUID=1000 # Host user id for mounted files
PGID=1000 # Host group id for mounted files
STEAM_LOGIN=anonymous # SteamCMD login
STEAM_PASS= # Leave empty for anonymous login
WINDROSE_APP_ID=4129620 # Steam AppID for Windrose Dedicated Server
UPDATE_ON_START=true # Set false to skip update on container restart
UPDATE_VERIFY_TIMEOUT=120 # Post-update runtime verification timeout in seconds
GENERATE_SETTINGS=true # Set false to skip env-based JSON patching
INVITE_CODE= # Optional invite code
SERVER_NAME= # Optional server name
SERVER_NOTE= # Optional public server note/description
SERVER_PASSWORD= # Optional password
MAX_PLAYERS=4 # Recommended for stability
P2P_PROXY_ADDRESS=127.0.0.1 # Keep default unless players connect over LAN
# Direct connection (alternative to invite code, requires port forwarding)
USE_DIRECT_CONNECTION=false
DIRECT_CONNECTION_SERVER_PORT=7777
DIRECT_CONNECTION_PROXY_ADDRESS=0.0.0.0
USER_SELECTED_REGION= # Leave empty for auto-detect (SEA, CIS, EU)
PORT=7777
QUERYPORT=7778
MULTIHOME=0.0.0.0Set NO_COLOR=1 to disable ANSI colors in helper/CLI output.
If your host is slow to start the container after ./windrose update, increase UPDATE_VERIFY_TIMEOUT (for example to 180 or 300).
| Variable | Default | Description |
|---|---|---|
CONTAINER_NAME |
windrose |
Change only if you run more than one server on the same host |
HOSTNAME |
localhost |
Internal container hostname used by ICE candidate discovery; keep localhost unless custom name resolves inside container |
IMAGE_REPOSITORY |
GHCR repo | Published image repository |
IMAGE_TAG |
v1.6.4 |
Stable image tag to run |
PUID |
1000 |
User id used for mounted files |
PGID |
1000 |
Group id used for mounted files |
UPDATE_ON_START |
true |
Update and validate server files on startup |
UPDATE_VERIFY_TIMEOUT |
120 |
Timeout in seconds for post-update runtime verification in ./windrose update; increase on slower hosts |
GENERATE_SETTINGS |
true |
Auto-patch ServerDescription.json from env values |
INVITE_CODE |
empty | Invite code shown to players. Leave empty to use direct connection instead |
SERVER_NAME |
empty | Display name of the server |
SERVER_NOTE |
empty | Short public server note/description |
SERVER_PASSWORD |
empty | Leave empty for a public server |
MAX_PLAYERS |
4 |
Maximum number of simultaneous players |
P2P_PROXY_ADDRESS |
127.0.0.1 |
Internal socket proxy address. Change to LAN IP if players connect from the same network |
USE_DIRECT_CONNECTION |
false |
Set to true to allow players to connect directly via IP instead of invite code. Requires port forwarding. |
DIRECT_CONNECTION_SERVER_PORT |
7777 |
Port used for direct connection (TCP and UDP). Only applies when USE_DIRECT_CONNECTION=true |
DIRECT_CONNECTION_PROXY_ADDRESS |
0.0.0.0 |
Proxy address for direct connection. Only applies when USE_DIRECT_CONNECTION=true |
USER_SELECTED_REGION |
empty | Connection service region: SEA, CIS, EU. Leave empty to auto-detect. EU covers both EU and NA regions |
PORT |
7777 |
Game port (UDP) |
QUERYPORT |
7778 |
Query port (UDP) |
WINDROSE_APP_ID |
4129620 |
Steam AppID |
STEAM_LOGIN |
anonymous |
SteamCMD login |
| Host path | Container path | Contents |
|---|---|---|
./data |
/data |
Server files, saves, config |
./steam-home |
/home/steam |
Wine prefix, SteamCMD cache |
Windrose stores each world under the save database path:
data/R5/Saved/SaveProfiles/Default/RocksDB/<GameVersion>/Worlds/<WorldIslandId>
or
data/R5/Saved/SaveProfiles/Default/RocksDB_v2/<GameVersion>/Worlds/<WorldIslandId>
The active world is selected by ServerDescription.json:
ServerDescription_Persistent.WorldIslandIdUse the helper command to switch interactively:
./windrose switchTo only list available worlds without changing anything:
./windrose worldsTo detect orphan or broken world directories:
./windrose worlds-checkWhat it does:
- Lists all worlds found under the current RocksDB save version.
- Marks the currently selected world.
- Lets you switch to an existing world or create a new one.
- When creating a new world, it can store a display name and sync it into
WorldDescription.jsonafter the game creates the metadata file. - Stops the server first if it is running, updates
WorldIslandId, then starts it again. - Hides stale placeholder entries (for example directories with only
.windrose-world-name) unless that placeholder is currently selected.
Important:
- Do not rename world folders. The save database relies on those IDs.
- If you create a new world, the server initializes its data on the next start.
- World discovery is version-specific, so the command uses the latest directory found under the auto-detected save root (
RocksDB_v2/preferred, thenRocksDB/).
Gameplay difficulty is stored per world in WorldDescription.json and is not controlled by docker-compose.yml environment variables.
-
Stop the server:
./windrose stop
-
Find the active world ID from
data/R5/ServerDescription.json:ServerDescription_Persistent.WorldIslandId -
Edit this file for that active world:
data/R5/Saved/SaveProfiles/Default/RocksDB/<GameVersion>/Worlds/<WorldIslandId>/WorldDescription.json or data/R5/Saved/SaveProfiles/Default/RocksDB_v2/<GameVersion>/Worlds/<WorldIslandId>/WorldDescription.json -
Set the preset fields in
WorldDescription.json. Reference values per preset:Easy
WorldPresetType = "Easy"MobHealthMultiplier = 0.7,MobDamageMultiplier = 0.6ShipsHealthMultiplier = 0.7,ShipsDamageMultiplier = 0.6BoardingDifficultyMultiplier = 0.7CombatDifficulty = EasyEasyExplore = true(disables map markers — shown as "Immersive exploration" in-game; despite the name, this makes exploration harder)
Medium (default)
WorldPresetType = "Medium"- All multipliers =
1.0 CombatDifficulty = NormalEasyExplore = false
Hard
WorldPresetType = "Hard"MobHealthMultiplier = 1.5,MobDamageMultiplier = 1.25ShipsHealthMultiplier = 1.5,ShipsDamageMultiplier = 1.25BoardingDifficultyMultiplier = 1.5CombatDifficulty = HardEasyExplore = false
-
Start the server:
./windrose start
Tip: if values do not apply, verify the edited world ID is the same as ServerDescription_Persistent.WorldIslandId.
To keep the same game world instead of generating new ones:
- Keep persistent host binds for
/dataand/home/steam(do not change them between deployments). - Always keep
ServerDescription_Persistent.WorldIslandIdset to an existing world folder name. - Do not rename world folders.
- Stop the server before editing
ServerDescription.jsonorWorldDescription.json. - Restart after edits and verify logs.
If a new world keeps appearing:
- Check that
WorldIslandIdpoints to a folder that exists under.../RocksDB/<GameVersion>/Worlds/or.../RocksDB_v2/<GameVersion>/Worlds/. - Run
./windrose worlds-checkto detect broken or placeholder entries. - Re-select the intended world with
./windrose switch.
To avoid accidental new-world generation and confusing config drift, keep these values aligned:
ServerDescription_Persistent.WorldIslandId- The selected world folder name under
.../Worlds/<WorldIslandId> WorldDescription.IslandIdinside that world'sWorldDescription.json
If any of these mismatch, the server may generate a new world and rewrite IDs on startup.
WorldPresetTypeshould be one ofEasy,Medium, orHardfor preset mode.- If you change individual
WorldSettingsvalues, the world can switch toCustomon next launch. - For predictable outcomes, either:
- Use preset values only, or
- Intentionally manage a full custom profile and treat
WorldPresetTypeasCustom.
Note: It is generally easier to configure these settings in-game first, then copy the resulting values from your local save file to the server.
| Parameter | Default | Range | Description |
|---|---|---|---|
CoopQuests |
true |
— | Auto-completes co-op quests for all active players |
EasyExplore |
false |
— | Disables map markers ("Immersive exploration" in-game). Despite the name, makes exploration harder |
MobHealthMultiplier |
1.0 |
0.2–5.0 |
Enemy health multiplier |
MobDamageMultiplier |
1.0 |
0.2–5.0 |
Enemy damage multiplier |
ShipHealthMultiplier |
1.0 |
0.4–5.0 |
Enemy ship health multiplier |
ShipDamageMultiplier |
1.0 |
0.2–2.5 |
Enemy ship damage multiplier |
BoardingDifficultyMultiplier |
1.0 |
0.2–5.0 |
Enemy sailors needed to win boarding |
Coop_StatsCorrectionModifier |
1.0 |
0.0–2.0 |
Scales enemy health by active player count |
Coop_ShipStatsCorrectionModifier |
0.0 |
0.0–2.0 |
Scales enemy ship health by active player count |
CombatDifficulty |
Normal |
Easy/Normal/Hard |
Boss aggression level |
Use this sequence every time you change server/world JSON files:
- Stop server.
- Back up config/save files.
- Edit files.
- Start server.
- Verify loaded values in logs and in active JSON.
This avoids partial writes, tool/UI overwrites, and startup-time regeneration surprises.
- Start the server once and wait until it is healthy
- Open
data/R5/ServerDescription.jsonand copy theInviteCodevalue - Share that code with players — they use it in-game under Join via Code
- Invite codes are case-sensitive and should be at least 6 characters long
- No port forwarding is required for the normal invite-code flow
The server still binds internal game and query ports, mainly for local binding and advanced or multi-instance setups.
Based on official Windrose documentation and Steam announcements:
- Players can join via invite code in-game: Play -> Connect to Server.
- There is a Show Server Info section in the in-game Esc menu.
ServerNameis intended to help identify the correct server when invite codes are similar.
What is not clearly documented as visible in dedicated-server UI:
- Detailed world difficulty internals (for example
WorldPresetType, combat tags, and multipliers).
Treat those as file-based settings in WorldDescription.json and verify with logs/file values when needed.
Official references:
- https://playwindrose.com/dedicated-server-guide/
- https://steamcommunity.com/app/3041230/announcements/
# First-time interactive setup (.env, backup options, optional auto-start)
./windrose setup
# Start
docker compose up -d
# Stop
docker compose stop
# Restart helper flow
./windrose restart
# Helper status overview
./windrose status
# JSON snapshot for monitoring integrations
./windrose status-json
# Full operator preflight checks
./windrose doctor
# Create a diagnostics bundle (default: 300 log lines)
./windrose diagnostics
# View live logs
docker compose logs -f windrose
# Helper log shortcut
./windrose logs
# Best-effort player activity lines from recent logs
./windrose activity history
# Structured join/leave events (JSONL)
./windrose activity events
# List worlds
./windrose worlds
# Detect orphan/broken world entries
./windrose worlds-check
# Switch to another world interactively
./windrose switch
# Start or inspect activity notifications
./windrose notify
./windrose notify status
./windrose notify test
# Create a backup or install the backup cron helper
./windrose backup
./windrose install-backup-cron
# Pull the latest published image tag
./windrose pull
# Update helper flow (safe pull -> up; use --force-down for full recreate)
./windrose update
# Show detailed update log (default: last 120 lines)
./windrose update-log
# Stop and remove the stack
./windrose down
# Check server process inside container
docker compose exec windrose pgrep -a WindroseServer
# Container status + health
docker compose ps
# Optional system-wide install target
./windrose install /usr/local/bin/windrosectlUse these commands for a fast operational check:
# 1) Basic container and health status
./windrose status
# 2) Full host/runtime preflight
./windrose doctor
# 3) World integrity check (orphan/broken entries)
./windrose worlds-check
# 4) Recent critical network/auth errors from current log file
docker compose logs --no-color --tail 400 windrose | grep -Ei "account verification failed|turn session was expired|p2pgate disconnected|server authorization failed|login finished with error"
# 5) Create diagnostics bundle for incident review
./windrose diagnosticsIf command 3 returns lines repeatedly, check outbound connectivity and firewall/NAT behavior for *.windrose.support on UDP/TCP 3478.
For a machine-readable snapshot, use ./windrose status-json.
./windrose status shows a compact operator dashboard: container state and health, currently online players (parsed from the last 24 hours of container logs), last activity event timestamp, backup age, and notifier status. It does not require the notify background process to be running — player data is read directly from container logs. Using a 24-hour log window means players active for many hours will still appear correctly.
./windrose activity status is a focused diagnostic tool for player activity: it shows how many log lines were scanned, how many join/leave events were matched, and the full list of online players without a display cap. Use it when you want to verify the parser is working or diagnose a mismatch between expected and reported online counts. You can pass a custom line count: ./windrose activity status 8000.
For quick player activity extraction from logs, use ./windrose activity history [lines].
For structured join/leave records, use ./windrose activity events [lines].
Events are appended as JSON lines to ./logs/player-events.log.
The parser is best-effort and now prefers richer Windrose/UE markers such as Login request, prelogin/account verification, and account summary dumps when they are present.
Entries may also include an optional name field when the server log exposes a human-readable player name.
A persistent identity map is maintained in ./state/player-identities.tsv and reused to improve name resolution for disconnect events.
Legacy aliases are still supported for backward compatibility: ./windrose player-history, ./windrose player-events.
For deeper investigation, extended symptom table, and network playbooks, see TROUBLESHOOTING.md.
A basic log watcher is included for best-effort player activity notifications.
- Choose a notification backend in
.env:
NOTIFY_PROVIDER=auto
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/...
GOTIFY_URL=https://gotify.example.com
GOTIFY_TOKEN=your_app_token
GOTIFY_PRIORITY=5Provider modes:
auto: prefers Gotify when it is configured, otherwise falls back to Discorddiscord: sends only to Discordgotify: sends only to Gotifyboth: sends to Discord and Gotify for every event
- Test the webhook once before long-term use:
./windrose notify test- Start the watcher:
./windrose notify- Check watcher status and effective backend:
./windrose notify statusThe helper asks whether to run in background mode. If you start it in background mode, running ./windrose notify again detects the running watcher and offers to stop it.
Background logs are written to:
./logs/notify.log
At the moment this is log-based and best-effort. Disconnect events are easier to detect reliably than joins, so treat it as a lightweight helper rather than a perfect audit system.
When available, the notifier also uses ./state/player-identities.tsv to resolve player names for disconnect lines that do not contain a name directly.
World saves live under:
data/R5/Saved/SaveProfiles/Default/RocksDB/<game-version>/Worlds/
or
data/R5/Saved/SaveProfiles/Default/RocksDB_v2/<game-version>/Worlds/
Each world is a folder named with its world ID (for example EC10598E83A14ED04D9C44CBFBF3F4B1). The server loads the world whose ID matches WorldIslandId in ServerDescription.json.
Operator note: if both RocksDB and RocksDB_v2 exist at the same time, ./windrose switch still changes one global WorldIslandId value in ServerDescription.json, independent of layout. Save backups with scope save or both archive the whole R5/Saved tree, so both RocksDB and RocksDB_v2 are included when present.
⚠ Always back up your saves first. Also shut down both the dedicated server and the game client before copying files.
-
Stop the dedicated server:
./windrose stop
-
Locate the source world folder on the machine that currently has the save:
- Steam:
C:\Users\{UserName}\AppData\Local\R5\Saved\SaveProfiles\{YourProfile}\RocksDB\{GameVersion}\Worlds\{WorldID}or...\RocksDB_v2\{GameVersion}\Worlds\{WorldID} - EGS:
C:\Users\{UserName}\AppData\Local\R5\Saved\SaveProfiles\{YourProfile}\RocksDB\{GameVersion}\Worlds\{WorldID}or...\RocksDB_v2\{GameVersion}\Worlds\{WorldID} - Stove:
C:\Users\{UserName}\AppData\Local\R5\Saved\SaveProfiles\StoveDefault\RocksDB\{GameVersion}\Worlds\{WorldID}or...\RocksDB_v2\{GameVersion}\Worlds\{WorldID} - Example:
C:\Users\YarrHarrPirate\AppData\Local\R5\Saved\SaveProfiles\76561199699067790\RocksDB_v2\0.8.0\Worlds\EC10598E83A14ED04D9C44CBFBF3F4B1
- Steam:
-
Copy the entire world folder to the dedicated server data directory, preserving the folder name exactly:
data/R5/Saved/SaveProfiles/Default/RocksDB/<game-version>/Worlds/ or data/R5/Saved/SaveProfiles/Default/RocksDB_v2/<game-version>/Worlds/Example using
scpfrom a local machine (copy folder as-is):scp -r "./EC10598E83A14ED04D9C44CBFBF3F4B1" user@yourserver:/windrose/data/R5/Saved/SaveProfiles/Default/RocksDB_v2/<version>/Worlds/
Use the copied folder name exactly. Do not rename world folders.
Restore note: copy the
WorldIDfolder directly into.../Worlds/. Do not create nestedWorlds/Worlds/...paths. Helper commands (./windrose worlds,./windrose worlds-check,./windrose switch,./windrose worlds-prune) auto-detectRocksDB_v2orRocksDB. -
Set the world ID in
data/R5/ServerDescription.json:"WorldIslandId": "EC10598E83A14ED04D9C44CBFBF3F4B1"
Use the copied folder name exactly. Do not rename world folders.
-
Start the server:
./windrose start
-
Verify — check logs to confirm the correct world loaded:
./windrose logs
-
Server to client transfer: reverse the same steps in the opposite direction. If the game asks, choose local saves.
Note: The
<game-version>path segment is version-specific (for example0.8.0). Use the exact version directory that contains your world.
Use the built-in helper for a safer backup flow. It briefly stops the server, creates a timestamped archive, and starts it again if it was running. If the activity notifier (./windrose notify) was active before the backup, it is restarted automatically afterwards.
# Create a manual backup
./windrose backup
# Install a host cron job running daily at 06:00
./windrose install-backup-cron
# Or provide your own schedule
./windrose install-backup-cron "0 3 * * *"Backups are stored in backups by default and old archives are pruned after 7 days. You can change that in .env with BACKUP_DIR and BACKUP_RETENTION_DAYS. Relative paths in BACKUP_DIR are resolved relative to the repository directory, not the current working directory.
You can choose what gets archived in .env:
BACKUP_SCOPE=fullSupported values:
full(default): archive fullR5directorysave: archive only save data (R5/SavedandR5/ServerDescription.jsonwhen present)both: create both full and save archives in one run
You can choose the archive format in .env:
BACKUP_FORMAT=tar.gzSupported values:
tar.gz(default)zip(more convenient to open on Windows)
After each archive is created, the script runs an integrity test (tar -tzf or zip -T) and fails fast if verification does not pass.
If you use BACKUP_FORMAT=zip, the script checks whether zip is available.
In an interactive shell it asks whether it should install zip; in cron/non-interactive mode it exits with a clear error.
The installed cron job appends logs to backups/backup.log.
Before creating an archive, the backup script checks whether any players are currently online by reading recent container logs. If players are detected, the backup is aborted and a notification is sent via the configured provider (Discord or Gotify). To skip this check (for example in a maintenance window where you know the state), set:
BACKUP_SKIP_ONLINE_CHECK=trueYou can also enable backup result notifications in .env:
BACKUP_NOTIFY_SUCCESS=false
BACKUP_NOTIFY_FAIL=trueWhen enabled, backup status notifications use the same backend as ./windrose notify (NOTIFY_PROVIDER, Discord, or Gotify).
You can also upload the backup archive directly to a Discord channel after each successful backup:
BACKUP_DISCORD_UPLOAD=falseWhen set to true, Discord upload depends on BACKUP_SCOPE:
saveorboth: upload the newestwindrose-backup-save-*archive (.tar.gzor.zip)full: skip upload intentionally
Files larger than 25 MB are skipped with a warning (Discord free tier limit).
The backup script also checks for available disk space before creating an archive. It estimates the required space as 1.5× the size of the data directory plus a 2 GB safety margin. If the target disk does not have enough free space, the backup is aborted with a clear error. The check runs against the filesystem where BACKUP_DIR is mounted.
windrose/
├── Dockerfile # Ubuntu 22.04 + Wine + SteamCMD
├── docker-compose.yml # Service definition
├── scripts/ # Canonical runtime scripts used by container
├── .env # Environment variables (do not commit with secrets)
├── data/ # Persistent server files and saves (created on first run)
│ └── R5/
│ ├── ServerDescription.json
│ └── Saved/
├── steam-home/ # Wine prefix and SteamCMD state (created on first run)
├── backups/ # Archive files only (tar.gz, zip) from backup operations
├── logs/ # Log files (update, backup, player activity)
├── state/ # Metadata (player identities, event deduplication)
└── diagnostics/ # Diagnostics bundles (tar.gz archives)
Migration note: If you are upgrading from an older version with a combined backups/ folder, run the included migrate-folders.sh script once to reorganize files:
./migrate-folders.shThis moves log files, state files, and diagnostics to their respective folders while keeping backup archives in backups. The script is safe to run multiple times.
For the full symptom table, diagnostics playbooks, and network troubleshooting, see TROUBLESHOOTING.md.
Common quick fixes:
| Symptom | Fix |
|---|---|
wine: '/home/steam' is not owned by you |
Set PUID and PGID correctly in .env, then restart the container |
Server is already active for display 99 |
Stale Xvfb lock — entrypoint removes it automatically on restart |
| Config reset after restart | Edit JSON only when container is stopped |
| Server not visible to players | Share the InviteCode from ServerDescription.json |
| Players have issues after a game patch | Keep the dedicated server version updated to match the game version |
| Server fails to start or crashes silently in Proxmox | Set CPU type to host in the VM/LXC settings (see below) |
If you are hosting this server inside a Proxmox VM or LXC container, set the CPU type to host in the Proxmox configuration for that VM or container.
Proxmox's default CPU types (for example kvm64) omit instruction sets that Wine and the server binary may depend on. This can cause the server to fail to start, crash at runtime, or fail silently with no useful log output.
In the Proxmox web UI: VM → Hardware → Processors → Type → host.
Using host CPU type passes the physical CPU's full instruction set through to the VM, which is required for Wine to run the dedicated server binary reliably.
- Most users should keep
IMAGE_TAG=v1.6.4for a stable server. - Use
latestonly for testing. - Use
stagingonly as a fallback for Wine compatibility issues on a specific host. - Use
debugwhen you need extra troubleshooting tools inside the image. - To upgrade later, change
IMAGE_TAGin.env, then run:
docker compose pull
docker compose up -d- Supports configurable
PUIDandPGIDto align mounted volumes with the host network_mode: host— no Docker NAT, direct network access- Xvfb provides a headless X display required by Wine
stop_grace_period: 90s— allows the server to save before shutdown- Optional env-based patching can update
ServerDescription.jsonautomatically - Healthcheck can fail on recent fatal runtime log patterns, not just missing process state
- Canonical runtime scripts are under
/opt/windrose/scripts/*; root-level script files are compatibility wrappers - Compatibility wrappers are kept for backward compatibility and may be removed in a future major release after deprecation notice
See the Save transfer and world selection section. In short: back up first, stop both server and client, copy the full world folder into data/R5/Saved/SaveProfiles/Default/RocksDB_v2/<version>/Worlds/ (or .../RocksDB/<version>/Worlds/), set WorldIslandId to the exact folder name, then start the server.
Start the server once, wait until it is healthy, then open data/R5/ServerDescription.json and share the InviteCode value with players.
The first launch needs to download and prepare SteamCMD, Wine runtime files, and the dedicated server files. This can take several minutes depending on your network and the upstream mirrors.
This usually means the mounted host directories are owned by a different user than the container expects. Check PUID and PGID in your .env, then restart the container.
Use the built-in test command before you start the watcher:
./windrose notify testPull the latest repository changes first, then refresh the selected image tag and recreate the container:
git pull
./windrose update./windrose update writes detailed command output to backups/update.log and keeps three rotated history files (update.log.1, update.log.2, update.log.3).
Use ./windrose update-log [lines] to quickly inspect recent update details from the active log file.
Use a pinned version tag such as v1.6.4 for production stability. Use latest only when you want the newest changes for testing.
For developer image channels (dev, dev-staging, dev-debug), see DEVELOPMENT.md.
-
Clone and enter the repository:
git clone https://github.com/UberDudePL/windrose-dedicated-server-docker.git cd windrose-dedicated-server-docker -
Create
.envand adjust only required values:cp .env.example .env nano .env
Set at least
PUID,PGID, and optional server identity values (SERVER_NAME,INVITE_CODE). -
Run first launch:
./windrose setup
-
Verify running state before inviting players:
./windrose status ./windrose logs
-
Create a backup first:
./windrose backup
-
Stop server before any manual save copy/edit:
./windrose stop
-
Validate worlds and active world mapping:
./windrose worlds ./windrose worlds-check
-
Switch world with helper (recommended):
./windrose switch
-
Use prune in safe order:
./windrose worlds-prune ./windrose worlds-prune --apply
Default mode is dry-run.
--applyrequires confirmation in interactive shell and never removes the active world.
-
Check update status and details:
./windrose status ./windrose update-log 200
-
Keep mounts and compose defaults unchanged (
./data,./steam-home, ports, and network settings). -
Roll back to a known-good Git ref only if needed, then restart:
git checkout <known-good-tag-or-commit> ./windrose update --force-down
-
If startup still fails, generate diagnostics bundle for review:
./windrose diagnostics
If you need to roll back this script layout migration, use this short procedure:
-
Check out the previous known-good ref and rebuild:
git checkout <known-good-tag-or-commit> docker compose build --no-cache windrose
-
Recreate the service:
docker compose up -d windrose
-
Verify health and status:
./windrose status ./windrose doctor
This rollback does not require data migration and keeps existing save paths unchanged (./data, ./steam-home).
-
Pick the new stable version (example:
v1.6.0). -
Update version bump points before tagging:
.env.example: setIMAGE_TAG=v1.6.0README.md: update all stable version references (IMAGE_TAGdefault examples, quick start snippets, stable guidance lines)
-
Verify old stable version references are gone from
.env.exampleandREADME.md. -
Verify behavior locally before publishing:
bash -n serverctl.sh backup.sh notify.sh ./windrose status ./windrose worlds-prune ./windrose notify status
-
Commit docs/version changes and push them to
mainfirst. -
Run a manual approval checkpoint for script layout migration changes before tagging:
- Confirm compose parity checks passed.
- Confirm rollback procedure was tested and documented.
- Confirm root compatibility wrappers delegate correctly.
-
After
maincontains the version bump commit and manual approval is recorded, create and push the release tag. -
Publish the GitHub release notes for that tag.
-
If a tag was created too early, move it to the latest
maincommit before publishing release notes.
If you hit a bug or want a new feature, please open an issue in the GitHub repository.
If this project saved you time and you want to support further maintenance, you can use:
- Ko-fi: https://ko-fi.com/uberdudepl
- PayPal: https://paypal.me/uberdudepl
- Revolut: https://revolut.me/uberdudepl
MIT — see LICENSE