#!/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 :$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"