Use case · Application backend

PocketBase self hosted: one binary, one machine, done properly

A self-hosted PocketBase is one Go executable that carries its own SQLite database, auth, realtime subscriptions, file storage and admin dashboard, so the whole deploy is: download it, check the checksum, run it under systemd. Everything it stores lives in one directory, pb_data. This page installs version 0.41.1 on Ubuntu 24.04 under a no-login service user, creates the first superuser from the command line before any port is open, lets PocketBase fetch its own Let's Encrypt certificate, and covers backups, upgrades and the limits of a single-server design.

See the machines

What PocketBase is, and what it is not

PocketBase is a backend in a single file. The project describes it as an embedded SQLite database with realtime subscriptions, built-in auth, a dashboard and a REST-style API, usable as a standalone application or as a Go framework. The prebuilt executable also runs JavaScript hooks from pb_hooks and writes JS migrations into pb_migrations.

PocketBase ships no official container image; the production guide gives a sample Dockerfile, but its main path is the bare binary, and so is this page: no container runtime between the process and the disk.

The introduction states that full backward compatibility is not guaranteed before v1.0.0, and that PocketBase is not yet recommended for production-critical applications unless you are fine with reading the changelog and applying manual migration steps from time to time.

Sizing the machine

PocketBase publishes no hardware table. Its FAQ states it scales only vertically, on a single server, and reports more than 10,000 persistent realtime connections on a small virtual machine with 2 vCPU and 4 GB of RAM. What actually grows on a real install:

ResourceWhat drives itWhat to do
Diskpb_data holds data.db, auxiliary.db, uploaded files under storage/ and, by default, the dashboard's backup ZIPs under backups/SSD or NVMe, with room for the data plus at least one more full copy
MemoryThe v0.40.6 changelog names thumbnail generation as one of the main culprits for potential out-of-memory errorsHeadroom for image uploads; GOMEMLIMIT as a soft cap
File descriptorsNetwork connections use them, realtime clients included; the docs cite a typical default of about 1,024LimitNOFILE in the unit: 65536 here, not the docs' 4096
CPUOne process serving everythingClock and a few cores; more cores help less than more memory

The last column is our reading, not PocketBase's. In a systemd unit, LimitNOFILE sets both the soft and the hard limit (systemd.exec), so the docs' example value of 4096 would cap the process at 4,096 descriptors, below the FAQ's 10,000-connection figure. The unit below uses 65536.

Which machine to run it on

One process and one data directory is the textbook case for a single-service machine. The m710x, a Moonshot cartridge with a four-core Xeon E3-1585L v5 and NVMe storage, fits most PocketBase apps and keeps an out-of-band console, which matters the first time a ufw rule locks you out. For large upload stores, pick a single-socket rack server with more disk; every racked single-socket server has a full console (iLO, iDRAC or a Supermicro BMC). Each is listed on the dedicated server range with its memory, storage and cities.

  • The commands below use the linux_amd64 build and Ubuntu 24.04. PocketBase also ships linux_arm64, which suits the Ampere Altra line; the X-Gene cartridge, which boots only Ubuntu 16.04 and 18.04, is out of scope here.
  • Racked lines: about 30 minutes from payment approval to hand-over. Built-to-order lines: 4 to 24 hours.
  • The port is unmetered, so file downloads and realtime traffic carry no transfer bill.

If the app needs PostgreSQL or several app servers, PocketBase is the wrong tool: see self-hosted Supabase or Appwrite on your own server.

Installing the binary

The project moved its repository and downloads to ganigeorgiev.com; the GitHub v0.40.6 release notes say it is most likely the last release published there, and the docs now link 0.41.1 from the new host. The block below turns on ufw with SSH only, creates a pocketbase system user with no login shell, and installs the executable root-owned, so the service can write its data but not replace its own binary. It aborts unless the SHA-256 matches checksums.txt from the same release.

# As root on Ubuntu 24.04, x86_64. Safe to re-run; used again for upgrades.
V=0.41.1; D=/srv/pocketbase; W=$(mktemp -d)
U=https://ganigeorgiev.com/meta/pocketbase/pocketbase/download/v$V
Z=pocketbase_${V}_linux_amd64.zip
! systemctl is-active --quiet pocketbase || { echo "STOP - stop pocketbase first"; false; } &&
sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw && ufw allow OpenSSH && ufw --force enable &&
apt-get update && apt-get install -y unzip curl &&
{ id pocketbase >/dev/null 2>&1 || useradd --system --home-dir "$D" --shell /usr/sbin/nologin pocketbase; } &&
install -d -m 755 "$D" &&
install -d -m 750 -o pocketbase -g pocketbase "$D/pb_data" "$D/pb_migrations" "$D/pb_hooks" "$D/pb_public" &&
cd "$W" && curl -fsSLO "$U/$Z" && curl -fsSLO "$U/checksums.txt" &&
grep " $Z\$" checksums.txt | sha256sum -c - &&
unzip -q "$Z" pocketbase && install -m 755 -o root -g root pocketbase "$D/pocketbase" &&
"$D/pocketbase" --version && echo "INSTALL OK" || echo "STOP - install did not finish, read the error above"

Every step is guarded, so a half-run block is fixed by running it again. Nothing listens on the network yet.

First superuser, then systemd and TLS

On first start PocketBase prints an installer link to its log, carrying a token valid for 30 minutes, that creates the first superuser. Creating the superuser from the CLI beforehand means no installer token is ever minted (apis/installer.go only does so while no superuser exists). The block generates a random password, stores it in a mode-600 file, and only then writes the unit and opens 80 and 443.

# As root. Set your own domain and e-mail first; DNS for the domain must already point here.
DOMAIN=pb.example.com; PB_EMAIL=you@example.com; D=/srv/pocketbase; K=/root/pocketbase-superuser.txt
{ [ "$DOMAIN" != pb.example.com ] && [ "$PB_EMAIL" != you@example.com ]; } || { echo "STOP - set DOMAIN and PB_EMAIL"; false; } &&
cd "$D" && { [ -e /etc/pocketbase.env ] || install -m 600 /dev/null /etc/pocketbase.env; } &&
install -m 600 /dev/null "$K" && P=$(openssl rand -base64 24) && [ -n "$P" ] &&
sudo -u pocketbase "$D/pocketbase" superuser upsert "$PB_EMAIL" "$P" --dir="$D/pb_data" &&
printf '%s\n%s\n' "$PB_EMAIL" "$P" > "$K" && unset P &&
printf '%s\n' '[Unit]' 'Description=PocketBase' 'After=network-online.target' 'Wants=network-online.target' \
  '[Service]' 'User=pocketbase' 'Group=pocketbase' "WorkingDirectory=$D" 'LimitNOFILE=65536' \
  'EnvironmentFile=-/etc/pocketbase.env' 'AmbientCapabilities=CAP_NET_BIND_SERVICE' 'Restart=always' \
  'RestartSec=5s' "ExecStart=$D/pocketbase serve $DOMAIN --dir=$D/pb_data --encryptionEnv=PB_ENCRYPTION_KEY" \
  '[Install]' 'WantedBy=multi-user.target' \
  > /etc/systemd/system/pocketbase.service &&
ufw allow 80,443/tcp && systemctl daemon-reload && systemctl enable --now pocketbase &&
sleep 5 && systemctl is-active --quiet pocketbase && echo "RUNNING - login in $K" ||
echo "STOP - not running, see journalctl -u pocketbase"

Our changes to the docs' unit, all of them: a pocketbase user instead of root; AmbientCapabilities=CAP_NET_BIND_SERVICE instead of setcap on the binary, because a capability set on the file is lost when an upgrade replaces it; logs to the journal instead of std.log; an explicit --dir; network-online ordering; LimitNOFILE=65536 instead of 4096; and an optional /etc/pocketbase.env read through EnvironmentFile=- (the dash means the file may be absent) with --encryptionEnv=PB_ENCRYPTION_KEY, which changes nothing until a key is put in that file. The password sits on a command line for a moment, visible to ps on the box; log in at https://your-domain/_/, change it, then delete /root/pocketbase-superuser.txt.

Given a domain, pocketbase serve listens on 0.0.0.0:80 and 0.0.0.0:443, gets the certificate itself and redirects HTTP to HTTPS (cmd/serve.go). To share the box with other sites, drop the domain so it stays on 127.0.0.1:8090, put nginx or Caddy in front, and set the User IP proxy headers in the dashboard.

Hardening the production instance

The production guide marks three settings highly recommended and the rest optional:

  1. SMTP. By default PocketBase sends mail with the local sendmail command, which the docs say will likely land in spam. Set an SMTP service under Settings > Mail settings.
  2. Rate limiter. Built in since v0.23.0, under Settings > Application.
  3. Superuser IPs. Since v0.38.0, a superuser token used from any other address gets a 403. If your address changes, stop the service and run superuser ips with the new address (or none, to clear it) and --dir=/srv/pocketbase/pb_data, as the pocketbase user.
  4. MFA for superusers, with superuser otp as the fallback when mail fails.
  5. GOMEMLIMIT, a soft memory limit the docs suggest on memory-tight machines or with large uploads. The setup block created an empty, root-only /etc/pocketbase.env; add a line such as GOMEMLIMIT=512MiB, sized to your RAM, and run systemctl restart pocketbase.
  6. Settings encryption. SMTP and S3 credentials are otherwise stored as plain JSON in the database. The key is exactly 32 characters: as root, `grep -q '^PB_ENCRYPTION_KEY=' /etc/pocketbase.env && echo "STOP - a key is already set; never replace it" || { k=$(openssl rand -hex 16) && [ ${#k} -eq 32 ] && echo "PB_ENCRYPTION_KEY=$k" >> /etc/pocketbase.env && systemctl restart pocketbase; }. The unit already passes --encryptionEnv. Settings already stored stay plain text until they are next saved (core/settings_query.go reads plain JSON first; core/settings_model.go encrypts on save), so add the key before entering SMTP or S3 credentials, or press Save once in the dashboard's Settings after the restart. Keep a copy of /etc/pocketbase.env off the machine and outside the backup archives (the job does not archive it): per core/settings_query.go, without the key PocketBase cannot read its stored settings and will not start. CLI commands then need the key too, for example (set -a; . /etc/pocketbase.env; runuser -u pocketbase -- /srv/pocketbase/pocketbase superuser ips --dir=/srv/pocketbase/pb_data --encryptionEnv=PB_ENCRYPTION_KEY)`.

serve also defaults --origins (CORS) to *. If only your own frontend calls the API, set it to that origin.

Backups and restores

The docs' rule: copying pb_data is a complete backup, and for transactional safety the application should not be running while you copy it. The dashboard backups (Settings > Backups) take a ZIP of pb_data, excluding local backups and files already in S3, but the docs warn they slow down on a large pb_data (5 GB and up). The job below stops the service for one tar, starts it again even when tar fails, verifies the archive, and prunes archives older than 14 days only after a good one exists.

# As root: the nightly job. It stops PocketBase for the length of one tar.
install -m 700 /dev/null /usr/local/sbin/pb-backup.sh && cat > /usr/local/sbin/pb-backup.sh <<'EOF'
set -o pipefail
B=/var/backups/pocketbase; D=/srv/pocketbase; F=$B/pb-$(date +%F-%H%M%S).tar.gz
install -d -m 700 "$B" || exit 1
trap 'systemctl start pocketbase' EXIT
systemctl stop pocketbase || exit 1
tar -czf "$F" -C "$D" --exclude=pb_data/backups pb_data pb_migrations pb_hooks pb_public \
  -C / etc/systemd/system/pocketbase.service || { rm -f "$F"; echo "backup FAILED, see above"; exit 1; }
systemctl start pocketbase
tar -tzf "$F" pb_data/data.db >/dev/null || { echo "STOP - $F is unreadable"; exit 1; }
echo "ok $F"
find "$B" -name 'pb-*.tar.gz' -mtime +14 -delete
EOF
printf '%s\n' '30 3 * * * root umask 077; bash /usr/local/sbin/pb-backup.sh > /var/log/pb-backup.log 2>&1' \
  > /etc/cron.d/pocketbase-backup && bash /usr/local/sbin/pb-backup.sh

Copy /var/backups/pocketbase to another machine yourself; a backup on the same disk does not survive the disk. To restore, as root:

  1. Check the archive: tar -tzf FILE pb_data/data.db must list the file. Run the backup job once more so the current state is saved too.
  2. Stop the service with systemctl stop pocketbase.
  3. In /srv/pocketbase, refuse if pb_data.previous exists, otherwise move each of pb_data, pb_migrations, pb_hooks and pb_public aside with a .previous suffix.
  4. Extract with tar -xzf FILE -C /srv/pocketbase pb_data pb_migrations pb_hooks pb_public, then chown -R pocketbase:pocketbase those four directories.
  5. Start the service and log in. If it is wrong, stop it, move the restored directories to .failed, move the .previous ones back and start again. Once satisfied, delete the .previous copies so the next restore is not blocked.

Upgrading without surprises

Read every changelog entry between your version and the target first; pre-1.0 releases can need manual migration steps. Then run the backup job, systemctl stop pocketbase, keep the old executable with cp -a /srv/pocketbase/pocketbase /srv/pocketbase/pocketbase.prev, set the new V= in the install block, run it, and systemctl start pocketbase. To roll back, put pocketbase.prev back and restore the archive the backup job wrote just before the upgrade, since the new version may have migrated the database.

The built-in pocketbase update command is the official alternative: it fetches the latest release from the new host, swaps the executable and by default writes a pb_data backup ZIP. Per plugins/update/update.go it deletes the old executable once the swap succeeds, so the manual route above is the one with a way back.

The MIT licence

PocketBase is under the MIT licence (LICENSE.md). Our general reading, not legal advice: you may use, modify and sell software built on it, including closed-source products, provided the copyright notice and the licence text travel with copies of PocketBase itself. There is no duty to publish source.

Questions

How much server does PocketBase need?

There is no official minimum. The FAQ reports more than 10,000 persistent realtime connections on a 2 vCPU, 4 GB virtual machine, and growth is vertical only. Size disk first (database, uploads and backup copies), then memory for image thumbnails.

Can PocketBase run on serverless platforms?

Not as intended. It keeps its database and uploads in a local pb_data directory, so it needs a machine with a persistent disk; the FAQ points to servers with persistent storage, and the project offers no hosted version.

Can I use PostgreSQL or MySQL instead of SQLite?

No. The FAQ says PocketBase uses embedded SQLite in WAL mode with no plans for other databases, and suggests Litestream for replication. If the app needs Postgres, choose a different backend.

Give the backend its own machine

A whole server with root, an unmetered port, and a console on the m710x cartridge and every racked rack server.

See the machines

Related on Server Room

Other use cases