Is PocketBase right for your app?
Yes, if your app is small or mid-sized and one server is enough. The project's FAQ names SaaS products, mobile app backends and intranets as good fits; the only way to grow is a bigger server, never more of them. Your frontend talks to it through the official JavaScript or Dart SDK, and the dashboard at /_/ is where you manage collections and their API rules.
Look elsewhere if you need PostgreSQL, several app servers sharing one database, or a company standing behind the software: the FAQ says it runs on SQLite only and calls PocketBase a personal project, built by volunteers, without a business behind it. For a Postgres-based stack, see self-hosted Supabase or self-hosted Appwrite.
Before v1.0.0, PocketBase does not promise full backward compatibility. Its introduction advises against production-critical use unless you are happy to read the changelog and sometimes apply manual migration steps. Build that reading into every upgrade.
How big a server you need
Smaller than you might think. There is no official minimum, but the FAQ reports PocketBase holding more than 10,000 live connections on a modest virtual machine with two vCPUs and 4 GB. On a real app, these fill up first:
- Disk. pb_data holds the two database files, every file your users upload and, unless you point them elsewhere, the dashboard's backup ZIPs. Leave room for one extra full copy.
- Memory. The v0.40.6 changelog calls image thumbnail generation one of the main causes of potential out-of-memory errors, so image-heavy apps want spare memory.
- Open files. Each network connection uses a file descriptor; the docs mention a usual default of around 1,024. Their example service sets 4096, but systemd applies that one number as the ceiling too, so PocketBase could never open more and would stop well short of the FAQ's 10,000 connections. Our service sets 65536 instead.
The server that fits
A compact cartridge is usually plenty: the HPE m710x, a Xeon E3-1585L v5 cartridge on NVMe, runs one job well and includes the out-of-band console, the screen and power button you can reach even when a firewall rule shuts SSH out. If you store many uploads, an everyday single-processor rack server gives you more disk, again with a console. Browse both on the dedicated server page, where each card lists the memory, disks and cities.
- A ready server is handed over within 30 minutes of approval; a server built to your specification takes hours rather than minutes.
- You manage the server; we keep the hardware and the network running.
- Nobody meters the port, so file downloads and live updates never turn into a bandwidth bill.
- This guide uses the x86_64 build. PocketBase also publishes a linux_arm64 build for Ampere Altra machines; the X-Gene cartridge only runs Ubuntu 16.04 and 18.04, so skip it here.
Up and running in an evening
Downloads now come from the developer's own host: the README says the repository has moved to ganigeorgiev.com, and the docs link 0.41.1 there. Point your domain's DNS at the server first. Then paste these three steps in order.
- Install the program. Step 1 switches on the firewall with only SSH allowed, adds a
pocketbaseuser that cannot log in, and installs the file owned by root, so the running service cannot overwrite itself. It refuses to install unless the download matches the SHA-256 in the release'schecksums.txt. - Make your admin, then go live. Step 2 creates your superuser with a random password, saves it in a file only root can read, writes the systemd service and only then opens ports 80 and 443.
- Log in and tidy up. Open
https://your-domain/_/, change the password, and delete/root/pb-admin.txt.
# Paste as root on Ubuntu 24.04 (x86_64). You can paste it again later to upgrade.
PBV=0.41.1; PBD=/opt/pocketbase; TMP=$(mktemp -d)
SRC=https://ganigeorgiev.com/meta/pocketbase/pocketbase/download/v$PBV
ZIP=pocketbase_${PBV}_linux_amd64.zip
if systemctl is-active --quiet pocketbase; then echo "STOP - run systemctl stop pocketbase first"; false; fi &&
sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw && ufw allow OpenSSH && ufw --force enable &&
apt-get update && apt-get install -y unzip curl &&
{ getent passwd pocketbase >/dev/null || useradd -r -d "$PBD" -s /usr/sbin/nologin pocketbase; } &&
install -d -m 755 "$PBD" && (cd "$PBD" &&
install -d -m 750 -o pocketbase -g pocketbase pb_data pb_migrations pb_hooks pb_public) &&
cd "$TMP" && curl -fsSLO "$SRC/$ZIP" && curl -fsSLO "$SRC/checksums.txt" &&
grep " $ZIP\$" checksums.txt | sha256sum -c - && unzip -q "$ZIP" pocketbase &&
install -m 755 -o root -g root pocketbase "$PBD/pocketbase" && "$PBD/pocketbase" --version &&
echo "STEP 1 OK" || echo "STOP - step 1 did not finish, read the message above"
# Paste as root. Put your own domain and e-mail in the first line before you paste.
SITE=api.example.com; ME=you@example.com; PBD=/opt/pocketbase; CRED=/root/pb-admin.txt
if [ "$SITE" = api.example.com ] || [ "$ME" = you@example.com ]; then echo "STOP - edit SITE and ME"; false; fi &&
cd "$PBD" && { test -f /etc/pocketbase.env || install -m 600 /dev/null /etc/pocketbase.env; } &&
install -m 600 /dev/null "$CRED" && PW=$(openssl rand -base64 24) && [ -n "$PW" ] &&
sudo -u pocketbase "$PBD/pocketbase" superuser upsert "$ME" "$PW" --dir="$PBD/pb_data" &&
printf 'email %s\npassword %s\n' "$ME" "$PW" > "$CRED" && unset PW &&
cat > /etc/systemd/system/pocketbase.service <<EOF &&
[Unit]
Description=PocketBase
After=network-online.target
Wants=network-online.target
[Service]
User=pocketbase
Group=pocketbase
WorkingDirectory=$PBD
LimitNOFILE=65536
EnvironmentFile=-/etc/pocketbase.env
AmbientCapabilities=CAP_NET_BIND_SERVICE
Restart=always
RestartSec=5s
ExecStart=$PBD/pocketbase serve $SITE --dir=$PBD/pb_data --encryptionEnv=PB_ENCRYPTION_KEY
[Install]
WantedBy=multi-user.target
EOF
ufw allow 80,443/tcp && systemctl daemon-reload && systemctl enable --now pocketbase && sleep 5 &&
systemctl is-active --quiet pocketbase && echo "LIVE - your login is in $CRED" ||
echo "STOP - not running yet, check journalctl -u pocketbase"
Why the admin comes first: on a first start with no superuser, PocketBase writes a setup link with a 30-minute token into its log, according to apis/installer.go. Creating the account beforehand means that link never exists. For a moment the password appears as a command-line argument, which anyone else logged in to the box could spot in ps: one more reason to change it once you log in. If a step stops halfway, fix the cause and paste it again; each part is safe to repeat.
Our changes to the example service in the docs are: a dedicated user instead of root, the CAP_NET_BIND_SERVICE capability on the service (the docs use setcap on the file, which a new binary would lose), the journal instead of a std.log file, an explicit --dir, waiting for the network, a 65536 open-file limit where the example has 4096, and a private settings file, /etc/pocketbase.env (step 2 creates it empty), read together with --encryptionEnv=PB_ENCRYPTION_KEY. Neither changes anything until you write a line into that file. Given a domain, serve takes ports 80 and 443, requests the certificate from Let's Encrypt and sends plain HTTP visitors to HTTPS (cmd/serve.go). Sharing the server with other sites? Start PocketBase without a domain so it listens only on 127.0.0.1:8090, put Caddy or nginx before it as a reverse proxy, and fill in the "User IP proxy headers" setting.
Settings worth changing on day one
- Mail. Out of the box PocketBase sends through the local sendmail command, and the docs warn those messages will probably land in spam. Enter an SMTP provider's details in Settings > Mail settings before users start resetting passwords.
- Rate limiter (v0.23.0 and later) in Settings > Application, to slow down abusive sign-in or create requests.
- Superuser IPs (v0.38.0 and later): a stolen admin token gets a 403 from anywhere else. Locked out after your address changed? Stop PocketBase, then as the
pocketbaseuser runsuperuser ipswith your new address and--dir=/opt/pocketbase/pb_data; giving no address clears the list. - Two-step login for superusers (MFA with one-time codes); if mail is down,
superuser otpmakes a code by hand. - Allowed origins.
serveaccepts API calls from any website (--originsdefaults to*); limit it to your own frontend if nothing else should call it. - A memory ceiling. On a small server or with big uploads, the docs suggest GOMEMLIMIT, a soft limit the Go runtime tries to stay under. Put a line like
GOMEMLIMIT=512MiB, chosen to fit your memory, into the private settings file from step 2, then runsystemctl restart pocketbase. - Encrypted settings, so SMTP and S3 passwords are not kept as plain JSON. PocketBase wants a key of exactly 32 characters;
openssl rand -hex 16gives you one of that length. Add it to the same settings file as aPB_ENCRYPTION_KEY=line, runsystemctl restart pocketbase, then open any settings page in the dashboard and press Save once: PocketBase encrypts its settings the next time it writes them (core/settings_model.go), not at startup. Only ever add one such line, once: a second, different key would leave settings saved under the first one unreadable. Save a copy of the file somewhere other than this server and apart from the nightly archives, which leave it out on purpose:core/settings_query.goshows PocketBase refuses to start when it cannot decrypt its settings. After that, thepocketbaseuser cannot read the key on its own, so run command-line tools from root with the key passed along, for exampleset -a; . /etc/pocketbase.env; set +a; sudo --preserve-env=PB_ENCRYPTION_KEY -u pocketbase /opt/pocketbase/pocketbase superuser ips 203.0.113.7 --encryptionEnv=PB_ENCRYPTION_KEY --dir=/opt/pocketbase/pb_data(with your own address in place of 203.0.113.7). Use that form for the Superuser IPs fix above too.
Nightly copies you can restore
The official advice is simple: a copy of pb_data taken while PocketBase is stopped is a full backup. Step 3 does exactly that every night, and the pause lasts as long as the archive takes to write. It always restarts the service, checks that the archive can be read, and removes archives more than two weeks old only once a fresh one has passed that check.
# Paste as root once. It installs a nightly copy job and runs it straight away.
install -m 700 /dev/null /usr/local/sbin/pocketbase-copy.sh && cat > /usr/local/sbin/pocketbase-copy.sh <<'EOF'
set -o pipefail
OUT=/var/backups/pocketbase; PBD=/opt/pocketbase
ARC=$OUT/pocketbase-$(date +%F-%H%M%S).tar.gz
install -d -m 700 "$OUT" || exit 1
trap 'systemctl start pocketbase' EXIT
systemctl stop pocketbase || exit 1
if ! tar -czf "$ARC" -C "$PBD" --exclude=pb_data/backups pb_data pb_migrations pb_hooks pb_public \
-C / etc/systemd/system/pocketbase.service; then
rm -f "$ARC"; echo "copy FAILED, nothing pruned"; exit 1
fi
systemctl start pocketbase
tar -tzf "$ARC" pb_data/data.db >/dev/null || { echo "STOP - $ARC cannot be read"; exit 1; }
echo "ok $ARC"
find "$OUT" -name 'pocketbase-*.tar.gz' -mtime +14 -delete
EOF
echo '15 4 * * * root umask 077; bash /usr/local/sbin/pocketbase-copy.sh > /var/log/pocketbase-copy.log 2>&1' \
> /etc/cron.d/pocketbase-copy && bash /usr/local/sbin/pocketbase-copy.sh
The dashboard has its own backups too (Settings > Backups), stored on the server itself or sent to S3-compatible storage, but the docs suggest another method once pb_data passes about 5 GB. Either way, keep archives on a second machine as well: if this disk dies, copies stored on it die with it.
Bringing a copy back, as root:
- Make sure
tar -tzf ARCHIVE pb_data/data.dblists the file, and run the copy job once so today's state is saved as well. - Run
systemctl stop pocketbase. - In
/opt/pocketbase, add.previousto the name of each of the four folders that start withpb_(if.previousfolders remain from an earlier restore, deal with those first). - Run
tar -xzf ARCHIVE -C /opt/pocketbase --wildcards 'pb_*'and thenchown -R pocketbase:pocketbase /opt/pocketbase/pb_*. - Start the service and check your data. To undo, stop it, rename the restored folders with
.failed, give the.previousfolders their old names back, and start it. When all is well, delete the.previousfolders.
Upgrading calmly
Read the changelog entries between your version and the new one. Then run the copy job, stop the service, save the current program with cp -a /opt/pocketbase/pocketbase /opt/pocketbase/pocketbase.prev, change PBV= in step 1, paste it, and start the service again. If the new version misbehaves, copy pocketbase.prev over the new program and bring back the archive you took just before upgrading, because the upgrade may already have changed the database.
PocketBase also has a one-command pocketbase update. It downloads the newest release, replaces the program and, by default, saves a pb_data backup ZIP, but plugins/update/update.go shows it deletes the old program after a successful swap. The manual route keeps that way back.
The licence in plain words
PocketBase is MIT licensed (LICENSE.md). This is how we read it, not legal advice: build paid or closed products on it and change it freely; when you hand out copies of PocketBase, include its copyright line and the MIT permission notice with them. Nothing obliges you to share your own code.
Questions people ask
- Do I need Docker to run PocketBase?
- No. PocketBase publishes no official Docker image; the production guide shows an example Dockerfile, but the program runs fine on its own. One file plus a systemd service is the whole installation.
- Can I host PocketBase for free on a serverless platform?
- It does not suit one. Everything lives in a folder on local disk, so PocketBase needs a machine whose disk survives restarts. The project offers no hosting of its own; its FAQ sends you to a server with lasting storage.
- Where does PocketBase keep my data?
- In the pb_data folder: two SQLite files, data.db and auxiliary.db, a storage subfolder with every upload, and a backups subfolder for the dashboard's ZIPs (unless those go to S3). Your hooks and migrations sit beside it, which is why the nightly copy takes those folders too.
- How many users can one PocketBase server handle?
- There is no official number for users. The FAQ reports over 10,000 live realtime connections on that small two-vCPU machine; your own limit depends on how much each user reads, writes and uploads. Since the only way up is a larger server, start with room to spare.