How to build a continuous deployment (CI/CD) script for your MU Online server
Build a continuous deployment pipeline for your MU Online server, automating emulator builds, database migrations, client file distribution, and safe GameServer/ConnectServer restarts.
Keeping a private MU Online server up to date by hand — copying DLLs, editing configs, and restarting services manually — works while the project is small, but becomes a source of human error the moment the team grows or the update frequency increases. A continuous deployment (CI/CD) script fixes th
Keeping a private MU Online server up to date by hand — copying DLLs, editing configs, and restarting services manually — works while the project is small, but becomes a source of human error the moment the team grows or the update frequency increases. A continuous deployment (CI/CD) script fixes this: every change to the emulator code, configuration files, or database goes through a repeatable, tested, and reversible pipeline. This tutorial shows how to structure that pipeline from scratch, from infrastructure requirements to the rollback script, covering GameServer, ConnectServer, the database, and configuration files.
Why automate deployment for an MU server
A typical MU server has multiple components that need to be updated together: the GameServer binary (compiled from the emulator source, e.g. IGCN/MuEMU/OpenMU), the ConnectServer, the .txt/.xml configuration files for items and monsters, and the database schema (MSSQL or MySQL, depending on the emulator). Updating these components out of order — for example, pushing a new ItemList.txt without the matching stored procedure — is the most common cause of post-update crashes. A continuous deployment pipeline guarantees everything goes up in the right order, with validation at every step.
Infrastructure prerequisites
- A version-controlled repository (Git) with the emulator source code and configuration files.
- A staging environment — a replica of the production server, even if smaller, to validate builds before release.
- SSH/WinRM access to the production server(s), using a dedicated service account (not the administrator's personal account).
- A CI runner (GitHub Actions, GitLab CI, Jenkins, or even a scheduled script via Task Scheduler/cron) to execute the pipeline.
- Automated backup of the database and configuration files before any deploy.
Repository structure
Organize the repository by clearly separating what is compilable source code from what is game data configuration:
mu-server/
├── src/ # emulator source code (GameServer, ConnectServer)
├── config/ # environment configuration .ini/.txt files
├── db/
│ ├── migrations/ # sequentially numbered .sql scripts
│ └── seed/ # initial data (items, monsters, maps)
├── deploy/
│ ├── deploy.sh # main deploy script
│ ├── rollback.sh # rollback script
│ └── healthcheck.sh # post-deploy validation
└── .github/workflows/deploy.yml
This separation lets you version gameplay changes (a new event, a new item) independently from infrastructure changes (a bug fix in the emulator core).
Step 1 — Building the emulator
The pipeline starts by compiling the GameServer/ConnectServer source code. Here's an example build step in bash, assuming a C++ project using CMake:
#!/bin/bash
set -euo pipefail
echo "[deploy] Building GameServer..."
cd src/GameServer
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release -j"$(nproc)"
if [ ! -f build/GameServer.exe ]; then
echo "[deploy] ERROR: build failed, binary was not generated."
exit 1
fi
echo "[deploy] Build completed successfully."
If your emulator is distributed as a closed binary (no recompilation), this step becomes just an integrity check (checksum) of the downloaded package before proceeding.
Step 2 — Database migration
Every schema change must live in a numbered, versioned migration file, never applied manually via SSMS/HeidiSQL directly in production. Here's an example script for applying incremental migrations:
#!/bin/bash
set -euo pipefail
DB_HOST="$1"
LAST_APPLIED=$(sqlcmd -S "$DB_HOST" -Q "SELECT MAX(version) FROM SchemaMigrations" -h -1)
for file in db/migrations/*.sql; do
version=$(basename "$file" | cut -d'_' -f1)
if [ "$version" -gt "$LAST_APPLIED" ]; then
echo "[deploy] Applying migration $file"
sqlcmd -S "$DB_HOST" -i "$file"
sqlcmd -S "$DB_HOST" -Q "INSERT INTO SchemaMigrations (version, applied_at) VALUES ($version, GETDATE())"
fi
done
Never use a destructive DROP TABLE or ALTER COLUMN in a migration without first confirming the pre-deploy backup completed successfully.
Step 3 — Automatic backup before deploy
The backup step must be blocking: if it fails, the entire pipeline stops and nothing gets deployed. A minimal example:
#!/bin/bash
set -euo pipefail
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/backups/mu/$TIMESTAMP"
mkdir -p "$BACKUP_DIR"
sqlcmd -S "$DB_HOST" -Q "BACKUP DATABASE MuOnline TO DISK='$BACKUP_DIR/MuOnline.bak'"
cp -r /opt/mu-server/config "$BACKUP_DIR/config"
tar -czf "$BACKUP_DIR/binaries.tar.gz" /opt/mu-server/bin
echo "[deploy] Backup saved to $BACKUP_DIR"
Keep at least the last 7 daily backups and 4 weekly backups, with automatic rotation so you don't run out of disk space.
Step 4 — Distributing files to the server
With the build validated and the backup complete, the pipeline copies the artifacts to the production server(s). Using rsync over SSH:
rsync -avz --delete \
--exclude 'config/local.ini' \
build/GameServer.exe deploy/ \
user@mu-prod:/opt/mu-server/bin/
The --exclude flag matters here: environment-specific configuration files (database passwords, license keys) should never be overwritten by the pipeline — they live outside version control, managed via environment variables or a secrets vault (Vault, AWS Secrets Manager).
Step 5 — Safe service restart
Restarting the GameServer drops every connected player, so this step must be timed and communicated. A controlled restart script:
#!/bin/bash
set -euo pipefail
echo "[deploy] Warning players about restart in 5 minutes..."
echo "notice Server will restart in 5 minutes for maintenance" > /opt/mu-server/gs_console_pipe
sleep 300
systemctl stop mu-gameserver
systemctl stop mu-connectserver
cp /tmp/new_build/GameServer.exe /opt/mu-server/bin/
systemctl start mu-connectserver
systemctl start mu-gameserver
Always restart the ConnectServer last, so it only starts accepting new connections once the GameServer is already up.
Step 6 — Post-deploy healthcheck
After the restart, automatically validate that services came up correctly before declaring the deploy complete:
#!/bin/bash
RETRIES=10
for i in $(seq 1 $RETRIES); do
if nc -z localhost 44405; then
echo "[deploy] ConnectServer responding on port 44405."
exit 0
fi
echo "[deploy] Waiting for ConnectServer to come up... ($i/$RETRIES)"
sleep 10
done
echo "[deploy] ERROR: ConnectServer did not respond. Starting rollback."
exit 1
If the healthcheck fails, the pipeline should automatically trigger the rollback script, without waiting for manual intervention.
Step 7 — Rollback script
The rollback restores the backup generated in Step 3 and reverts the binaries:
#!/bin/bash
set -euo pipefail
BACKUP_DIR="$1"
systemctl stop mu-gameserver mu-connectserver
sqlcmd -S "$DB_HOST" -Q "RESTORE DATABASE MuOnline FROM DISK='$BACKUP_DIR/MuOnline.bak' WITH REPLACE"
tar -xzf "$BACKUP_DIR/binaries.tar.gz" -C /
systemctl start mu-connectserver mu-gameserver
echo "[deploy] Rollback completed from $BACKUP_DIR"
Test this script periodically in staging — a rollback that's never been tested has a high chance of failing exactly when you need it most.
Integrating with GitHub Actions
An example workflow that orchestrates the steps above, triggered on a push to the main branch after pull request approval:
name: Deploy MU Server
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: ./deploy/build.sh
- name: Backup production
run: ./deploy/backup.sh
- name: Migrate database
run: ./deploy/migrate.sh ${{ secrets.DB_HOST }}
- name: Distribute files
run: ./deploy/rsync-deploy.sh
- name: Restart services
run: ssh deploy@mu-prod './deploy/restart.sh'
- name: Healthcheck
run: ./deploy/healthcheck.sh || ./deploy/rollback.sh
Keep secrets (database host, SSH keys) in the repository's Secrets, never hardcoded in the YAML.
Staging environment as a mandatory gate
No build should go straight to production. Configure the pipeline to first deploy to staging, run an automated smoke test (login, character creation, teleport, item drop), and only then release the production deploy — manually or automatically, depending on the team's maturity. This gate is what separates continuous deployment from "deploying blind."
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| GameServer won't start after deploy | DLL/binary incompatible with the current database schema | Run migrations before swapping the binary, never after |
| Rollback fails | Rollback script never tested in staging | Test the rollback on every major release, not just document it |
| Players get dropped without warning | Restart not announced in advance | Add an automated in-game/Discord message before the restart |
| Pipeline hangs on backup | Disk full on the database server | Configure rotation of old backups and monitor free space |
| Local configuration overwritten | rsync without excluding environment files | Explicitly list the config files that don't come from the repository |
Pipeline implementation checklist
- Versioned repository with separation between code, config, and database migrations.
- Functional staging environment to validate builds before production.
- Blocking automated backup script before every deploy.
- Numbered database migrations applied in order.
- Restart script with advance notice to players.
- Automated post-deploy healthcheck.
- Rollback script tested periodically in staging.
- Secrets (passwords, keys) kept out of version control.
With the continuous deployment pipeline up and running, the next step is to apply this same automation discipline to the initial environment setup — see the MU Online server creation tutorial to understand the foundation this pipeline was built on.
Frequently asked questions
Do I need a dedicated CI server (Jenkins/GitLab) for this?
It's not mandatory. You can start with a bash/PowerShell script triggered manually or by a scheduled task, and evolve into GitHub Actions/GitLab CI/Jenkins as the team grows. What matters is that the process is repeatable and versioned, not the tool itself.
Is it safe to automate deployment on a live MU server?
Yes, as long as the pipeline includes an automatic backup before every deploy, a staging environment to validate the build, and a tested rollback step. Manual deployment tends to be riskier than a well-designed pipeline, because it depends on human memory.
How should I handle the game client in continuous deployment?
The client (the files players download) usually has a separate pipeline from the server, publishing update packages through the launcher. The server deploy should check protocol version compatibility before releasing the client update.
What should I do if a deploy breaks the GameServer in production?
The pipeline should have an automatic rollback step: restore the previous binary/DLL and the pre-deploy database dump, then restart the services. This rollback needs to be tested periodically, not just written and forgotten.
Can I deploy without dropping online players?
Partially. Swapping GameServer DLLs usually requires a process restart, so the best approach is to schedule deploys during low-traffic windows and notify the community in advance. For ConnectServer and web services, zero-downtime deployment is possible using a simple load balancer.