assembleDebug, testDebugUnitTest, and recordPaparazziDebug all pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016bThmkmyUUdqQpy3MXFFe5
Deploying Bookshelf's PocketBase server
This assumes a spare always-on machine at home (a mini PC, NUC, Raspberry Pi, or an old laptop) running Linux. Pick one of the two run methods below — systemd or Docker — not both.
0. Get the files onto the server
Copy the whole server/ directory (minus .dev-credentials, which is
gitignored and dev-only) to the target machine, e.g.:
rsync -av --exclude .dev-credentials ~/bookshelf/server/ youruser@homeserver:/opt/bookshelf/
The pocketbase binary is architecture-specific — if your home server isn't
the same CPU architecture as wherever you built/downloaded it, grab the
matching build from https://github.com/pocketbase/pocketbase/releases
(this project is pinned to v0.40.2) and drop it in as /opt/bookshelf/pocketbase.
1. Run it — Option A: systemd (recommended for a bare-metal/VM host)
- Create a dedicated unprivileged user:
sudo useradd --system --home /opt/bookshelf --shell /usr/sbin/nologin bookshelf sudo chown -R bookshelf:bookshelf /opt/bookshelf - Install the unit:
sudo cp deploy/bookshelf.service /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable --now bookshelf - Check it's up:
systemctl status bookshelfandcurl http://127.0.0.1:8090/api/health. - Logs:
journalctl -u bookshelf -f.
Note the unit binds PocketBase to 127.0.0.1:8090 only — it is not
reachable from other machines yet. That's intentional; see step 4.
2. Run it — Option B: Docker
cd deploy
docker compose up -d --build
docker compose logs -f
This also only publishes to 127.0.0.1:8090 on the host, for the same
reason. Data persists in the bookshelf_pb_data named volume regardless of
container restarts/rebuilds.
3. Provision the schema and accounts
Once the server is up and answering on 127.0.0.1:8090, run the setup
scripts from server/ (one directory up from here):
cd /opt/bookshelf
# If this is a brand-new PocketBase data dir, it prints a one-time setup URL
# on first launch (see `journalctl -u bookshelf` or `docker compose logs`) —
# open that in a browser first to create your superuser account.
./setup-schema.sh http://127.0.0.1:8090 <superuser-email> <superuser-password>
# Then create the household accounts — there is no public sign-up:
./create-user.sh owner@example.com "a strong password" "Owner Name"
./create-user.sh spouse@example.com "a different strong password" "Spouse Name"
setup-schema.sh is idempotent — safe to re-run any time (e.g. after
pulling an updated server/ if the schema ever changes).
4. Reach it from outside your home network
The app needs a URL it can hit from anywhere your phone goes — not just your home wifi. Two real options:
Tailscale (recommended)
Install Tailscale on the home server and on your phone, join both to the
same tailnet. The server gets a stable 100.x.y.z address (or a
MagicDNS name like homeserver.your-tailnet.ts.net) reachable from
anywhere, encrypted end-to-end, with no ports opened on your router.
- Pros: essentially zero attack surface (nothing is exposed to the public internet at all — not even a login page), no TLS cert management, works behind CGNAT, five-minute setup.
- Cons: both devices need the Tailscale app installed and signed in; if Tailscale's coordination service has an outage, new connections may be briefly unable to establish (existing ones keep working) — acceptable for a two-person home library, not for something needing five-nines.
- Point the app's server-URL field at
http://100.x.y.z:8090(Tailscale's encryption makes plain HTTP tolerable inside the tailnet, but see the HTTPS note below — usinghttps://via Tailscale Serve, next, is easy enough to just do). - Even better: use
tailscale serveto get automatic HTTPS with a real cert on your tailnet domain, so the app can just always usehttps://:sudo tailscale serve --bg 8090
Port-forward + Caddy (reverse proxy with TLS)
Forward a port on your router to the home server, and run Caddy in front of PocketBase to terminate TLS with an automatic Let's Encrypt certificate.
- Pros: works with any client, no extra app/agent needed on the phone, a real public HTTPS URL.
- Cons: you're now running an internet-facing service from your home — bugs in PocketBase, Caddy, or your router's firmware are now a real attack surface; you need a domain name (or dynamic-DNS if your ISP gives you a changing IP) for Let's Encrypt's HTTP-01/TLS-ALPN-01 challenge to work; residential ISPs sometimes block inbound 80/443 or use CGNAT, which breaks this approach entirely (Tailscale sidesteps that).
Minimal Caddyfile:
bookshelf.yourdomain.com {
reverse_proxy 127.0.0.1:8090
}
Caddy handles the certificate automatically. Point your router's port
forward at the Caddy host's 443, and the app's server-URL field at
https://bookshelf.yourdomain.com.
HTTPS is not optional
The app sends the account password on every login. Never point the app
at a plain http:// URL that crosses the public internet or an untrusted
network — only plain HTTP over Tailscale (which is already
end-to-end-encrypted at the network layer) is acceptable, and even there,
prefer tailscale serve for a real cert. If you go the port-forward route,
Caddy above gets you HTTPS for free — don't skip it.
5. Back up regularly
./deploy/backup.sh
See the comments at the top of backup.sh for what it does (SQLite-safe
snapshot + uploaded covers, tarball, prune old ones) and the restore
procedure. Wire it into cron:
crontab -e
# add:
0 3 * * * /opt/bookshelf/deploy/backup.sh >> /var/log/bookshelf-backup.log 2>&1
Consider also copying the resulting tarballs off-box (another machine, a USB drive, cloud storage) — a backup that lives on the same disk as the data it's backing up doesn't protect against disk failure.