diff --git a/README.md b/README.md
index 3e3098c..16ec082 100644
--- a/README.md
+++ b/README.md
@@ -51,6 +51,26 @@ This tutorial will be showing how to install in both FreeBSD and Windows environ
+## Debian (systemd)
+
+For the Debian runtime used in production, the repository now also contains versioned `systemd` deployment files under [deploy/systemd](deploy/systemd/README.md).
+
+Example installation:
+
+```bash
+python3 deploy/systemd/install_systemd.py \
+ --user mt2.jakubkadlec.dev \
+ --group mt2.jakubkadlec.dev \
+ --runtime-root /home/mt2.jakubkadlec.dev/metin/runtime/server \
+ --channel 1 \
+ --channel 99 \
+ --restart
+```
+
+This installs the stack units, direct game/db launch helpers and a DB readiness gate so `auth` and `game` do not start before the DB socket is listening.
+
+
+
##  **FreeBSD**
### ⬇️ Obtaining the Serverfiles
@@ -773,4 +793,4 @@ After finishing this part, you should now have knowledge of:
## Next steps
After following either the **FreeBSD** method or the **Windows** method, you should be ready to proceed to cloning, building and distributing the [Client Source project](https://github.com/d1str4ught/m2dev-client-src)
-⭐ **NEW**: We are now on Discord, feel free to [check us out](https://discord.gg/ETnBChu2Ca)!
\ No newline at end of file
+⭐ **NEW**: We are now on Discord, feel free to [check us out](https://discord.gg/ETnBChu2Ca)!
diff --git a/deploy/systemd/README.md b/deploy/systemd/README.md
new file mode 100644
index 0000000..de36b60
--- /dev/null
+++ b/deploy/systemd/README.md
@@ -0,0 +1,32 @@
+# systemd deployment
+
+This directory contains the versioned systemd deployment used for the Debian runtime.
+
+## Install
+
+Run the installer as `root` and point it at the live runtime root:
+
+```bash
+cd /path/to/m2dev-server
+python3 deploy/systemd/install_systemd.py \
+ --user mt2.jakubkadlec.dev \
+ --group mt2.jakubkadlec.dev \
+ --runtime-root /home/mt2.jakubkadlec.dev/metin/runtime/server \
+ --channel 1 \
+ --channel 99 \
+ --restart
+```
+
+`--channel-limit 1` is also supported and will auto-include channel `99` when present in `channels.py`.
+
+## What it installs
+
+- `metin-server.service`
+- `metin-db.service`
+- `metin-db-ready.service`
+- `metin-auth.service`
+- `metin-game@.service`
+- `/usr/local/libexec/metin-game-instance-start`
+- `/usr/local/libexec/metin-wait-port`
+
+The `metin-db-ready.service` gate waits until the DB socket is actually accepting connections before `auth` and `game` units start.
diff --git a/deploy/systemd/bin/metin-game-instance-start.in b/deploy/systemd/bin/metin-game-instance-start.in
new file mode 100644
index 0000000..906a42b
--- /dev/null
+++ b/deploy/systemd/bin/metin-game-instance-start.in
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+instance="${1:?missing instance name}"
+root_dir="{{RUNTIME_ROOT}}/channels"
+channel_dir="${instance%_*}"
+core_dir="${instance##*_}"
+workdir="${root_dir}/${channel_dir}/${core_dir}"
+binary="./${instance}"
+
+if [ ! -d "$workdir" ]; then
+ echo "Missing workdir for instance ${instance}: ${workdir}" >&2
+ exit 1
+fi
+
+cd "$workdir"
+
+if [ ! -x "$binary" ]; then
+ echo "Missing executable for instance ${instance}: ${workdir}/${instance}" >&2
+ exit 1
+fi
+
+exec "$binary"
diff --git a/deploy/systemd/bin/metin-wait-port b/deploy/systemd/bin/metin-wait-port
new file mode 100644
index 0000000..de3dcb1
--- /dev/null
+++ b/deploy/systemd/bin/metin-wait-port
@@ -0,0 +1,29 @@
+#!/usr/bin/env python3
+import socket
+import sys
+import time
+
+
+def main() -> int:
+ if len(sys.argv) < 3:
+ print("usage: metin-wait-port [timeout_secs]", file=sys.stderr)
+ return 2
+
+ host = sys.argv[1]
+ port = int(sys.argv[2])
+ timeout_secs = float(sys.argv[3]) if len(sys.argv) > 3 else 30.0
+ deadline = time.monotonic() + timeout_secs
+
+ while time.monotonic() < deadline:
+ try:
+ with socket.create_connection((host, port), timeout=0.5):
+ return 0
+ except OSError:
+ time.sleep(0.2)
+
+ print(f"Timed out waiting for {host}:{port}", file=sys.stderr)
+ return 1
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/deploy/systemd/install_systemd.py b/deploy/systemd/install_systemd.py
new file mode 100644
index 0000000..693a2f6
--- /dev/null
+++ b/deploy/systemd/install_systemd.py
@@ -0,0 +1,169 @@
+#!/usr/bin/env python3
+import argparse
+import os
+import shutil
+import subprocess
+import sys
+from pathlib import Path
+
+SCRIPT_DIR = Path(__file__).resolve().parent
+REPO_ROOT = SCRIPT_DIR.parent.parent
+sys.path.insert(0, str(REPO_ROOT))
+
+import channels
+
+
+TEMPLATES_DIR = SCRIPT_DIR / "templates"
+BIN_DIR = SCRIPT_DIR / "bin"
+
+
+def parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(description="Install Metin systemd units")
+ parser.add_argument("--user", required=True, help="Runtime user")
+ parser.add_argument("--group", help="Runtime group, defaults to --user")
+ parser.add_argument("--runtime-root", required=True, help="Absolute path to the live runtime root")
+ parser.add_argument("--systemd-dir", default="/etc/systemd/system", help="systemd unit destination")
+ parser.add_argument("--libexec-dir", default="/usr/local/libexec", help="Helper script destination")
+ parser.add_argument("--wait-host", default="127.0.0.1", help="DB readiness host")
+ parser.add_argument("--wait-port", type=int, default=9000, help="DB readiness port")
+ parser.add_argument("--wait-timeout", type=int, default=30, help="DB readiness timeout in seconds")
+ parser.add_argument("--restart", action="store_true", help="Restart metin-server.service after install")
+
+ channel_group = parser.add_mutually_exclusive_group(required=True)
+ channel_group.add_argument(
+ "--channel-limit",
+ type=int,
+ help="Enable channels up to this number and keep channel 99 if present",
+ )
+ channel_group.add_argument(
+ "--channel",
+ dest="channels",
+ action="append",
+ type=int,
+ help="Enable an explicit channel, may be passed multiple times",
+ )
+ return parser.parse_args()
+
+
+def ensure_root() -> None:
+ if os.geteuid() != 0:
+ print("This installer must run as root.", file=sys.stderr)
+ raise SystemExit(1)
+
+
+def render_template(template_path: Path, values: dict[str, str]) -> str:
+ content = template_path.read_text(encoding="utf-8")
+ for key, value in values.items():
+ content = content.replace(f"{{{{{key}}}}}", value)
+ return content
+
+
+def write_text(path: Path, content: str, mode: int) -> None:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(content, encoding="utf-8")
+ os.chmod(path, mode)
+
+
+def copy_file(source: Path, destination: Path, mode: int) -> None:
+ destination.parent.mkdir(parents=True, exist_ok=True)
+ shutil.copy2(source, destination)
+ os.chmod(destination, mode)
+
+
+def resolve_channels(args: argparse.Namespace) -> list[int]:
+ available_channels = {int(channel_id) for channel_id in channels.CHANNEL_MAP.keys()}
+
+ if args.channels:
+ selected = set(args.channels)
+ else:
+ selected = {channel_id for channel_id in available_channels if channel_id <= args.channel_limit}
+
+ if 99 in available_channels:
+ selected.add(99)
+
+ unknown = sorted(selected - available_channels)
+ if unknown:
+ print(f"Unknown channels requested: {unknown}", file=sys.stderr)
+ raise SystemExit(1)
+
+ return sorted(selected)
+
+
+def resolve_instances(selected_channels: list[int]) -> list[str]:
+ instances: list[str] = []
+ for channel_id in selected_channels:
+ cores = channels.CHANNEL_MAP[int(channel_id)]
+ for core_id in sorted(int(core) for core in cores.keys()):
+ instances.append(f"channel{channel_id}_core{core_id}")
+ return instances
+
+
+def run(command: list[str]) -> None:
+ subprocess.run(command, check=True)
+
+
+def main() -> int:
+ args = parse_args()
+ ensure_root()
+
+ group_name = args.group or args.user
+ runtime_root = str(Path(args.runtime_root).resolve())
+ systemd_dir = Path(args.systemd_dir)
+ libexec_dir = Path(args.libexec_dir)
+
+ selected_channels = resolve_channels(args)
+ instances = resolve_instances(selected_channels)
+
+ template_values = {
+ "USER_NAME": args.user,
+ "GROUP_NAME": group_name,
+ "RUNTIME_ROOT": runtime_root,
+ "WAIT_HOST": args.wait_host,
+ "WAIT_PORT": str(args.wait_port),
+ "WAIT_TIMEOUT": str(args.wait_timeout),
+ "WAIT_TIMEOUT_SYSTEMD": str(args.wait_timeout + 5),
+ }
+
+ unit_names = [
+ "metin-server.service",
+ "metin-db.service",
+ "metin-db-ready.service",
+ "metin-auth.service",
+ "metin-game@.service",
+ ]
+
+ for unit_name in unit_names:
+ template_path = TEMPLATES_DIR / f"{unit_name}.in"
+ write_text(systemd_dir / unit_name, render_template(template_path, template_values), 0o644)
+
+ write_text(
+ libexec_dir / "metin-game-instance-start",
+ render_template(BIN_DIR / "metin-game-instance-start.in", template_values),
+ 0o755,
+ )
+ copy_file(BIN_DIR / "metin-wait-port", libexec_dir / "metin-wait-port", 0o755)
+
+ verify_units = [str(systemd_dir / unit_name) for unit_name in unit_names]
+ run(["systemd-analyze", "verify", *verify_units])
+ run(["systemctl", "daemon-reload"])
+
+ enable_units = [
+ "metin-server.service",
+ "metin-db.service",
+ "metin-db-ready.service",
+ "metin-auth.service",
+ *[f"metin-game@{instance}.service" for instance in instances],
+ ]
+ run(["systemctl", "enable", *enable_units])
+
+ if args.restart:
+ run(["systemctl", "restart", "metin-server.service"])
+
+ print("Installed systemd units for instances:")
+ for instance in instances:
+ print(f" - {instance}")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/deploy/systemd/templates/metin-auth.service.in b/deploy/systemd/templates/metin-auth.service.in
new file mode 100644
index 0000000..86b1621
--- /dev/null
+++ b/deploy/systemd/templates/metin-auth.service.in
@@ -0,0 +1,22 @@
+[Unit]
+Description=Metin auth runtime
+After=metin-db-ready.service
+Wants=metin-db-ready.service
+Requires=metin-db-ready.service
+PartOf=metin-server.service
+Before=metin-server.service
+
+[Service]
+Type=simple
+User={{USER_NAME}}
+Group={{GROUP_NAME}}
+WorkingDirectory={{RUNTIME_ROOT}}/channels/auth
+ExecStart={{RUNTIME_ROOT}}/channels/auth/game_auth
+Restart=on-failure
+RestartSec=5
+KillSignal=SIGTERM
+TimeoutStopSec=60
+LimitNOFILE=65535
+
+[Install]
+RequiredBy=metin-server.service
diff --git a/deploy/systemd/templates/metin-db-ready.service.in b/deploy/systemd/templates/metin-db-ready.service.in
new file mode 100644
index 0000000..fae50a0
--- /dev/null
+++ b/deploy/systemd/templates/metin-db-ready.service.in
@@ -0,0 +1,16 @@
+[Unit]
+Description=Metin DB readiness gate
+After=metin-db.service
+Wants=metin-db.service
+Requires=metin-db.service
+PartOf=metin-server.service
+Before=metin-auth.service metin-game@.service metin-server.service
+
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+ExecStart=/usr/local/libexec/metin-wait-port {{WAIT_HOST}} {{WAIT_PORT}} {{WAIT_TIMEOUT}}
+TimeoutStartSec={{WAIT_TIMEOUT_SYSTEMD}}
+
+[Install]
+RequiredBy=metin-server.service
diff --git a/deploy/systemd/templates/metin-db.service.in b/deploy/systemd/templates/metin-db.service.in
new file mode 100644
index 0000000..33109fe
--- /dev/null
+++ b/deploy/systemd/templates/metin-db.service.in
@@ -0,0 +1,22 @@
+[Unit]
+Description=Metin database runtime
+After=network-online.target mariadb.service
+Wants=network-online.target
+Requires=mariadb.service
+PartOf=metin-server.service
+Before=metin-server.service
+
+[Service]
+Type=simple
+User={{USER_NAME}}
+Group={{GROUP_NAME}}
+WorkingDirectory={{RUNTIME_ROOT}}/channels/db
+ExecStart={{RUNTIME_ROOT}}/channels/db/db
+Restart=on-failure
+RestartSec=5
+KillSignal=SIGTERM
+TimeoutStopSec=60
+LimitNOFILE=65535
+
+[Install]
+RequiredBy=metin-server.service
diff --git a/deploy/systemd/templates/metin-game@.service.in b/deploy/systemd/templates/metin-game@.service.in
new file mode 100644
index 0000000..dd15b94
--- /dev/null
+++ b/deploy/systemd/templates/metin-game@.service.in
@@ -0,0 +1,22 @@
+[Unit]
+Description=Metin game runtime (%i)
+After=metin-auth.service metin-db-ready.service
+Wants=metin-auth.service metin-db-ready.service
+Requires=metin-auth.service metin-db-ready.service
+PartOf=metin-server.service
+Before=metin-server.service
+
+[Service]
+Type=simple
+User={{USER_NAME}}
+Group={{GROUP_NAME}}
+WorkingDirectory={{RUNTIME_ROOT}}
+ExecStart=/usr/local/libexec/metin-game-instance-start %i
+Restart=on-failure
+RestartSec=5
+KillSignal=SIGTERM
+TimeoutStopSec=60
+LimitNOFILE=65535
+
+[Install]
+RequiredBy=metin-server.service
diff --git a/deploy/systemd/templates/metin-server.service.in b/deploy/systemd/templates/metin-server.service.in
new file mode 100644
index 0000000..cd3014c
--- /dev/null
+++ b/deploy/systemd/templates/metin-server.service.in
@@ -0,0 +1,15 @@
+[Unit]
+Description=Metin server stack
+After=network-online.target mariadb.service
+Wants=network-online.target
+Requires=mariadb.service
+
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+ExecStart=/bin/true
+ExecStop=/bin/true
+TimeoutStopSec=60
+
+[Install]
+WantedBy=multi-user.target