Linux hosting guide

A practical guide for running pserver on a Debian or Ubuntu VPS. It covers where files go on disk, how to front your media with nginx, how to register services with systemd, how to set optional host passwords, and which helper scripts to drop onto the box. The scripts are built so you can run many palaces on one server—one Linux user per palace, one shared nginx for media—and roll out new pserver builds from a single template rather than copying binaries into every palace by hand.

Overview

This page shows the production layout we recommend. Paths, usernames and ports on your server will look a little different, so read the commands before pasting and adjust the names.

The model in a single paragraph:

  • One Linux user per palace. Each palace’s files live in that user’s home directory at /home/<username>/palace/, and it runs as a systemd service named palace-<username>.service.
  • One nginx in front of all palaces for media (props, artwork, rooms). Players talk to https://media.thepalace.app/<unique-path>/ (or your own hostname), and nginx forwards each path to the right palace’s local HTTP port.
  • One cron job calls gen-media-nginx.sh --scan-homes every couple of minutes, so when you add a new palace its media mapping appears automatically—no manual nginx edits.

If you only ever plan to run a single palace with no TLS, you can skip nginx and the cron job and just expose pserver’s built-in HTTP port directly. Most of this guide is about the multi-palace, HTTPS setup.

Where everything lives

Here’s the map of what a production host looks like. If something in this guide references a file and you’re not sure where it is, this table is the quick answer.

PieceWhere it lives / how it works
The palace itself (TCP + built-in HTTP on -H)A systemd service, one per palace, called palace-<username>.service.
Palace data (rooms, prefs, media, logs)/home/<username>/palace/ — owned by that user.
The shared pserver binary/usr/local/bin/pserver, installed by the update script.
Shared template for new palaces/root/palace-template/, refreshed by the update script.
Nginx media config (auto-generated)Something like /etc/nginx/sites-enabled/03-media.thepalace.app.conf — written by gen-media-nginx.sh.
Cron that keeps nginx in sync/etc/cron.d/palace-media-scan-homes, running gen-media-nginx.sh --scan-homes --reload.
Log rotation/etc/logrotate.d/palace-<username>, sending SIGHUP to the service after each rotation.
Optional host-password file/etc/palacehostpass (fixed path; see Host passwords).

The sample systemd unit under Script downloads (palace-testpalace.service) is a good reference — it shows a typical User=, WorkingDirectory=, and ExecStart including --reverseproxymedia.

How pserver advertises media URLs

When pserver starts, it writes two small text files next to pserver.pat. They tell you (and the nginx helper script) where media is published. You do not normally edit them by hand.

FileWhat it contains
mediaserverurl.txtThe public URL players get — usually the HTTPS address served by nginx.
internalmediaserverurl.txtThe private URL — the pserver built-in HTTP port, which only nginx talks to.

When you launch with --reverseproxymedia https://media.thepalace.app, pserver publishes a URL that looks like https://media.thepalace.app/<directorykey-sha1>/. The SHA-1 part is unique to each palace and is what lets a single media hostname serve many palaces. The underlying directory key is fetched automatically from the keyserver on first start — you don’t have to create or place any key file by hand. gen-media-nginx.sh reads mediaserverurl.txt from each palace and wires the right SHA-1 path to the right internal HTTP port.

If you don’t use --reverseproxymedia, mediaserverurl.txt simply points at pserver’s own HTTP port directly — which is fine for a lab, but not what most operators want in production.

A typical pair of files after startup looks like this:

mediaserverurl.txt          → https://media.thepalace.app/02F18C0E83632F1EC692746D4F2C5F3AC290CDE3/
internalmediaserverurl.txt  → http://203.0.113.10:8080/
Why bother with nginx and a reverse proxy?
  • One hostname, many palaces. With nginx in front, every palace you host shares the same media domain (for example media.thepalace.app). You do not need to buy a new subdomain or get a new TLS certificate for each palace — each one just gets its own unique path under the shared URL.
  • HTTPS for all your players. nginx handles TLS at the edge, so props and artwork load over an encrypted connection. Without nginx each palace would be serving plain HTTP on its own port.
  • --reverseproxymedia is the switch that tells pserver, “advertise this public HTTPS address to players instead of my internal port.” Pair it with nginx and you get a clean, multi-tenant media front.

The nginx helper: gen-media-nginx.sh

This script is what builds your shared nginx media config from the URL files each pserver writes. It ships inside the Palace server tarball under scripts/. Copy it once to /usr/local/bin/gen-media-nginx.sh and chmod +x it.

What it does, step by step

  1. Finds every palace on the box. With --scan-homes it walks /home/*/palace/ looking for mediaserverurl.txt. (Alternative: --palaces-dir PARENT/ if all your palaces live under a single non-home folder.)
  2. Skips anything not ready. Both mediaserverurl.txt and internalmediaserverurl.txt must exist and be non-empty. A palace that has never started is ignored safely.
  3. Generates the nginx site file, for example /etc/nginx/sites-enabled/03-media.thepalace.app.conf, with one location /<sha1>/ block per palace that proxies to that palace’s internal port.
  4. Validates before reloading. It runs nginx -t. If that fails, the bad file is removed and the script exits non-zero — so a broken palace can’t take your whole media host down.

Two flags control how strict it is about URLs:

  • --match-scheme: which schemes in the txt files are accepted (http, https, or both — the default).
  • --edge-scheme: what nginx itself serves. https is normal production (with a redirect from port 80). http is for labs without TLS. dual serves both, which you only need if some clients insist on http://media….
# See what it would write, without changing anything:
sudo gen-media-nginx.sh --scan-homes --dry-run

# Actually write the config and reload nginx:
sudo gen-media-nginx.sh --scan-homes --reload

# Uncommon layout (palaces not under /home):
# sudo gen-media-nginx.sh --palaces-dir /srv/palace --dry-run

Typical cron job (this is what the provisioning script drops in for you):

*/2 * * * * root /usr/local/bin/gen-media-nginx.sh --scan-homes --match-scheme both --edge-scheme https --reload

A single static palace can run the script manually once after TLS is set up and never touch it again. Any host running multiple palaces — or where new palaces get added over time — should keep the cron (or an equivalent systemd timer).

TLS with Let’s Encrypt & nginx

nginx terminates HTTPS on your media hostname (for example media.thepalace.app) using standard Let’s Encrypt certificates. The generator script just needs to know where the certificate files live — it doesn’t issue them. The steps below get a fresh VPS to a working HTTPS setup.

  1. Point DNS at the server. Create an A record (and AAAA if you use IPv6) for your media hostname pointing at this VPS.
  2. Install nginx and Certbot:
    sudo apt install nginx certbot python3-certbot-nginx
    If you are missing /etc/letsencrypt/ssl-dhparams.pem, create it once:
    sudo openssl dhparam -out /etc/letsencrypt/ssl-dhparams.pem 2048
  3. Issue the certificate. Pick one of:
    • Option A — one cert per hostname: sudo certbot --nginx -d media.thepalace.app. The files will land under /etc/letsencrypt/live/media.thepalace.app/.
    • Option B — one cert covering several hostnames: sudo certbot certonly --nginx -d thepalace.app -d www.thepalace.app -d media.thepalace.app. The folder is named after the first -d you pass (so typically /etc/letsencrypt/live/thepalace.app/).
  4. Tell the generator where the cert lives. If your PEMs are under …/live/media.thepalace.app/, add --cert-dir to every invocation of the generator (including the cron entry):
    sudo gen-media-nginx.sh --scan-homes --media-host media.thepalace.app \
      --cert-dir /etc/letsencrypt/live/media.thepalace.app --reload
  5. Do things in the right order. Start pserver first (with -H and --reverseproxymedia) so that mediaserverurl.txt actually exists. Then run gen-media-nginx.sh --scan-homes --reload. If nginx -t fails, fix the cert path or install the missing PEMs before retrying.
  6. Open the firewall. Allow inbound TCP 443 (HTTPS) and 80 (HTTP → HTTPS redirect + Let’s Encrypt renewals). Your -H port does not need to be public; it only needs to be reachable by nginx, usually on 127.0.0.1.
  7. Make renewals reload nginx automatically. Certbot already renews the cert on its own timer. Drop in a deploy hook so nginx picks up the new cert:
    sudo sh -c 'printf "%s\n" "#!/bin/sh" "systemctl reload nginx" > /etc/letsencrypt/renewal-hooks/deploy/nginx-reload.sh'
    sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/nginx-reload.sh
    Confirm with sudo certbot renew --dry-run.

Reverse proxy checklist

A quick sanity-check list before you go live. The goal is that one public URL — for example https://media.thepalace.app/ — serves every palace on this host, each under its own path.

For the why behind this layout, see Why bother with nginx and a reverse proxy.

  • nginx and Certbot are installed, and the cert path matches the --cert-dir you pass to gen-media-nginx.sh.
  • Each pserver is started with -H and --reverseproxymedia https://<your-media-host>.
  • The generator has been run (or cron is running it) after any time you add, remove or restart a palace.
  • Firewall allows 443 and 80 inbound; internal -H port is reachable by nginx.

One thing to watch: the operator /admin UI is served on the internal URL from internalmediaserverurl.txt, not through the public media.thepalace.app vhost. That’s by design — admin traffic shouldn’t go through the player-facing proxy.

Host passwords (/etc/palacehostpass)

A host password is the secret an operator types in the Palace client to become a full server admin (“host rank”). On Linux, pserver checks for a file at a fixed location — /etc/palacehostpass — and will use any entries it finds there. The file is optional; without it, only --hostpass on the command line or HOSTPASSWORD_HASH inside pserver.pat are used.

Blank lines and # comments are ignored. Each real line is one of two shapes:

  • A bcrypt hash on its own (it will start with $2a$, $2b$, or $2y$). This is a shared secret: anyone who knows the matching plaintext password can become a host.
  • username:bcrypt_hash on a single line. Each operator has their own password; the hash still starts with $2….

The file must be readable by the Linux user pserver runs as (the User= in the systemd unit). The safe pattern is chmod 640 /etc/palacehostpass and chgrp it to that user.

Generating hashes with password_bcrypt_hash.py

You never put plaintext passwords in /etc/palacehostpass — only bcrypt hashes. The helper script password_bcrypt_hash.py in the downloads section generates those hashes for you. It needs Python 3 and the bcrypt package (it uses cost factor 10, which matches the server).

pip install bcrypt   # or pip3 install bcrypt
python3 password_bcrypt_hash.py 'YourPlainPassword'

The script prints a short banner and then one line starting with $2. That single $2… line is the hash you store in the file.

# Shared secret: append just the hash line to the file
python3 password_bcrypt_hash.py 'SharedSecret' | grep '^\$2' | sudo tee -a /etc/palacehostpass

# Named entry for user "alice": put "alice:" in front of the hash on one line
printf 'alice:' | sudo tee -a /etc/palacehostpass
python3 password_bcrypt_hash.py 'HerSecret' | grep '^\$2' | sudo tee -a /etc/palacehostpass

You can mix shared and named lines in the same file. The file is added to any hashes already set via --hostpass or HOSTPASSWORD_HASH. On Linux, pserver picks up changes to /etc/palacehostpass automatically — no restart needed to add or remove entries.

Notes: the file path is fixed at /etc/palacehostpass — there is no /etc/hostpass. The provisioning scripts assume Debian/Ubuntu-style adduser semantics.

What to type in the client (~susr)

In the Palace client, operators run the ~susr command to become a host. What they type after ~susr depends on which kind of line is in the file:

Line in /etc/palacehostpassWhat the operator types after ~susr
Hash only (shared secret)Just the plaintext password. No username: prefix.
username:hash (per-operator)username:password. If the password itself contains colons, only the first colon splits the name from the secret.

Becoming a host gives operator-level powers inside the server. See the operator command reference for the full set of ~ commands that host rank unlocks.

Log rotation

Always launch pserver with -l /path/to/pserver.log so it writes to a real file you can rotate. After rotating you have to tell pserver to reopen the log file; the standard way is to send it SIGHUP. (That same signal also makes pserver re-read pserver.pat; if nothing in that file changed, the reload is a no-op, so it’s safe to send on every rotation.)

A minimal /etc/logrotate.d/palace-testpalace for the user testpalace:

/home/testpalace/palace/pserver.log {
    su testpalace testpalace
    daily
    maxsize 500M
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    create 0644 testpalace testpalace
    sharedscripts
    postrotate
        systemctl kill -s HUP palace-testpalace.service >/dev/null 2>&1 || true
    endscript
}

Tip: copytruncate works without a signal, but it has a small race window where log lines can be lost. The postrotate + SIGHUP pattern above is cleaner.

systemd checklist

Each palace runs as its own systemd service named palace-<username>.service. The provisioning script writes a working unit for you, but these are the settings to double-check if you ever hand-edit one:

  • Type=simple, and pass -nofork on the pserver command line so it stays in the foreground under systemd.
  • WorkingDirectory= pointing at the palace’s data folder (the production ExecStart uses absolute paths anyway, but this keeps relative lookups sane).
  • Restart=on-failure with a modest RestartSec= so a crashed palace comes back on its own.
  • After=network-online.target so the service starts after the network is actually up, not just configured.

After editing a unit file:

sudo systemctl daemon-reload
sudo systemctl enable --now palace-<username>.service
systemctl status palace-<username>.service
journalctl -u palace-<username>.service -f   # live logs

Official bundle & in-place upgrades

We publish a single Linux amd64 tarball that contains the current pserver binary, the ratbot binary, bundled media/, a sample pserver.pat, and helper scripts. You can download it directly:

latest-linux-amd64.tar.gz

You don’t need to unpack this tarball by hand. The update-install-pserver.sh script (see downloads below) does three things for you:

  1. Downloads the latest tarball and expands it into a template directory — /root/palace-template by default.
  2. Copies the new pserver binary to /usr/local/bin/pserver so it’s on everyone’s PATH.
  3. Optionally, with --restartall, restarts every palace-*.service on the box so they all pick up the new binary.

Recommended rollout. We ship server features often, so most operators run an upgrade about once a month. A safe pattern is:

  1. Run the script without --restartall first — this refreshes the template and /usr/local/bin/pserver, but leaves all running palaces alone.
  2. Manually restart one test palace (sudo systemctl restart palace-<testuser>.service) and verify that it comes up cleanly and players can still connect.
  3. Happy with the test? Run the script again with --restartall to roll the new binary out to the rest.
What you want to doCommand
Refresh the template and install the new binarysudo ./update-install-pserver.sh
Do the above and restart every palace unitsudo ./update-install-pserver.sh --restartall (your palace data under /home/*/palace/ is untouched)
Put the template somewhere other than /rootPALACE_TEMPLATE_DIR=/srv/palace-template sudo ./update-install-pserver.sh
Preview without changing anything--dry-run

When you later run host-provision-demo.sh --from-template to set up a new palace, it copies just what each palace needs out of the template: the pserver binary (into PSERVER_BIN), the bundled media/, ratbot if present, and a starter pserver.pat (only if the palace doesn’t already have one — pass --force-template-pat to overwrite). It deliberately does not copy the scripts/ folder into every palace; those stay in the template root. If --from-template complains, make sure rsync is installed.

Script downloads

Grab these and drop them into /root (or wherever you manage admin scripts). Shell scripts need to be made executable with chmod +x before you run them. All of them are the same helpers that ship inside the Palace server tarball.

update-install-pserver.sh

Downloads the latest linux-amd64 bundle into /root/palace-template, installs the shared binary, optional --restartall.

Download

host-provision-demo.sh

Creates unprivileged user, palace dir, systemd unit, logrotate, cron; use --from-template after the update script.

Download

password_bcrypt_hash.py

Python 3 — prints a bcrypt hash for /etc/palacehostpass, --hostpass, or prefs fields. Requires pip install bcrypt. See Host passwords.

Download

palace-testpalace.service

Example systemd unit for reference (adjust user, ports, paths).

Download

Provisioning a new palace (host-provision-demo.sh)

This is the script that sets up a new palace on the box: it creates the Linux user, the palace data directory, the systemd unit, the logrotate rule, and (once, for the first palace) the cron job for the nginx regenerator. There are two ways to use it:

  • Option A — from the shared template (recommended; you’ve already run update-install-pserver.sh). The script will copy the binary, media/, ratbot, and a starter pserver.pat out of /root/palace-template into the new palace:
    sudo ./host-provision-demo.sh --user mypalace --from-template \
      --tcp-port 9998 --http-port 8081
    The directory key is fetched automatically from the keyserver the first time pserver starts — there’s no key file for you to drop in.
  • Option B — manual files: run the script without --from-template, then copy pserver.pat and media/ into the new palace folder yourself. Make sure pserver is on PATH or set PSERVER_BIN.

The script writes the systemd unit and runs systemctl daemon-reload, but it deliberately does not start the service — that way you never accidentally boot a palace before its config is ready.

A full first-time setup, end to end, looks like this:

  1. sudo ./update-install-pserver.sh  — download the latest bundle and install /usr/local/bin/pserver.
  2. sudo ./host-provision-demo.sh --user mypalace --from-template …  — create the user, files, unit, and logrotate.
  3. Edit /home/mypalace/palace/pserver.pat if you need to change the palace name, admin password, etc.
  4. sudo systemctl enable --now palace-mypalace.service  — start it and make it boot on reboot.

Run ./host-provision-demo.sh --help (or --help-all) to see every flag. Useful ones: --verbosity, --match-scheme, --edge-scheme, --lab (for a no-TLS test setup), and explicit path overrides.

What it creates on the system:

  • The Linux user (via Debian adduser) if it doesn’t already exist.
  • /home/<user>/palace/ — the palace’s own data directory.
  • /etc/systemd/system/palace-<user>.service — the systemd unit.
  • /etc/logrotate.d/palace-<user> — log rotation for this palace.
  • /etc/cron.d/palace-media-scan-homes — the shared cron that keeps nginx in sync (unless you pass --no-cron). The first palace on a host normally installs it; subsequent palaces should pass --no-cron so you don’t end up with duplicate cron entries.
Environment variableWhat it controls
PSERVER_BINWhere the pserver binary lives (default /usr/local/bin/pserver).
GEN_MEDIA_NGINXPath to the gen-media-nginx.sh helper.
PALACE_TEMPLATE_DIRTemplate folder used by --from-template (default /root/palace-template).
PALACE_CRON_SCHEDULEHow often the nginx regen cron runs (default */2 * * * *, every two minutes).

Don’t confuse two similarly-named flags. The provisioning script’s --data-dir points at one palace’s working directory (e.g. /home/mypalace/palace). The generator’s --palaces-dir points at a parent folder that contains one subdirectory per palace — a layout most hosts don’t use, since --scan-homes handles the standard /home/*/palace/ pattern.

Adding a second (or tenth) palace on the same host: rerun the script with a new --user and unique ports, and pass --no-cron so you don’t duplicate the global cron job.

Advanced options & troubleshooting

Advanced options

TopicWhat to know
Many palaces on one serverStick to the standard pattern: one Linux user per palace, data under /home/<user>/palace/, unique -p (TCP) and -H (HTTP) ports per service. They can all share the same --reverseproxymedia URL — the per-palace SHA-1 path keeps them separated.
Running without a reverse proxyOmit --reverseproxymedia. mediaserverurl.txt will just point at the -H port directly, and the generator will only accept URLs whose scheme matches --match-scheme and whose host matches --media-host.
--bindPicks the IP address TCP and -H bind to. This also changes what the server advertises to the public directory.
Directory keyEach palace needs a directory key to register in the public directory and to produce the SHA-1 segment in its media URLs. pserver requests one from the keyserver automatically on first start and caches it — no manual key management required.
systemd hardeningThe sample units include options like ProtectSystem and ReadWritePaths. These are safe defaults; tweak them if your palace writes to non-default paths.
systemd timer instead of cronIf you prefer journald-centric ops, replace /etc/cron.d/palace-media-scan-homes with a systemd.timer that runs the same gen-media-nginx.sh --scan-homes --reload command.

Troubleshooting

SymptomWhere to look first
mediaserverurl.txt is empty or missing.Make sure -H is set in the ExecStart. If you’re using --reverseproxymedia, confirm the host can reach the keyserver on first start so pserver can obtain its directory key — without one, it has no SHA-1 path to advertise.
The nginx generator skips one of your palaces.Run it with --dry-run and read the SKIP lines on stderr. Common causes: one of the two URL files is empty, the scheme in mediaserverurl.txt doesn’t match --match-scheme, or the host doesn’t match --media-host.
nginx -t fails after the generator runs.Almost always a cert-path mismatch. The script deletes its bad output so nginx itself stays healthy — fix the PEM path (see TLS) and re-run.
Log file keeps growing after rotation.Your postrotate block must send SIGHUP to the exact palace-<user>.service that owns the file. A typo in the unit name means pserver never reopens the log and keeps writing to the rotated-away inode.
~susr in the client keeps failing.Check three things: (1) every bcrypt line in /etc/palacehostpass starts with $2a$, $2b$, or $2y$; (2) the file is readable by the Linux user in User=; (3) if you used --hostpass on the command line, it expects a hash, not a plaintext password.

Ongoing maintenance

Once a host is set up, the day-to-day loop is small. A typical monthly refresh:

  1. sudo ./update-install-pserver.sh  — refresh the template and install the new /usr/local/bin/pserver.
  2. Restart one test palace and confirm it comes back clean. Then either run the script again with --restartall, or restart the rest one by one with systemctl restart palace-<user>.service. See the recommended rollout above.
  3. After adding or removing a palace, run sudo gen-media-nginx.sh --scan-homes --reload — or just wait a couple of minutes for the cron job to do it.
  4. If you ever hand-edit anything in /etc/nginx/, run sudo nginx -t before reloading. One important rule: never hand-edit the generated 03-media… file. Fix the inputs (mediaserverurl.txt, flags to the generator, cert paths) and re-run the script.

This page is designed to stand on its own — every edge case referenced above is covered in the sections earlier on this page.

Listing palaces from the public directory

Running palaces automatically register with the public directory on a schedule, which is how regular users discover them. If you’d like to feature palaces on your own website — either the whole directory or just the palaces you host — you can pull the same data as a JSON feed and render it however you like.

  • The feed: https://directory.thepalace.app/index.json. It’s a JSON document with a servers array — each entry has a name, a picture, a palace_url (under _directory), and more. The HTTP response includes cache hints; please honor them rather than hammering the endpoint.
  • Show only the palaces you host: every palace that registers sends along a provider label (the same string you set with --provider on the server). In the JSON that label is exposed as yphost_provider — so on your site, filter the list down to rows whose yphost_provider matches your own provider string.

If you just want the human-friendly view, the live directory UI is at directory.thepalace.app.