Skip to content

Instantly share code, notes, and snippets.

@iolson
Created September 7, 2026 22:13
Show Gist options
  • Select an option

  • Save iolson/4b34f99d2c4bbd3b0bd8bedb32e8b7f3 to your computer and use it in GitHub Desktop.

Select an option

Save iolson/4b34f99d2c4bbd3b0bd8bedb32e8b7f3 to your computer and use it in GitHub Desktop.
LANChamp Servers
#!/usr/bin/env bash
set -Eeuo pipefail
# LANChamp - Generic CS2 LAN Server Golden VM Installer
# Fresh Ubuntu Server amd64 -> SteamCMD -> CS2 -> Metamod -> CounterStrikeSharp -> MatchZy.
# The base image stays generic. MatchZy controls per-match team names.
# Run with: sudo bash ./lanchamp-cs2-server-setup.sh
if [[ $EUID -ne 0 ]]; then
echo "Run with sudo: sudo bash $0"
exit 1
fi
export DEBIAN_FRONTEND=noninteractive
CS2_USER="cs2"
CS2_HOME="/home/${CS2_USER}"
STEAMCMD_DIR="/opt/steamcmd"
CS2_DIR="/opt/cs2"
CSGO_DIR="${CS2_DIR}/game/csgo"
ENV_FILE="/etc/cs2-server.env"
echo "============================================================"
echo " LANChamp - Generic CS2 LAN Server Installer"
echo "============================================================"
read -rp "Server number [01]: " SERVER_NUMBER
SERVER_NUMBER="${SERVER_NUMBER:-01}"
DEFAULT_SERVER_NAME="LANChamp Server ${SERVER_NUMBER}"
read -rp "Fallback server name [${DEFAULT_SERVER_NAME}]: " SERVER_NAME
SERVER_NAME="${SERVER_NAME:-${DEFAULT_SERVER_NAME}}"
read -rp "Game/RCON port [27015]: " SERVER_PORT
SERVER_PORT="${SERVER_PORT:-27015}"
read -rp "CSTV port [27020]: " TV_PORT
TV_PORT="${TV_PORT:-27020}"
read -rp "Starting map [de_mirage]: " START_MAP
START_MAP="${START_MAP:-de_mirage}"
DEFAULT_MATCH_HOSTNAME="LANChamp | {TEAM1} vs {TEAM2}"
read -rp "MatchZy hostname format [${DEFAULT_MATCH_HOSTNAME}]: " MATCH_HOSTNAME_FORMAT
MATCH_HOSTNAME_FORMAT="${MATCH_HOSTNAME_FORMAT:-${DEFAULT_MATCH_HOSTNAME}}"
while true; do
read -rsp "RCON password: " RCON_PASSWORD
echo
[[ -n "${RCON_PASSWORD}" ]] && break
echo "RCON password cannot be empty."
done
read -rsp "GSLT (optional; press Enter to skip): " GSLT
echo
read -rp "MatchZy admin SteamID64 (optional; press Enter to skip): " MATCHZY_ADMIN
echo "[1/10] Installing dependencies..."
apt-get update
apt-get install -y ca-certificates curl jq unzip tar xz-utils \
lib32gcc-s1 lib32stdc++6 libicu-dev qemu-guest-agent python3
systemctl enable --now qemu-guest-agent || true
echo "[2/10] Creating service account..."
if ! id "${CS2_USER}" >/dev/null 2>&1; then
useradd --create-home --home-dir "${CS2_HOME}" --shell /bin/bash "${CS2_USER}"
fi
install -d -o "${CS2_USER}" -g "${CS2_USER}" "${STEAMCMD_DIR}" "${CS2_DIR}"
echo "[3/10] Installing SteamCMD..."
TMPDIR_INSTALL="$(mktemp -d)"
trap 'rm -rf "${TMPDIR_INSTALL}"' EXIT
curl -fL --retry 5 --retry-delay 2 \
"https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz" \
-o "${TMPDIR_INSTALL}/steamcmd_linux.tar.gz"
tar -xzf "${TMPDIR_INSTALL}/steamcmd_linux.tar.gz" -C "${STEAMCMD_DIR}"
chown -R "${CS2_USER}:${CS2_USER}" "${STEAMCMD_DIR}"
sudo -u "${CS2_USER}" -H "${STEAMCMD_DIR}/steamcmd.sh" +quit
STEAMCLIENT_SO="$(find "${STEAMCMD_DIR}" -type f -path '*/linux64/steamclient.so' -print -quit || true)"
if [[ -n "${STEAMCLIENT_SO}" ]]; then
install -d -o "${CS2_USER}" -g "${CS2_USER}" "${CS2_HOME}/.steam/sdk64"
ln -sfn "${STEAMCLIENT_SO}" "${CS2_HOME}/.steam/sdk64/steamclient.so"
chown -h "${CS2_USER}:${CS2_USER}" "${CS2_HOME}/.steam/sdk64/steamclient.so"
fi
echo "[4/10] Installing CS2 Dedicated Server..."
sudo -u "${CS2_USER}" -H "${STEAMCMD_DIR}/steamcmd.sh" \
+force_install_dir "${CS2_DIR}" \
+login anonymous \
+app_update 730 validate \
+quit
[[ -x "${CS2_DIR}/game/bin/linuxsteamrt64/cs2" ]] || {
echo "ERROR: CS2 server binary not found."
exit 1
}
echo "[5/10] Installing Metamod:Source..."
MM_FILE="$(curl -fsSL "https://mms.alliedmods.net/mmsdrop/2.0/mmsource-latest-linux" | tr -d '\r\n')"
[[ -n "${MM_FILE}" ]] || { echo "ERROR: Could not determine latest Metamod build."; exit 1; }
curl -fL --retry 5 --retry-delay 2 \
"https://mms.alliedmods.net/mmsdrop/2.0/${MM_FILE}" \
-o "${TMPDIR_INSTALL}/${MM_FILE}"
tar -xzf "${TMPDIR_INSTALL}/${MM_FILE}" -C "${CSGO_DIR}"
GAMEINFO="${CSGO_DIR}/gameinfo.gi"
if ! grep -Fq 'Game csgo/addons/metamod' "${GAMEINFO}"; then
cp -a "${GAMEINFO}" "${GAMEINFO}.pre-metamod"
sed -i '/Game_LowViolence[[:space:]]\+csgo_lv/a\ Game csgo/addons/metamod' "${GAMEINFO}"
fi
echo "[6/10] Installing CounterStrikeSharp with runtime..."
CSS_JSON="$(curl -fsSL "https://api.github.com/repos/roflmuffin/CounterStrikeSharp/releases/latest")"
CSS_TAG="$(jq -r '.tag_name // empty' <<<"${CSS_JSON}")"
CSS_URL="$(jq -r '[.assets[] | select(.name | test("counterstrikesharp-with-runtime-linux.*\\.zip$"; "i")) | .browser_download_url][0] // empty' <<<"${CSS_JSON}")"
[[ -n "${CSS_URL}" ]] || { echo "ERROR: CounterStrikeSharp Linux runtime asset not found."; exit 1; }
curl -fL --retry 5 --retry-delay 2 "${CSS_URL}" -o "${TMPDIR_INSTALL}/counterstrikesharp.zip"
unzip -oq "${TMPDIR_INSTALL}/counterstrikesharp.zip" -d "${CSGO_DIR}"
echo "[7/10] Installing MatchZy..."
MZ_JSON="$(curl -fsSL "https://api.github.com/repos/shobhit-pathak/MatchZy/releases/latest")"
MZ_TAG="$(jq -r '.tag_name // empty' <<<"${MZ_JSON}")"
MZ_URL="$(jq -r '[.assets[] | select(.name | test("^MatchZy-[0-9.]+\\.zip$")) | .browser_download_url][0] // empty' <<<"${MZ_JSON}")"
[[ -n "${MZ_URL}" ]] || { echo "ERROR: MatchZy plugin ZIP not found."; exit 1; }
curl -fL --retry 5 --retry-delay 2 "${MZ_URL}" -o "${TMPDIR_INSTALL}/matchzy.zip"
unzip -oq "${TMPDIR_INSTALL}/matchzy.zip" -d "${CSGO_DIR}"
echo "[8/10] Applying generic LAN configuration..."
cat > "${CSGO_DIR}/cfg/server.cfg" <<EOF
// LANChamp generic server-level settings.
// Match-specific competitive settings are managed by MatchZy.
sv_cheats 0
tv_enable 1
tv_port ${TV_PORT}
tv_autorecord 0
EOF
MATCHZY_CFG="${CSGO_DIR}/cfg/MatchZy/config.cfg"
if [[ -f "${MATCHZY_CFG}" ]]; then
sed -i -E 's|^[[:space:]]*matchzy_demo_recording_enabled[[:space:]].*$|matchzy_demo_recording_enabled true|' "${MATCHZY_CFG}"
python3 - "${MATCHZY_CFG}" "${MATCH_HOSTNAME_FORMAT}" <<'PY'
import re, sys
from pathlib import Path
path = Path(sys.argv[1])
fmt = sys.argv[2]
text = path.read_text()
line = f'matchzy_hostname_format "{fmt}"'
pat = r'(?m)^[ \t]*matchzy_hostname_format[ \t]+.*$'
text = re.sub(pat, line, text) if re.search(pat, text) else text + "\n" + line + "\n"
path.write_text(text)
PY
fi
if [[ -n "${MATCHZY_ADMIN}" ]]; then
install -d "${CSGO_DIR}/cfg/MatchZy"
cat > "${CSGO_DIR}/cfg/MatchZy/admins.json" <<EOF
{
"${MATCHZY_ADMIN}": ""
}
EOF
fi
install -d -o "${CS2_USER}" -g "${CS2_USER}" "${CSGO_DIR}/MatchZy"
chown -R "${CS2_USER}:${CS2_USER}" "${CS2_DIR}" "${STEAMCMD_DIR}"
cat > "${ENV_FILE}" <<EOF
SERVER_NUMBER=${SERVER_NUMBER@Q}
SERVER_NAME=${SERVER_NAME@Q}
SERVER_PORT=${SERVER_PORT@Q}
TV_PORT=${TV_PORT@Q}
START_MAP=${START_MAP@Q}
RCON_PASSWORD=${RCON_PASSWORD@Q}
GSLT=${GSLT@Q}
MATCH_HOSTNAME_FORMAT=${MATCH_HOSTNAME_FORMAT@Q}
EOF
chown root:"${CS2_USER}" "${ENV_FILE}"
chmod 0640 "${ENV_FILE}"
cat > /usr/local/bin/cs2-server-start <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail
source /etc/cs2-server.env
BIN="/opt/cs2/game/bin/linuxsteamrt64/cs2"
args=(
-dedicated -console -usercon -secure
-ip 0.0.0.0
-port "${SERVER_PORT}"
+map "${START_MAP}"
+hostname "${SERVER_NAME}"
+rcon_password "${RCON_PASSWORD}"
+exec server.cfg
)
[[ -n "${GSLT:-}" ]] && args+=(+sv_setsteamaccount "${GSLT}")
exec "${BIN}" "${args[@]}"
EOF
chmod 0755 /usr/local/bin/cs2-server-start
cat > /usr/local/sbin/cs2-game-update <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail
systemctl stop cs2-server.service || true
sudo -u cs2 -H /opt/steamcmd/steamcmd.sh \
+force_install_dir /opt/cs2 +login anonymous +app_update 730 validate +quit
GAMEINFO="/opt/cs2/game/csgo/gameinfo.gi"
if ! grep -Fq 'Game csgo/addons/metamod' "${GAMEINFO}"; then
sed -i '/Game_LowViolence[[:space:]]\+csgo_lv/a\ Game csgo/addons/metamod' "${GAMEINFO}"
fi
chown -R cs2:cs2 /opt/cs2
systemctl start cs2-server.service
echo "CS2 updated. Re-test Metamod, CounterStrikeSharp, MatchZy and demos."
EOF
chmod 0755 /usr/local/sbin/cs2-game-update
cat > /usr/local/sbin/lanchamp-server-profile <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail
[[ $EUID -eq 0 ]] || { echo "Run: sudo lanchamp-server-profile"; exit 1; }
ENV_FILE="/etc/cs2-server.env"
MATCHZY_CFG="/opt/cs2/game/csgo/cfg/MatchZy/config.cfg"
source "${ENV_FILE}"
echo "Current fallback hostname: ${SERVER_NAME}"
echo "Current MatchZy format: ${MATCH_HOSTNAME_FORMAT}"
echo
read -rp "Fallback server name [${SERVER_NAME}]: " NEW_SERVER_NAME
NEW_SERVER_NAME="${NEW_SERVER_NAME:-${SERVER_NAME}}"
read -rp "MatchZy hostname format [${MATCH_HOSTNAME_FORMAT}]: " NEW_MATCH_FORMAT
NEW_MATCH_FORMAT="${NEW_MATCH_FORMAT:-${MATCH_HOSTNAME_FORMAT}}"
python3 - "${ENV_FILE}" "${NEW_SERVER_NAME}" "${NEW_MATCH_FORMAT}" <<'PY'
import shlex, sys
from pathlib import Path
path = Path(sys.argv[1])
server_name, match_format = sys.argv[2], sys.argv[3]
lines = path.read_text().splitlines()
out = []
seen_server = seen_match = False
for line in lines:
if line.startswith("SERVER_NAME="):
out.append("SERVER_NAME=" + shlex.quote(server_name)); seen_server = True
elif line.startswith("MATCH_HOSTNAME_FORMAT="):
out.append("MATCH_HOSTNAME_FORMAT=" + shlex.quote(match_format)); seen_match = True
else:
out.append(line)
if not seen_server: out.append("SERVER_NAME=" + shlex.quote(server_name))
if not seen_match: out.append("MATCH_HOSTNAME_FORMAT=" + shlex.quote(match_format))
path.write_text("\n".join(out) + "\n")
PY
if [[ -f "${MATCHZY_CFG}" ]]; then
python3 - "${MATCHZY_CFG}" "${NEW_MATCH_FORMAT}" <<'PY'
import re, sys
from pathlib import Path
path = Path(sys.argv[1])
fmt = sys.argv[2]
text = path.read_text()
line = f'matchzy_hostname_format "{fmt}"'
pat = r'(?m)^[ \t]*matchzy_hostname_format[ \t]+.*$'
text = re.sub(pat, line, text) if re.search(pat, text) else text + "\n" + line + "\n"
path.write_text(text)
PY
fi
chown root:cs2 "${ENV_FILE}"
chmod 0640 "${ENV_FILE}"
chown -R cs2:cs2 /opt/cs2/game/csgo/cfg/MatchZy
systemctl restart cs2-server.service
echo "Updated."
echo "Fallback hostname: ${NEW_SERVER_NAME}"
echo "MatchZy format: ${NEW_MATCH_FORMAT}"
EOF
chmod 0755 /usr/local/sbin/lanchamp-server-profile
cat > /etc/systemd/system/cs2-server.service <<'EOF'
[Unit]
Description=LANChamp CS2 Dedicated Server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=cs2
Group=cs2
WorkingDirectory=/opt/cs2
ExecStart=/usr/local/bin/cs2-server-start
Restart=on-failure
RestartSec=5
LimitNOFILE=1048576
TimeoutStopSec=30
KillSignal=SIGINT
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable cs2-server.service
cat > /opt/cs2/INSTALLED-VERSIONS.txt <<EOF
Installed: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
Metamod: ${MM_FILE}
CounterStrikeSharp: ${CSS_TAG:-unknown}
MatchZy: ${MZ_TAG:-unknown}
EOF
chown "${CS2_USER}:${CS2_USER}" /opt/cs2/INSTALLED-VERSIONS.txt
echo "[9/10] Applying conservative VM setting..."
cat > /etc/sysctl.d/99-cs2-lan.conf <<'EOF'
vm.swappiness=1
EOF
sysctl --system >/dev/null
echo "[10/10] Starting CS2..."
systemctl start cs2-server.service
echo
echo "============================================================"
echo " Installation complete"
echo "============================================================"
echo "Fallback hostname: ${SERVER_NAME}"
echo "MatchZy hostname: ${MATCH_HOSTNAME_FORMAT}"
echo "Game/RCON port: ${SERVER_PORT}"
echo "CSTV port: ${TV_PORT}"
echo
echo "Commands:"
echo " sudo systemctl status cs2-server --no-pager"
echo " sudo journalctl -u cs2-server -f"
echo " sudo systemctl restart cs2-server"
echo " sudo cs2-game-update"
echo " sudo lanchamp-server-profile"
echo
echo "Use MatchZy for actual {TEAM1}/{TEAM2} match names."
#!/usr/bin/env bash
set -Eeuo pipefail
# Standalone copy of the profile helper installed by lanchamp-cs2-server-setup.sh.
[[ $EUID -eq 0 ]] || { echo "Run with sudo."; exit 1; }
ENV_FILE="/etc/cs2-server.env"
MATCHZY_CFG="/opt/cs2/game/csgo/cfg/MatchZy/config.cfg"
[[ -f "${ENV_FILE}" ]] || { echo "Missing ${ENV_FILE}"; exit 1; }
source "${ENV_FILE}"
echo "Current fallback hostname: ${SERVER_NAME}"
echo "Current MatchZy format: ${MATCH_HOSTNAME_FORMAT}"
read -rp "Fallback server name [${SERVER_NAME}]: " NEW_SERVER_NAME
NEW_SERVER_NAME="${NEW_SERVER_NAME:-${SERVER_NAME}}"
read -rp "MatchZy hostname format [${MATCH_HOSTNAME_FORMAT}]: " NEW_MATCH_FORMAT
NEW_MATCH_FORMAT="${NEW_MATCH_FORMAT:-${MATCH_HOSTNAME_FORMAT}}"
python3 - "${ENV_FILE}" "${NEW_SERVER_NAME}" "${NEW_MATCH_FORMAT}" <<'PY'
import shlex, sys
from pathlib import Path
path = Path(sys.argv[1])
server_name, match_format = sys.argv[2], sys.argv[3]
lines = path.read_text().splitlines()
out=[]
for line in lines:
if line.startswith("SERVER_NAME="):
out.append("SERVER_NAME=" + shlex.quote(server_name))
elif line.startswith("MATCH_HOSTNAME_FORMAT="):
out.append("MATCH_HOSTNAME_FORMAT=" + shlex.quote(match_format))
else:
out.append(line)
path.write_text("\n".join(out) + "\n")
PY
if [[ -f "${MATCHZY_CFG}" ]]; then
python3 - "${MATCHZY_CFG}" "${NEW_MATCH_FORMAT}" <<'PY'
import re, sys
from pathlib import Path
path=Path(sys.argv[1]); fmt=sys.argv[2]
text=path.read_text()
line=f'matchzy_hostname_format "{fmt}"'
pat=r'(?m)^[ \t]*matchzy_hostname_format[ \t]+.*$'
path.write_text(re.sub(pat,line,text) if re.search(pat,text) else text+"\n"+line+"\n")
PY
fi
chown root:cs2 "${ENV_FILE}"
chmod 0640 "${ENV_FILE}"
chown -R cs2:cs2 /opt/cs2/game/csgo/cfg/MatchZy
systemctl restart cs2-server.service
echo "Profile updated."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment