10 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", "version" = the build
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. /health reports the
running build as version (git describe of the source); check it matches,
and upgrade the server before or with a web client served from elsewhere.
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.