#!/bin/sh # ql-lg-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 Quake Live game files from Steam # Steps: check tools -> .env -> RCON password -> build image -> download QL (SteamCMD, ~1 GB) # -> build minqlx -> 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 MINQLX_REPO=https://github.com/MinoMino/minqlx.git MINQLX_COMMIT=fbdd915 # v0.5.3-15, the version this repo was tested with 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" | 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}" } 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 the health checks)" echo "ok" say "Settings (.env)" if [ ! -f .env ]; then cp .env.example .env setenv QL_HOSTNAME "$(ask 'Server name' "$(getenv QL_HOSTNAME)")" l=$(ask 'List it in the in-game server browser? (yes/no)' yes) case "$l" in y|Y|yes|YES) setenv QL_LISTED yes ;; *) setenv QL_LISTED no ;; esac setenv QL_PASSWORD "$(ask 'Join password (empty = open server)' '')" setenv QL_PORT "$(ask 'Game port (UDP)' "$(getenv QL_PORT)")" echo "created .env (edit it any time, then: docker compose up -d)" else echo "using existing .env" fi setenv QL_UID "$(id -u)" setenv QL_GID "$(id -g)" PORT=$(getenv QL_PORT); RPORT=$(getenv QL_RCON_PORT); NAME=$(getenv QL_NAME) [ "$PORT" != "$RPORT" ] || die "QL_PORT and QL_RCON_PORT must differ" 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 QL_PORT in .env" ! ss -ltn | grep -q ":$RPORT " || die "TCP port $RPORT is already in use: set another QL_RCON_PORT in .env" fi echo "server '$(getenv QL_HOSTNAME)', container $NAME, UDP $PORT, listed: $(getenv QL_LISTED)" say "RCON password" mkdir -p secrets server steamcmd-home logs minqlx-build chmod 700 secrets if [ ! -s secrets/rcon-password.txt ]; then (umask 077; head -c 64 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | head -c 24 > secrets/rcon-password.txt) echo "generated secrets/rcon-password.txt (you never need to type it: ./rcon uses it)" else echo "keeping secrets/rcon-password.txt" fi chmod +x rcon tools/*.sh scripts/*.sh say "Building the Docker image" docker compose build -q if [ ! -x server/qzeroded.x64 ] || [ -n "$UPDATE" ]; then say "Downloading Quake Live dedicated server (SteamCMD app 349090, ~1 GB)" docker compose run --rm --no-deps --entrypoint /opt/steamcmd/steamcmd.sh ql \ +@sSteamCmdForcePlatformType linux +force_install_dir /ql/server \ +login anonymous +app_update 349090 validate +quit [ -x server/qzeroded.x64 ] || die "download failed (server/qzeroded.x64 missing): run ./setup.sh again" else echo "game files present (./setup.sh --update to update them)" fi if [ ! -f minqlx-build/minqlx.x64.so ]; then say "Building minqlx ($MINQLX_COMMIT)" docker run --rm -v "$PWD/minqlx-build:/out" debian:bookworm-slim sh -c " apt-get update -qq && apt-get install -y -qq build-essential python3-dev git ca-certificates >/dev/null && git clone -q $MINQLX_REPO /src && cd /src && git checkout -q $MINQLX_COMMIT && make >/dev/null && cp bin/minqlx.x64.so bin/minqlx.zip bin/run_server_x64_minqlx.sh /out/ && chown -R $(id -u):$(id -g) /out" [ -f minqlx-build/minqlx.x64.so ] || die "minqlx build failed" fi cp minqlx-build/minqlx.x64.so minqlx-build/minqlx.zip minqlx-build/run_server_x64_minqlx.sh server/ chmod +x server/run_server_x64_minqlx.sh echo "minqlx installed into server/" say "Starting" docker compose up -d printf 'waiting for the server to answer' i=0 until python3 tools/qlq.py 127.0.0.1 "$PORT" >/dev/null 2>&1; do i=$((i+1)); [ $i -lt 30 ] || { echo; die "no answer after 90 s: look at 'docker logs --tail 50 $NAME'"; } printf '.'; sleep 3 done echo " up" python3 tools/qlq.py 127.0.0.1 "$PORT" --status || true say "Done" IPS=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -v ':' | grep -v '^172\.' | head -3 | tr '\n' ' ') echo "Join from Quake Live (console, ~ key):" for ip in $IPS; do echo " /connect $ip:$PORT"; done if [ "$(getenv QL_LISTED)" = yes ]; then echo "From the internet: forward UDP $PORT to this machine, then /connect :$PORT" echo "(listed servers show up in the in-game browser after a few minutes)" fi echo echo "Console: ./rcon status ./rcon addbot hunter 5 ./rcon kick allbots" echo "Auto-restart: ./scripts/install-cron.sh (watchdog, nightly backup, daily restart)" echo "Docs: README.md, docs/ADMIN.md, docs/PLAYERS.md"