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 namedpalace-<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-homesevery 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.
| Piece | Where 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.
| File | What it contains |
|---|---|
mediaserverurl.txt | The public URL players get — usually the HTTPS address served by nginx. |
internalmediaserverurl.txt | The 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/
- 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.
--reverseproxymediais the switch that tellspserver, “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
- Finds every palace on the box. With
--scan-homesit walks/home/*/palace/looking formediaserverurl.txt. (Alternative:--palaces-dir PARENT/if all your palaces live under a single non-home folder.) - Skips anything not ready. Both
mediaserverurl.txtandinternalmediaserverurl.txtmust exist and be non-empty. A palace that has never started is ignored safely. - Generates the nginx site file, for example
/etc/nginx/sites-enabled/03-media.thepalace.app.conf, with onelocation /<sha1>/block per palace that proxies to that palace’s internal port. - 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, orboth— the default).--edge-scheme: what nginx itself serves.httpsis normal production (with a redirect from port 80).httpis for labs without TLS.dualserves both, which you only need if some clients insist onhttp://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.
- Point DNS at the server. Create an
Arecord (andAAAAif you use IPv6) for your media hostname pointing at this VPS. - Install nginx and Certbot:
If you are missingsudo apt install nginx certbot python3-certbot-nginx/etc/letsencrypt/ssl-dhparams.pem, create it once:sudo openssl dhparam -out /etc/letsencrypt/ssl-dhparams.pem 2048 - 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-dyou pass (so typically/etc/letsencrypt/live/thepalace.app/).
- Option A — one cert per hostname:
- Tell the generator where the cert lives. If your PEMs are under
…/live/media.thepalace.app/, add--cert-dirto 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 - Do things in the right order. Start
pserverfirst (with-Hand--reverseproxymedia) so thatmediaserverurl.txtactually exists. Then rungen-media-nginx.sh --scan-homes --reload. Ifnginx -tfails, fix the cert path or install the missing PEMs before retrying. - Open the firewall. Allow inbound TCP 443 (HTTPS) and 80 (HTTP → HTTPS redirect + Let’s Encrypt renewals). Your
-Hport does not need to be public; it only needs to be reachable by nginx, usually on127.0.0.1. - 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:
Confirm withsudo 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.shsudo 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-diryou pass togen-media-nginx.sh. - Each
pserveris started with-Hand--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
-Hport 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_hashon 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/palacehostpass | What 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-noforkon thepservercommand line so it stays in the foreground under systemd.WorkingDirectory=pointing at the palace’s data folder (the productionExecStartuses absolute paths anyway, but this keeps relative lookups sane).Restart=on-failurewith a modestRestartSec=so a crashed palace comes back on its own.After=network-online.targetso 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:
You don’t need to unpack this tarball by hand. The update-install-pserver.sh script (see downloads below) does three things for you:
- Downloads the latest tarball and expands it into a template directory —
/root/palace-templateby default. - Copies the new
pserverbinary to/usr/local/bin/pserverso it’s on everyone’sPATH. - Optionally, with
--restartall, restarts everypalace-*.serviceon 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:
- Run the script without
--restartallfirst — this refreshes the template and/usr/local/bin/pserver, but leaves all running palaces alone. - Manually restart one test palace (
sudo systemctl restart palace-<testuser>.service) and verify that it comes up cleanly and players can still connect. - Happy with the test? Run the script again with
--restartallto roll the new binary out to the rest.
| What you want to do | Command |
|---|---|
| Refresh the template and install the new binary | sudo ./update-install-pserver.sh |
| Do the above and restart every palace unit | sudo ./update-install-pserver.sh --restartall (your palace data under /home/*/palace/ is untouched) |
Put the template somewhere other than /root | PALACE_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.
host-provision-demo.sh
Creates unprivileged user, palace dir, systemd unit, logrotate, cron; use --from-template after the update script.
password_bcrypt_hash.py
Python 3 — prints a bcrypt hash for /etc/palacehostpass, --hostpass, or prefs fields. Requires pip install bcrypt. See Host passwords.
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 starterpserver.patout of/root/palace-templateinto the new palace:
The directory key is fetched automatically from the keyserver the first timesudo ./host-provision-demo.sh --user mypalace --from-template \ --tcp-port 9998 --http-port 8081pserverstarts — there’s no key file for you to drop in. - Option B — manual files: run the script without
--from-template, then copypserver.patandmedia/into the new palace folder yourself. Make surepserveris onPATHor setPSERVER_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:
sudo ./update-install-pserver.sh— download the latest bundle and install/usr/local/bin/pserver.sudo ./host-provision-demo.sh --user mypalace --from-template …— create the user, files, unit, and logrotate.- Edit
/home/mypalace/palace/pserver.patif you need to change the palace name, admin password, etc. 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-cronso you don’t end up with duplicate cron entries.
| Environment variable | What it controls |
|---|---|
PSERVER_BIN | Where the pserver binary lives (default /usr/local/bin/pserver). |
GEN_MEDIA_NGINX | Path to the gen-media-nginx.sh helper. |
PALACE_TEMPLATE_DIR | Template folder used by --from-template (default /root/palace-template). |
PALACE_CRON_SCHEDULE | How 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
| Topic | What to know |
|---|---|
| Many palaces on one server | Stick 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 proxy | Omit --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. |
--bind | Picks the IP address TCP and -H bind to. This also changes what the server advertises to the public directory. |
| Directory key | Each 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 hardening | The 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 cron | If 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
| Symptom | Where 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:
sudo ./update-install-pserver.sh— refresh the template and install the new/usr/local/bin/pserver.- 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 withsystemctl restart palace-<user>.service. See the recommended rollout above. - 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. - If you ever hand-edit anything in
/etc/nginx/, runsudo nginx -tbefore reloading. One important rule: never hand-edit the generated03-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
serversarray — each entry has a name, a picture, apalace_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
--provideron the server). In the JSON that label is exposed asyphost_provider— so on your site, filter the list down to rows whoseyphost_providermatches your own provider string.
If you just want the human-friendly view, the live directory UI is at directory.thepalace.app.