cs16-dm-server: CS 1.6 deathmatch server in Docker
One-command setup (SteamCMD + pinned, checksum-verified ReHLDS / ReGameDLL / Metamod-R / AMXX / ReAPI / ReUnion / CSDM-ReAPI / YaPB / GunGame), .env settings, ./cs menu (map, DM/GunGame mode, bots, kill counter, restart, update), own plugins (extras, rtv, mode switch), all-time kill counter, watchdog + nightly backup, docs for admins and players. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
7e9cbee14d
33 files changed
+1890
No files matched your search
@@ -0,0 +1,131 @@
|
||||
#!/bin/sh
|
||||
# cs16-dm-server setup: run from the repo folder on a Linux machine with Docker.
|
||||
# ./setup.sh first install (asks a few questions), or re-run safely any time
|
||||
# ./setup.sh --yes no questions: use .env as it is (or the defaults)
|
||||
# ./setup.sh --update also update the game files from Steam (then re-installs the mods)
|
||||
# Steps: check tools -> .env -> secrets -> build image -> download CS 1.6 (SteamCMD, ~600 MB)
|
||||
# -> install the mod stack (pinned downloads) -> start -> check the server answers.
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
YES=; UPDATE=
|
||||
for a in "$@"; do
|
||||
case "$a" in
|
||||
--yes|-y) YES=1 ;;
|
||||
--update) UPDATE=1 ;;
|
||||
*) echo "unknown option: $a"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
say() { printf '\n\033[1m== %s\033[0m\n' "$*"; }
|
||||
die() { printf '\033[31merror:\033[0m %s\n' "$*" >&2; exit 1; }
|
||||
setenv() { # setenv KEY VALUE: replace the KEY= line in .env
|
||||
v=$(printf '%s' "$2" | tr -d '"' | sed 's/[\\&|]/\\&/g')
|
||||
sed -i "s|^$1=.*|$1=$v|" .env
|
||||
}
|
||||
getenv() { sed -n "s/^$1=//p" .env | tr -d '\r'; }
|
||||
ask() { # ask "question" default -> answer (default when --yes or no terminal)
|
||||
if [ -n "$YES" ] || [ ! -t 0 ]; then printf '%s' "$2"; return; fi
|
||||
printf '%s [%s]: ' "$1" "$2" >&2; read -r r; printf '%s' "${r:-$2}"
|
||||
}
|
||||
randpw() { python3 -c 'import secrets,string; print("".join(secrets.choice(string.ascii_letters+string.digits) for _ in range(32)))'; }
|
||||
|
||||
say "Checking tools"
|
||||
[ "$(id -u)" != 0 ] || die "don't run this as root: use a normal user in the 'docker' group"
|
||||
command -v docker >/dev/null || die "docker is not installed (https://docs.docker.com/engine/install/)"
|
||||
docker info >/dev/null 2>&1 || die "can't talk to docker: add yourself to the docker group (sudo usermod -aG docker \$USER, then log in again)"
|
||||
docker compose version >/dev/null 2>&1 || die "'docker compose' (v2 plugin) is missing"
|
||||
command -v python3 >/dev/null || die "python3 is missing (used by ./rcon, ./kills and the health checks)"
|
||||
echo "ok"
|
||||
|
||||
say "Settings (.env)"
|
||||
if [ ! -f .env ]; then
|
||||
cp .env.example .env
|
||||
setenv CS_HOSTNAME "$(ask 'Server name' "$(getenv CS_HOSTNAME)")"
|
||||
setenv CS_PASSWORD "$(ask 'Join password (empty = open server)' '')"
|
||||
setenv CS_PORT "$(ask 'Game port (UDP)' "$(getenv CS_PORT)")"
|
||||
m=$(ask 'Game mode: dm = deathmatch, gg = GunGame' "$(getenv CS_MODE)")
|
||||
case "$m" in gg|GG) setenv CS_MODE gg ;; *) setenv CS_MODE dm ;; esac
|
||||
b=$(ask 'Bots (fill the server up to this many players, 0 = none)' "$(getenv CS_BOTS)")
|
||||
case "$b" in ''|*[!0-9]*) b=0 ;; esac
|
||||
setenv CS_BOTS "$b"
|
||||
echo "created .env (edit it any time, then: docker compose up -d)"
|
||||
else
|
||||
echo "using existing .env"
|
||||
fi
|
||||
setenv CS_UID "$(id -u)"
|
||||
setenv CS_GID "$(id -g)"
|
||||
PORT=$(getenv CS_PORT); NAME=$(getenv CS_NAME)
|
||||
RUNNING=$(docker inspect -f '{{.State.Status}}' "$NAME" 2>/dev/null || true)
|
||||
if [ "$RUNNING" != running ] && command -v ss >/dev/null; then
|
||||
! ss -lun | grep -q ":$PORT " || die "UDP port $PORT is already in use: set another CS_PORT in .env"
|
||||
fi
|
||||
echo "server '$(getenv CS_HOSTNAME)', container $NAME, UDP $PORT, bots $(getenv CS_BOTS)"
|
||||
|
||||
say "Secrets"
|
||||
mkdir -p secrets hlds steamcmd-home dl backups
|
||||
chmod 700 secrets backups
|
||||
umask 077
|
||||
[ -s secrets/rcon-password.txt ] || { randpw > secrets/rcon-password.txt; echo "new RCON password in secrets/rcon-password.txt"; }
|
||||
# ReUnion salt: non-Steam players' IDs are derived from it. Keep it, or their stats/admin entries change.
|
||||
[ -s secrets/reunion-salt.txt ] || randpw > secrets/reunion-salt.txt
|
||||
umask 022
|
||||
echo "ok"
|
||||
|
||||
say "Building the Docker image"
|
||||
docker compose build -q
|
||||
echo "ok"
|
||||
|
||||
# the running server holds the engine files open: stop it while they're replaced (it starts again below)
|
||||
if [ "$RUNNING" = running ]; then
|
||||
say "Stopping the server for the update"
|
||||
docker compose stop
|
||||
fi
|
||||
|
||||
if [ ! -x hlds/hlds_linux ] || [ -n "$UPDATE" ]; then
|
||||
say "Downloading Counter-Strike 1.6 server files (SteamCMD, ~600 MB)"
|
||||
# steam_legacy: ReHLDS needs engine build <= 8684. app 90 often fails on the first try, so retry.
|
||||
# validate only on the first download: it would also put back Valve's versions of files you edited
|
||||
VALIDATE=validate; [ -x hlds/hlds_linux ] && VALIDATE=
|
||||
[ -f hlds/cstrike/mapcycle.txt ] && cp hlds/cstrike/mapcycle.txt dl/mapcycle.txt.keep
|
||||
ok=
|
||||
for i in 1 2 3 4; do
|
||||
if docker compose run --rm --no-deps --entrypoint /opt/steamcmd/steamcmd.sh cs \
|
||||
+force_install_dir /cs/hlds +login anonymous +app_set_config 90 mod cstrike \
|
||||
+app_update 90 -beta steam_legacy $VALIDATE +quit && [ -f hlds/cstrike/liblist.gam ]; then
|
||||
ok=1; break
|
||||
fi
|
||||
echo "SteamCMD attempt $i didn't finish, trying again..."
|
||||
done
|
||||
[ -n "$ok" ] || die "game download failed: run ./setup.sh again"
|
||||
[ -f dl/mapcycle.txt.keep ] && mv dl/mapcycle.txt.keep hlds/cstrike/mapcycle.txt
|
||||
else
|
||||
echo; echo "game files present (./setup.sh --update to update them)"
|
||||
fi
|
||||
|
||||
say "Installing the mod stack"
|
||||
docker compose run --rm --no-deps -e CS_MODE="$(getenv CS_MODE)" \
|
||||
-v "$PWD:/cs/repo:ro" -v "$PWD/dl:/cs/repo/dl" \
|
||||
--entrypoint /bin/sh cs /cs/repo/tools/install-addons.sh
|
||||
|
||||
say "Starting"
|
||||
docker compose up -d --force-recreate
|
||||
printf 'waiting for the server to answer'
|
||||
i=0
|
||||
until python3 tools/hlq.py info 127.0.0.1 "$PORT" >/dev/null 2>&1; do
|
||||
i=$((i+1)); [ $i -lt 40 ] || { echo; die "no answer after 2 min: look at 'docker logs --tail 50 $NAME'"; }
|
||||
printf '.'; sleep 3
|
||||
done
|
||||
echo " up"
|
||||
python3 tools/hlq.py info 127.0.0.1 "$PORT" || true
|
||||
|
||||
say "Done"
|
||||
IPS=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -v ':' | grep -v '^172\.' | head -3 | tr '\n' ' ')
|
||||
echo "Join from Counter-Strike 1.6 (console, ~ key):"
|
||||
for ip in $IPS; do echo " connect $ip:$PORT"; done
|
||||
echo "From the internet: forward UDP $PORT on your router to this machine, then connect <your public IP>:$PORT"
|
||||
echo "No port forwarding possible? A free playit.gg UDP tunnel to 127.0.0.1:$PORT works too (README)."
|
||||
echo
|
||||
echo "Control menu: ./cs (status, map, mode, bots, kill counter, restart, ...)"
|
||||
echo "Console: ./rcon status ./rcon changelevel de_inferno"
|
||||
echo "Auto-care: ./scripts/install-cron.sh (watchdog + nightly backup)"
|
||||
echo "Docs: README.md, docs/ADMIN.md, docs/PLAYERS.md"
|
||||
Reference in new issue
Block a user