Files
chess/doc/deployment-linux.md
T

9.8 KiB

Deployment: Linux with systemd and PostgreSQL

A native install on Debian, Ubuntu, Arch, or another systemd distribution that already runs PostgreSQL. The service is chessd, running as a system account chess without a login shell; it reaches PostgreSQL over the local Unix socket with peer authentication, so no database password exists. A reverse proxy (nginx) on the same host, if any, forwards /chess/api/ to it. The FreeBSD jail layout is in deployment.md; database maintenance is in database.md.

browser ──TLS──▶ nginx ──HTTP 127.0.0.1:8080──▶ chessd (chess-server, user chess)
                                                    │ Unix socket, peer auth
                                                    ▼
                                             PostgreSQL 17+ (database chess)

Prerequisites

PostgreSQL 17 or later (18 recommended), running, with its default local socket; Stockfish; systemd; Go 1.27 to build.

Debian / Ubuntu. Ubuntu 24.04 ships PostgreSQL 16, so 18 comes from the PostgreSQL project's repository (PGDG). With 18 already installed, check the cluster with pg_lsclusters and skip this.

sudo apt install postgresql-common stockfish
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh
sudo apt install postgresql-18        # creates and starts cluster 18/main

The default pg_hba.conf already maps local OS accounts to roles of the same name (local all all peer). Stockfish installs as /usr/games/stockfish; the service adds /usr/games to its PATH.

Arch. The package does not create a cluster; initialize one the first time only:

sudo pacman -S postgresql stockfish
sudo -u postgres initdb -D /var/lib/postgres/data --encoding=UTF8 \
    --locale-provider=builtin --builtin-locale=C.UTF-8 \
    --auth-local=peer --auth-host=scram-sha-256
sudo systemctl enable --now postgresql

A cluster made by a plain initdb trusts every local connection. The chess account connects either way, but peer is what you want: without it, any local account can connect as any role.

Existing clusters. Other databases, roles, and applications on the server are left alone: the setup creates one role and one database named chess, changes no server settings, and edits pg_hba.conf only when asked (PG_HBA=yes, below). It does not change listen_addresses; the service itself only uses the socket.

Install

make server                                         # bin/chess-server, static
sudo CHESS_BINARY=bin/chess-server TRUSTED_PROXIES=127.0.0.1 deploy/linux/setup.sh
sudo chess-db user add -username <name>             # prompts for the password

The script is idempotent: rerun it with a new binary to upgrade. It stops chessd, installs everything, migrates the schema, starts the service, and fails unless /health reports "storage":"ok".

Input Default Meaning
CHESS_BINARY required chess-server binary to install
TRUSTED_PROXIES kept from chessd.env Proxy address(es) whose X-Real-IP is trusted; 127.0.0.1 for nginx on this host
CHESSD_HOST, CHESSD_PORT 127.0.0.1, 8080 Listen address, first install only
SPLIT_PRIVILEGES no yes: the service role only reads and writes rows (database.md)
ENABLE_BACKUP no yes: nightly pg_dump through a systemd timer
PG_HBA no yes: add local chess chess peer to pg_hba.conf if the chess account cannot connect

What it installs:

Path Purpose
account chess System account, no login shell, home /var/lib/chessd
/usr/local/bin/chess-server The server, root:root 0755; the previous copy kept as .prev
/usr/local/sbin/chess-db Runs chess-server db ... as chess against the database: chess-db user list, chess-db verify
/etc/chessd/chessd.env Settings, root:chess 0640; written on the first install only
/var/lib/chessd/jwt.key JWT signing key, chess 0600, generated once; sessions survive restarts
/etc/systemd/system/chessd.service The service unit, replaced on every run
chess-backup.service, .timer, /usr/local/sbin/chess-backup With ENABLE_BACKUP=yes: dumps to backups/ in the postgres home, 14 days kept

PostgreSQL steps, in order: check the server version (17+) and socket as postgres; create role, database, and schema with setup.sql when neither exists (a half-present pair stops the script); check that the chess account can connect.

When the chess account cannot connect

The first matching line of pg_hba.conf decides. A cluster whose local lines require a password (scram-sha-256, md5) refuses the passwordless chess role, and the script stops with the file's path and this line to add above the other local lines:

local   chess   chess   peer

Then reload (sudo systemctl reload postgresql) and rerun, or rerun with PG_HBA=yes to have the line inserted and PostgreSQL reloaded (the previous file is kept next to it with a timestamp).

Settings

/etc/chessd/chessd.env is read by the unit; restart after a change (sudo systemctl restart chessd).

Setting First-install value Purpose
CHESSD_HOST, CHESSD_PORT 127.0.0.1, 8080 Listen address; keep loopback when nginx runs on the host
CHESSD_DSN "host=<socket dir> dbname=chess" Database over the socket as the OS account
CHESSD_JWT_KEY /var/lib/chessd/jwt.key Stable signing key
CHESSD_TRUSTED_PROXIES TRUSTED_PROXIES Comma-separated proxy addresses
CHESSD_FLAGS empty Other server flags, e.g. -max-users 500 -db-cleanup report

Server flags worth knowing: -max-users (registration cap, default 100), -anonymous-game-ttl (default 24h), -finished-game-ttl (default 1h), -db-cleanup off|report|delete (the integrity sweep, see database.md), -log-level, -log-http.

Changes to the unit belong in a drop-in (sudo systemctl edit chessd), which the script never overwrites.

Operation

systemctl status chessd
journalctl -u chessd -f                       # server log
sudo chess-db user add -username <name>       # accounts; see database.md
sudo chess-db verify                          # check stored games; run after upgrades
curl -s http://127.0.0.1:8080/health          # "storage":"ok"
systemctl list-timers chess-backup.timer      # with ENABLE_BACKUP=yes

Upgrade: build, then rerun setup.sh with the new CHESS_BINARY. Schema migrations run before the service starts.

Rollback: until you delete it, the previous binary is kept:

sudo systemctl stop chessd
sudo cp /usr/local/bin/chess-server.prev /usr/local/bin/chess-server
sudo systemctl start chessd

A newer schema is not downgraded: a binary older than the database refuses to start (see the log), so keep a dump from before the upgrade.

Uninstall:

sudo systemctl disable --now chessd chess-backup.timer
sudo rm -f /etc/systemd/system/chessd.service /etc/systemd/system/chess-backup.{service,timer} \
    /usr/local/bin/chess-server{,.prev} /usr/local/sbin/chess-{db,backup}
sudo systemctl daemon-reload
sudo rm -rf /etc/chessd /var/lib/chessd
sudo -u postgres psql -X -c 'DROP DATABASE chess WITH (FORCE)' -c 'DROP ROLE IF EXISTS chess_owner' -c 'DROP ROLE chess'
sudo userdel chess

nginx on the Same Host

Use deploy/nginx-chess.conf with <server-address>:<port> = 127.0.0.1:8080, and install with TRUSTED_PROXIES=127.0.0.1, so the server keys rate limits on the client address nginx sends in X-Real-IP. The browser clients and their publishing steps are the same as on FreeBSD (deployment.md).

Service Sandbox

The unit runs the server with systemd's sandboxing: the whole file system is read-only except /var/lib/chessd, home directories and /tmp are hidden, no capabilities or privilege escalation, only Unix and IP sockets, and the @system-service system-call set. Stockfish runs inside the same sandbox. systemd-analyze security chessd summarizes the result.

Running as Your Own Account (Development)

Without a chess account, the server runs as you, with a database owned by your PostgreSQL role; tables then live in that database's public schema, which is fine for development:

sudo -u postgres createuser --createdb "$USER"    # once, if you have no role yet
createdb chess_dev
bin/chess-server db init -dsn "dbname=chess_dev"
bin/chess-server -dev -dsn "dbname=chess_dev" -serve   # API :8080, web client :9090
bin/chess-server db user add -username me -dsn "dbname=chess_dev"

-dev relaxes rate limits and uses a fixed JWT key; never use it in production. The test suites need a separate, disposable database whose tables they drop (test/README.md):

createdb chess_test
export CHESS_TEST_DSN="dbname=chess_test"
go test ./...
test/run-test-server.sh bin/chess-server     # then test/test-*.sh in another terminal

Verification Checklist

systemctl is-active chessd                                  # active
curl -s http://127.0.0.1:8080/health                        # "storage":"ok"
ps -o user= -C chess-server                                 # chess
sudo -u postgres psql -X -d chess -c '\dt chess.*'          # five tables
sudo chess-db verify                                        # 0 problem(s)
sudo -u chess psql -X -d chess -Atc 'SHOW search_path'     # chess

The scripts were exercised on Ubuntu 24.04 with PostgreSQL 18.6 and systemctl replaced by a stand-in that starts the unit's ExecStart as its User with its EnvironmentFile: a first install that stops at pg_hba.conf, the same with PG_HBA=yes, an idempotent rerun, an upgrade, a trusted-proxy change, backups, and a fresh split-privilege install. systemd-analyze verify accepts the units.