/posts/drafts/install-pocketbase-digital-ocean

How to Install PocketBase on DigitalOcean

TL;DR

A single Go binary handles your database, API, admin UI, and TLS termination. No reverse proxy, no dependency manager, no daemon supervisor beyond systemd. Full setup: droplet, binary transfer, systemd service, DNS, SQLite backup via rclone or Litestream.

7 min updated

Copy one file to a $4 DigitalOcean droplet, write a systemd unit, point DNS at it. Hit the URL. HTTPS is already working -- certificate and all -- with no nginx, no Caddy, no certbot cron. The whole thing takes about 15 minutes.

That one file is PocketBase: a single Go binary containing a SQLite database, a REST and realtime API, an admin dashboard, and an HTTPS server with automatic Let's Encrypt provisioning. No PostgreSQL, no package manager, no reverse proxy. The only decision you need to make upfront is your backup strategy, because PocketBase stores everything in a single pb_data/ directory and that directory is the entire state of your application.


A $4 Droplet Is Sufficient for Most Workloads

A 512 MB droplet running Ubuntu 22.04 or 24.04 handles PocketBase without breaking a sweat. There is no separate database server eating memory -- SQLite is embedded in the binary, single-file, single-process. The OS gets its share, PocketBase gets the rest, and nothing fights over RAM. Write serialization is what limits SQLite at scale, not read throughput, but on a small droplet serving a small API you will hit neither.

Spin up a droplet with these settings:

  • Image: Ubuntu 22.04 or 24.04 LTS (x86_64)
  • Plan: Basic shared CPU, $4/month (1 vCPU, 512 MB RAM, 10 GB SSD)
  • Authentication: SSH key (add yours during creation)

Once it boots, SSH in as root:

ssh root@YOUR_DROPLET_IP

Add the droplet's IP to your local ~/.ssh/config as a named host. You will SSH in repeatedly during setup and later during upgrades.

wget and chmod Are the Full Install

There is no apt install, no PPA, no Docker image required. PocketBase releases are zip files at github.com/pocketbase/pocketbase/releases. Download the linux_amd64.zip directly on the server:

mkdir -p /root/pb

# Replace X.Y.Z with the current version from the releases page
wget https://github.com/pocketbase/pocketbase/releases/download/vX.Y.Z/pocketbase_X.Y.Z_linux_amd64.zip

unzip pocketbase_X.Y.Z_linux_amd64.zip -d /root/pb/
chmod +x /root/pb/pocketbase

If you downloaded the zip locally first, unzip it and rsync the binary up:

# On your local machine after unzipping
rsync -avz -e ssh ./pocketbase root@YOUR_DROPLET_IP:/root/pb/

Verify the binary runs:

/root/pb/pocketbase --version

It should print the version string and exit. If it prints nothing or errors, the binary did not transfer correctly -- redownload and re-extract.

The systemd Unit Does the Heavy Lifting

Without a process supervisor, PocketBase dies with the SSH session and does not restart on crash or reboot. systemd handles both. Create the unit file:

vi /lib/systemd/system/pocketbase.service

Paste this content, replacing yourdomain.com with your actual domain or subdomain:

[Unit]
Description=PocketBase Service
After=network.target

[Service]
Type=simple
User=root
Group=root
LimitNOFILE=4096
Restart=always
RestartSec=5s
StandardOutput=append:/root/pb/errors.log
StandardError=append:/root/pb/errors.log
ExecStart=/root/pb/pocketbase serve --http="yourdomain.com:80" --https="yourdomain.com:443"

[Install]
WantedBy=multi-user.target

Enable and start it:

systemctl enable pocketbase.service --now
systemctl status pocketbase.service

The ExecStart line uses your domain name, not 0.0.0.0. PocketBase uses the hostname in --http and --https to request a Let's Encrypt certificate via ACME HTTP-01 challenge. If you pass an IP address instead of a domain, TLS provisioning silently fails and the service falls back to HTTP only.

The LimitNOFILE=4096 line raises the open file descriptor limit. SQLite holds the database file open and PocketBase opens additional files for uploads, logs, and the admin UI. The default system limit (1024 on older Ubuntu images) causes intermittent failures under modest load -- 4096 avoids this.

Restart=always with RestartSec=5s means systemd restarts PocketBase 5 seconds after any crash. If PocketBase crashes immediately on restart (misconfiguration, port conflict), systemd enters a restart loop. Check /root/pb/errors.log before assuming a hardware or network problem.

SQLite Backup Has One Correct Tool

Run cp data.db data.db.bak while PocketBase is running. Restore from it later. Your data is corrupted.

SQLite is single-writer, and a raw file copy does not coordinate with the write-ahead log. A write that lands mid-copy produces a backup file that SQLite cannot open. This is the most common way people lose PocketBase data.

Everything lives in /root/pb/pb_data/:

  • data.db -- the main SQLite database
  • storage/ -- uploaded files and assets
  • logs.db -- request and event logs (safe to exclude from backups)
  • types.d.ts -- generated TypeScript types (regenerable, lowest priority)

Two tools back this up correctly.

rclone + SQLite .backup pragma (simple, push to cloud storage):

apt install rclone -y

# Configure a remote: DigitalOcean Spaces, S3, Backblaze B2, etc.
rclone config

# Safe database dump using the SQLite backup API
sqlite3 /root/pb/pb_data/data.db ".backup '/tmp/data.db.bak'"

# Sync the safe copy and the rest of pb_data (excluding live logs)
cp /tmp/data.db.bak /root/pb/pb_data/data.db.bak
rclone sync /root/pb/pb_data/ remote:your-bucket/pb_data/ --exclude "*.log" --exclude "data.db"
rclone copyto /tmp/data.db.bak remote:your-bucket/pb_data/data.db

Put this in a nightly cron job (RPO = 24 hours):

crontab -e
# Add:
# 0 3 * * * sqlite3 /root/pb/pb_data/data.db ".backup '/tmp/data.db.bak'" && rclone copyto /tmp/data.db.bak remote:your-bucket/pb_data/data.db && rclone sync /root/pb/pb_data/storage/ remote:your-bucket/pb_data/storage/

Litestream (continuous WAL replication, RPO = seconds):

Litestream streams SQLite write-ahead log frames to object storage in near-real-time. It is the better choice when you cannot tolerate a full day of data loss.

wget https://github.com/benbjohnson/litestream/releases/latest/download/litestream-linux-amd64.tar.gz
tar -xzf litestream-linux-amd64.tar.gz -C /usr/local/bin/

Configure /etc/litestream.yml:

dbs:
  - path: /root/pb/pb_data/data.db
    replicas:
      - url: s3://your-bucket/data.db

Run Litestream as a systemd service alongside PocketBase. It wraps the PocketBase process and intercepts WAL commits before they are checkpointed.

Never back up a live SQLite database by copying the file directly. The .backup pragma is safe because it uses the SQLite backup API, which coordinates with the write-ahead log. A raw cp data.db does not.

certmagic Handles TLS -- No Reverse Proxy Required

Start PocketBase with --https="yourdomain.com:443". Open the URL in a browser. The padlock is there -- valid certificate, no warnings, no certbot, no cron renewal. You did not configure any of that.

Here is what happened behind the scenes: PocketBase uses the certmagic library (the same one Caddy uses) to:

  1. Request a certificate from Let's Encrypt via HTTP-01 challenge on port 80
  2. Store the issued certificate in pb_data/
  3. Serve HTTPS on port 443 and auto-renew before expiry

PocketBase must bind ports 80 and 443. On Ubuntu, non-root processes cannot bind ports below 1024 by default. Running as root (as the unit file above does) sidesteps this. To run as a non-root user instead, grant the capability:

setcap cap_net_bind_service=+ep /root/pb/pocketbase

UFW firewall rules:

ufw allow 22/tcp    # SSH -- do this first or you lock yourself out
ufw allow 80/tcp    # HTTP (required for ACME challenge)
ufw allow 443/tcp   # HTTPS
ufw enable

Run ufw allow 22/tcp before ufw enable. Enabling UFW without an SSH rule drops your current connection and locks you out. DigitalOcean's console gives recovery access, but it is a frustrating detour.

DNS: add an A record pointing your domain or subdomain to the droplet IP. Let's Encrypt's HTTP-01 challenge requires the domain to resolve to the server before PocketBase can obtain a certificate. If DNS has not propagated when PocketBase first starts, certificate issuance fails and the service falls back to HTTP. Restarting after propagation triggers a fresh ACME attempt.

SQLite Limits Where Postgres Wins

Push 50 concurrent writes at PocketBase. Some will fail. SQLite serializes writes -- one at a time, full stop. This is where the single-binary tradeoff bites.

AxisPocketBase (binary)nginx + Postgres app
Setup time~15 minutes30-90 minutes
DependenciesNonenginx, Postgres, app runtime
TLS managementAutomatic (certmagic)certbot or Caddy needed
Write concurrencySingle writer (SQLite)Multi-writer (Postgres)
Data portabilitySingle directory copypg_dump / restore
Horizontal scalingNot possible (SQLite)Possible with connection pooling
Custom business logicJS/Go hooks, limitedFull application code
Upgrade pathReplace binaryCoordinated deploy

PocketBase is the wrong tool when you need concurrent writes at scale, complex transactions across multiple tables, or full-text search with relevance ranking. It is the right tool for prototypes, internal tools, small APIs, and projects where one person manages the server.

Upgrading Is One Command

No migration scripts, no dependency dance, no deploy pipeline. PocketBase upgrades are binary replacements -- stop, swap, restart:

systemctl stop pocketbase.service

wget https://github.com/pocketbase/pocketbase/releases/download/vX.Y.Z/pocketbase_X.Y.Z_linux_amd64.zip -P /tmp/
unzip /tmp/pocketbase_X.Y.Z_linux_amd64.zip -d /tmp/pb-new/
chmod +x /tmp/pb-new/pocketbase

mv /root/pb/pocketbase /root/pb/pocketbase.bak
mv /tmp/pb-new/pocketbase /root/pb/pocketbase

systemctl start pocketbase.service
systemctl status pocketbase.service

Check logs after restart:

tail -f /root/pb/errors.log

PocketBase runs automatic schema migrations on startup. Keep pocketbase.bak until you confirm the new version is healthy -- rolling back is another binary swap.