Created
September 7, 2026 22:13
-
-
Save iolson/4b34f99d2c4bbd3b0bd8bedb32e8b7f3 to your computer and use it in GitHub Desktop.
LANChamp Servers
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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." |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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