How to Install PocketBase on Ubuntu 24.04 VPS: Single-Binary Backend with SQLite, Auth, Realtime
PocketBase is the fastest way to get a real backend running on a Linux box. It is one open-source Go binary — about 20 MB — that bundles a SQLite database, user authentication, realtime subscriptions, file storage, an admin dashboard and a fully documented REST and JavaScript SDK. No Docker Compose, no Postgres, no Redis, no Kafka, no docker-compose.yml with seventeen services. Download it, run it, point a domain at it.
This tutorial walks you through a production-style install on Ubuntu 24.04 LTS: a dedicated Linux user, a systemd unit, admin UI at /_/, your first collection and auth flow, realtime subscriptions, file uploads, custom JavaScript hooks, Nginx reverse proxy with Let's Encrypt TLS, and automated SQLite backups.
Skip the manual install? A CloudCore Starter VPS has more than enough headroom for PocketBase plus your frontend, and you will be online in under ten minutes.
Table of Contents
What is PocketBase?
PocketBase is an open-source backend-as-a-service written in Go. It gives you, out of a single binary:
- An embedded SQLite database with WAL journaling
- User authentication (email/password, OAuth2 for Google, GitHub, Apple, Discord, GitLab, and more, plus OTP and magic links)
- REST API auto-generated from your collection schema, including filter syntax, sorting, pagination and expand relations
- Realtime subscriptions over Server-Sent Events for live UIs
- File storage on local disk or S3-compatible object storage
- An admin dashboard at
/_/for managing collections, records, users, logs and settings - Official JavaScript, Dart and community SDKs
- An embedded JavaScript VM for extending the server with custom hooks and routes
Why Self-Host PocketBase Instead of Supabase or Firebase?
Supabase and Firebase are powerful but optimised for teams that want a managed cloud. Self-hosting PocketBase gives you something different: radical simplicity.
- One binary, one file. A PocketBase install is literally a binary plus a
pb_datadirectory. Supabase self-hosted is a Docker Compose stack with Postgres, GoTrue, PostgREST, Realtime, Storage, Kong, ImgProxy and Studio — eight moving parts you have to monitor, back up and upgrade independently. - Resource-cheap. PocketBase idles at ~30 MB of RAM. A comparable Supabase stack easily consumes 2 GB before you run your first query. This is why PocketBase is happy on a $5-10/mo VPS while Supabase pushes you toward larger plans.
- No vendor lock-in. Firebase ties you to Google Cloud; your data, auth records and functions live inside their proprietary platform. PocketBase lives on your VPS, your data is a SQLite file you can
scpanywhere, and the admin UI lets you export JSON on demand. - Flat, predictable pricing. Firebase bills per read, per write, per stored byte, per egress byte and per auth action. A PocketBase VPS costs the same whether you serve 100 or 10 million requests a month.
- Privacy and compliance. Your users' emails, password hashes, uploads and telemetry never leave your server. GDPR, HIPAA and data-residency audits become straightforward: point to the machine.
- Offline-friendly development. The whole stack runs locally on your laptop — no emulator quirks, no API key juggling, no staging project to reset.
When PocketBase is the Right Choice
PocketBase fits neatly in the gap between "Firebase for a weekend prototype" and "we run our own Postgres cluster". It is a great pick for:
- Indie SaaS products and micro-apps
- Mobile and Flutter app backends
- Realtime dashboards (chat, collaborative editors, live leaderboards)
- Internal admin tools
- MVPs that need auth, a database and file uploads on day one
Prerequisites
Before starting, make sure you have:
- A VPS running Ubuntu 24.04 LTS with root or
sudoaccess - SSH access to the server
- A domain name (e.g.
api.example.com) with an A record pointing at your VPS IP — required for TLS in Step 11 - 1 GB RAM minimum (2 GB+ recommended once you add your frontend, Nginx and cron jobs)
- 10 GB free disk for the OS, PocketBase binary and initial
pb_data
Recommended Plan: CloudCore Starter>
PocketBase is so lightweight that the CloudCore Starter plan is more than enough for a production deployment:>
- 2 vCPU cores
- 4 GB RAM
- 50 GB NVMe SSD
- Unmetered bandwidth>
This leaves plenty of headroom for Nginx, Certbot, a Node frontend and automated backups on the same box. If you plan to run a separate Node.js API or Next.js app alongside PocketBase, also see our Node.js install guide.
Connect to your server:
ssh root@your-server-ipStep 1: Update System Packages
Bring the base image up to date before installing anything:
sudo apt update && sudo apt upgrade -y
sudo apt install -y unzip curl ca-certificatesIf the kernel was upgraded, reboot once:
sudo rebootReconnect via SSH and continue.
Step 2: Create a Dedicated User and Directory
Running PocketBase as root is a bad habit. Create a system user that owns only the PocketBase tree:
sudo useradd --system --home /opt/pocketbase --shell /usr/sbin/nologin pocketbase
sudo mkdir -p /opt/pocketbase
sudo chown pocketbase:pocketbase /opt/pocketbase--system gives a low UID and no aging policy. --shell /usr/sbin/nologin prevents interactive logins even if the account is compromised.
Step 3: Download the Latest PocketBase Release
PocketBase ships as prebuilt ZIPs on GitHub Releases. Check the latest version at pocketbase.io/docs/ or the releases page and substitute the version string below.
cd /tmp
PB_VERSION="0.22.21"
curl -L -o pocketbase.zip \
"https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/pocketbase_${PB_VERSION}_linux_amd64.zip"
unzip pocketbase.zip
sudo mv pocketbase /opt/pocketbase/pocketbase
sudo chown pocketbase:pocketbase /opt/pocketbase/pocketbase
sudo chmod +x /opt/pocketbase/pocketbaseVerify:
/opt/pocketbase/pocketbase --versionExpected output:
PocketBase v0.22.21ARM64 VPS? Swaplinux_amd64forlinux_arm64in the URL. PocketBase ships both architectures officially.
Step 4: Create the systemd Service
A proper systemd unit makes PocketBase start on boot, restart on crash and stream logs to journalctl.
sudo tee /etc/systemd/system/pocketbase.service > /dev/null <<'EOF' [Unit] Description=PocketBase After=network.target[Service] Type=simple User=pocketbase Group=pocketbase WorkingDirectory=/opt/pocketbase ExecStart=/opt/pocketbase/pocketbase serve --http=127.0.0.1:8090 Restart=always RestartSec=5 LimitNOFILE=4096
Hardening
NoNewPrivileges=true PrivateTmp=true ProtectSystem=full ProtectHome=true ReadWritePaths=/opt/pocketbase
[Install] WantedBy=multi-user.target EOF
A couple of important choices:
--http=127.0.0.1:8090binds PocketBase to localhost. Nginx will terminate TLS in Step 11 and proxy to this port. Never expose 8090 directly to the public internet.- The
Protect*directives sandbox the process so a compromised PocketBase binary cannot read/home, write outside/opt/pocketbase, or gain new privileges.
sudo systemctl daemon-reload
sudo systemctl enable --now pocketbase
sudo systemctl status pocketbaseExpected Active: line:
Active: active (running) since Thu 2026-04-16 10:12:03 UTC; 3s agoStream logs with:
sudo journalctl -u pocketbase -fStep 5: Access the Admin UI at /\_/
PocketBase's admin dashboard lives at /_/ on the same port as the API. Since we bound to localhost, you have two options to reach it before Nginx is in place:
Option A — SSH tunnel (recommended for initial setup):
From your laptop:
ssh -L 8090:127.0.0.1:8090 root@your-server-ipThen visit http://127.0.0.1:8090/_/ in your browser.
Option B — Temporarily bind to all interfaces:
Edit the service (ExecStart=/opt/pocketbase/pocketbase serve --http=0.0.0.0:8090), open port 8090 in UFW, create your admin, then revert. Do not leave this open.
On first visit, PocketBase prompts you to create the first admin account (email + password). This account has full access to collections, records and settings, so use a strong password and enable a password manager.
After login you land on the Collections screen, with two default collections: users (authentication records) and a built-in _superusers table for admins.
Step 6: Create Collections and Schema
Collections are PocketBase's equivalent of tables. Each collection has a typed schema and generates a REST API automatically.
Click New collection and create a posts collection with these fields:
| Field name | Type | Options |
|---|---|---|
title | Text | required, min length 3, max length 200 |
slug | Text | required, unique, pattern ^[a-z0-9-]+$ |
content | Editor | required |
cover | File | single file, images only, max 5 MB |
published | Bool | default false |
author | Relation | → users, single, required |
tags | JSON | optional |
GET /api/collections/posts/records— list with filter, sort, expandGET /api/collections/posts/records/:id— fetch onePOST /api/collections/posts/records— createPATCH /api/collections/posts/records/:id— updateDELETE /api/collections/posts/records/:id— delete
curl http://127.0.0.1:8090/api/collections/posts/recordsIf List API rules are empty, the response will be an authorization error — which is exactly what we want. Let's fix that with rules next.
Step 7: Authentication and API Rules
PocketBase auth works out of the box. The built-in users collection supports email/password signup, email verification, password reset and OAuth2.
Signup and Login from the API
# Create an account
curl -X POST http://127.0.0.1:8090/api/collections/users/records \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "supersecret123",
"passwordConfirm": "supersecret123",
"name": "Alice"
}'Log in and receive a JWT
curl -X POST http://127.0.0.1:8090/api/collections/users/auth-with-password \
-H "Content-Type: application/json" \
-d '{
"identity": "[email protected]",
"password": "supersecret123"
}'The login response contains a token (JWT) and the full user record. Pass the token in subsequent requests as:
Authorization: Bearer <token>Enabling OAuth2
In the admin UI, open the users collection → Options → OAuth2 and toggle providers like Google or GitHub. Paste each provider's Client ID and Client Secret, set the redirect URL to https://api.example.com/api/oauth2-redirect, and the PocketBase JS SDK will handle the full flow with pb.collection('users').authWithOAuth2({ provider: 'google' }).
API Rules (Per-Collection Permissions)
Every collection has five rule slots: List, View, Create, Update, Delete. Rules are filter expressions evaluated against the authenticated request. An empty rule means "admins only"; "" (literally the empty string) means "public".
For the posts collection, configure:
| Action | Rule | Meaning |
|---|---|---|
| List | published = true \</td><td>\</td><td>author = @request.auth.id | Anyone can see published posts, authors see their own drafts |
| View | published = true \</td><td>\</td><td>author = @request.auth.id | Same as list |
| Create | @request.auth.id != "" | Any logged-in user can create |
| Update | author = @request.auth.id | Only the author can edit |
| Delete | author = @request.auth.id | Only the author can delete |
curl list call now returns published posts without authentication.Step 8: Realtime Subscriptions
PocketBase pushes record changes over Server-Sent Events. The JavaScript SDK exposes this as pb.collection('posts').subscribe().
Install the SDK in your frontend:
npm install pocketbaseMinimal browser example:
import PocketBase from 'pocketbase';const pb = new PocketBase('https://api.example.com');
await pb.collection('users').authWithPassword('[email protected]', 'supersecret123');
// Subscribe to all create/update/delete events on posts pb.collection('posts').subscribe('*', (e) => { console.log(e.action, e.record); // e.action is one of: 'create' | 'update' | 'delete' });
// Or subscribe to a single record pb.collection('posts').subscribe('RECORD_ID', (e) => { console.log('This post changed:', e.record); });
Subscriptions respect the same API rules as REST. If a user cannot View a record, they will not receive realtime events for it. Behind the scenes this is a long-lived GET /api/realtime connection — Nginx configuration below includes the right settings for SSE.
Step 9: File Uploads
The cover field on our posts collection accepts image uploads. PocketBase handles multipart form data natively.
Upload a cover image with curl:
TOKEN="eyJhbGciOi..." # from auth-with-password AUTHOR_ID="k9aljg32hjkl1p"
curl -X POST http://127.0.0.1:8090/api/collections/posts/records \ -H "Authorization: Bearer ${TOKEN}" \ -F "title=My First Post" \ -F "slug=my-first-post" \ -F "content=<p>Hello world</p>" \ -F "author=${AUTHOR_ID}" \ -F "published=true" \ -F "cover=@./cover.jpg"
Files are stored under /opt/pocketbase/pb_data/storage/<collectionId>/<recordId>/. Access them via:
https://api.example.com/api/files/<collectionId>/<recordId>/<filename>PocketBase also supports on-the-fly thumbnails: append ?thumb=100x100 (or 100x100f, 100x100b, 0x100) to resize images without storing extra derivatives.
S3-Compatible Storage
For production, you can switch storage from local disk to any S3-compatible provider (AWS S3, Backblaze B2, Cloudflare R2, Contabo Object Storage). In the admin UI: Settings → Files storage → Use S3 storage. Paste the endpoint, bucket, region, access key and secret. PocketBase will migrate new uploads to S3; existing files stay local until you copy them.
Step 10: Extend with JavaScript Hooks
This is where PocketBase goes from "nice little BaaS" to "real backend". An embedded Goja JavaScript runtime lets you register hooks, custom routes and scheduled jobs without recompiling.
Create the hooks directory:
sudo -u pocketbase mkdir -p /opt/pocketbase/pb_hooksExample hook: send a welcome email when a new user signs up, and add a custom /api/hello route:
sudo -u pocketbase tee /opt/pocketbase/pb_hooks/main.pb.js > /dev/null <<'EOF' // Fires after a new record is created in the "users" collection onRecordAfterCreateRequest((e) => { console.log([hook] new user signed up: ${e.record.get("email")});// Send a welcome email using PocketBase's built-in mailer const message = new MailerMessage({ from: { address: e.app.settings().meta.senderAddress, name: e.app.settings().meta.senderName }, to: [{ address: e.record.get("email") }], subject: "Welcome!", html:
<p>Hi ${e.record.get("name") || "there"}, thanks for signing up.</p>, });try { e.app.newMailClient().send(message); } catch (err) { console.log("mailer error:", err); } }, "users");
// Custom route: GET /api/hello?name=Bob → {"message":"Hello, Bob"} routerAdd("GET", "/api/hello", (c) => { const name = c.queryParam("name") || "world"; return c.json(200, { message:
Hello, ${name}}); });
// Daily cron job at 03:00 server time cronAdd("dailyCleanup", "0 3 *", () => { console.log("[cron] running daily cleanup"); // e.g. delete unverified users older than 30 days }); EOF
Reload PocketBase to pick up the hook file:
sudo systemctl restart pocketbaseTest the custom route:
curl http://127.0.0.1:8090/api/hello?name=Alice
{"message":"Hello, Alice"}
For heavier workloads — sharp image processing, native dependencies, SDKs not available in Goja — you can also use PocketBase as a Go library and compile your own binary. The same hooks API exists in Go.
Step 11: Nginx Reverse Proxy and Let's Encrypt TLS
Now put a proper front door on PocketBase. If you have not installed Nginx yet, see our Nginx install guide for the full walkthrough.
Install Nginx and Certbot:
sudo apt install -y nginx certbot python3-certbot-nginxCreate the site config. Replace api.example.com with your domain:
sudo tee /etc/nginx/sites-available/pocketbase > /dev/null <<'EOF' server { listen 80; server_name api.example.com; return 301 https://$host$request_uri; }server { listen 443 ssl http2; server_name api.example.com;
# Certbot will fill these in ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
# Security headers add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options DENY always; add_header Referrer-Policy strict-origin-when-cross-origin always;
# Raise upload size to match your largest file field (5 MB in our posts.cover) client_max_body_size 50m;
location / { proxy_pass http://127.0.0.1:8090; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme;
# Required for PocketBase realtime (SSE) and long uploads proxy_buffering off; proxy_cache off; proxy_read_timeout 600s; proxy_send_timeout 600s; } } EOF
sudo ln -sf /etc/nginx/sites-available/pocketbase /etc/nginx/sites-enabled/pocketbase sudo nginx -t
Obtain a certificate:
sudo certbot --nginx -d api.example.com
sudo systemctl reload nginxCertbot schedules automatic renewal via a systemd timer. Verify it:
sudo systemctl list-timers | grep certbotYou can now hit https://api.example.com/_/ in your browser to reach the admin UI over TLS, and https://api.example.com/api/... for your REST endpoints.
Locking Down the Admin UI
The /_/ path is only protected by the admin password. Options to further reduce attack surface:
- Add a second Nginx
location /_/block withallow <your-ip>; deny all; - Add HTTP Basic Auth in front of
/_/on top of the admin login - Move
/_/behind a VPN or Tailscale
Step 12: Backups of pb_data
Everything stateful in PocketBase lives under /opt/pocketbase/pb_data:
data.db— the main SQLite database (records, collections, schema)auxiliary.db— logs and internal metricsstorage/— uploaded fileslogs.db— request logs (if enabled)backups/— built-in snapshot archives
Option A: Built-in Backup API
PocketBase has a first-class backup feature. In the admin UI: Settings → Backups → Create new backup. It produces a .zip with the entire pb_data directory. You can also trigger it from the API:
curl -X POST https://api.example.com/api/backups \
-H "Authorization: Bearer <admin-token>" \
-H "Content-Type: application/json" \
-d '{"name":"daily.zip"}'Settings → Backups can upload each backup automatically to any S3-compatible bucket — combine this with Cloudflare R2 or Backblaze B2 for $0 egress offsite storage.
Option B: Scheduled SQLite Snapshots
For finer-grained backups, use SQLite's online backup from cron. SQLite is safe to snapshot while PocketBase is running (WAL mode handles concurrency).
sudo apt install -y sqlite3
sudo mkdir -p /opt/pocketbase/snapshots
sudo chown pocketbase:pocketbase /opt/pocketbase/snapshotssudo tee /opt/pocketbase/snapshot.sh > /dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
TS=$(date +%Y%m%d-%H%M%S)
OUT="/opt/pocketbase/snapshots/data-${TS}.db"
sqlite3 /opt/pocketbase/pb_data/data.db ".backup ${OUT}"
gzip -9 "${OUT}"
Keep last 14 snapshots
ls -1t /opt/pocketbase/snapshots/data-*.db.gz | tail -n +15 | xargs -r rm --
EOF
sudo chmod +x /opt/pocketbase/snapshot.sh
sudo chown pocketbase:pocketbase /opt/pocketbase/snapshot.shSchedule it daily at 04:00:
sudo crontab -u pocketbase -e
add this line:
0 4 * /opt/pocketbase/snapshot.sh >> /opt/pocketbase/snapshots/snapshot.log 2>&1Pair this with rclone or rsync to push /opt/pocketbase/snapshots/ and /opt/pocketbase/pb_data/storage/ to offsite object storage. A complete disaster-recovery plan is: hourly local SQLite backups, daily tarball of storage/ to S3, weekly full pb_data zip to a different region.
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
Active: failed after systemctl start pocketbase | Wrong path or missing execute bit on the binary | ls -la /opt/pocketbase/pocketbase; ensure owner pocketbase and mode 0755. Run sudo journalctl -u pocketbase -n 50 for the actual error. |
| Admin UI reachable but API returns 404 | Nginx stripped the path, or hitting the wrong port | Confirm proxy_pass http://127.0.0.1:8090; (no trailing slash) and that the service listens on 8090 with ss -tlnp \</td><td>grep 8090. |
| Realtime subscription disconnects every few seconds | Nginx is buffering SSE | Make sure proxy_buffering off; is inside the location / block and proxy_read_timeout is at least 600s. |
413 Request Entity Too Large on uploads | Default Nginx body limit is 1 MB | Raise client_max_body_size to cover your largest file field (e.g. 50m). |
database is locked under load | Too-aggressive concurrent writes, or pb_data on a network mount | Keep pb_data on local NVMe, never NFS/SMB. Consider batching writes. |
Backups fail with unable to open database file | Cron user cannot read pb_data | Ensure the cron runs as pocketbase (crontab -u pocketbase), not root, and pb_data is 0700 owned by pocketbase. |
| High CPU after a JS hook change | Infinite loop or recursive create → hook → create | Disable the hook (rename main.pb.js), restart, inspect logs with journalctl -u pocketbase. |
Cannot reach /_/ after enabling Nginx | Certbot modified the server block, removed the generic location | sudo nginx -t and re-check the file under /etc/nginx/sites-available/pocketbase. |
FAQ
Is PocketBase production-ready?
Yes. PocketBase has been in active development since 2022, is used by thousands of production apps, and the Go/SQLite combination has decades of proven reliability. The main caveats are the ones inherent to any single-node SQLite architecture: you scale vertically, and you should not put pb_data on a networked filesystem.
How many requests per second can PocketBase handle?
On a single 2 vCPU / 4 GB VPS (our CloudCore Starter), PocketBase comfortably sustains several thousand read requests per second and hundreds of writes per second thanks to SQLite WAL mode. Realtime subscriptions use long-lived connections and scale to tens of thousands of concurrent clients on modest hardware. Benchmarks on the PocketBase docs site and community runs show it outperforming many heavier Node.js backends because SQLite avoids cross-process network calls.
Can I use PocketBase with a Next.js or SvelteKit frontend?
Absolutely — this is its canonical use case. Install the pocketbase npm package, initialise new PocketBase('https://api.example.com'), and call pb.collection('...') from both server components (SSR) and the browser. For server-side rendering, pass the user's auth cookie into PocketBase on each request so API rules evaluate correctly. See the official SDK docs at pocketbase.io/docs for SSR patterns.
How do I migrate from Firebase to PocketBase?
Export your Firestore collections as JSON, map each collection to a PocketBase collection with equivalent fields, then import records through the admin UI's CSV/JSON import or via the REST API. Firebase Auth users can be imported into PocketBase's users collection — you will need to force a password reset on first login because Firebase's password hashes are not compatible. Firebase Storage files are easy to migrate: download and re-upload to PocketBase's file fields (or point PocketBase at an S3 bucket that already holds them).
Can I run multiple PocketBase instances on one VPS?
Yes — each instance is a separate binary pointing at a separate pb_data directory and port. Copy the systemd unit (pocketbase-app2.service), change WorkingDirectory and the --http port (e.g. 8091), and add a matching Nginx server block. This is a clean way to host multiple small apps on one VPS without container overhead.
How do I monitor PocketBase?
Out of the box, journalctl -u pocketbase captures all application logs. For metrics, wrap PocketBase with a small Go binary that exposes Prometheus endpoints, or scrape Nginx access logs with Loki + Grafana. The admin UI's Logs tab shows recent HTTP requests with filters, which is often enough for small apps.
Next Steps
PocketBase is live, authenticated and backed up. Here is what to build on top:
- Connect a frontend — scaffold a SvelteKit, Next.js, Nuxt or Flutter app with the PocketBase SDK and point it at
https://api.example.com. - Add OAuth2 providers — enable Google, GitHub, Apple or Discord logins in the
userscollection options for one-click signup. - Set up transactional email — in Settings → Mail settings, configure SMTP (Postmark, Resend, AWS SES, or self-hosted Mailcow) so password resets and the welcome hook from Step 10 actually send.
- Automate deployments — build a small CI pipeline that uploads new
pb_hooksfiles withrsyncover SSH, then callssystemctl restart pocketbase. - Graduate to Postgres when needed — if you hit true multi-writer scale, you can export your PocketBase JSON and import it into Supabase. Until then, enjoy the simplicity.
- Read the official docs — the best reference is still pocketbase.io/docs/, which covers the full JS hooks API, filter syntax, and SDK patterns.
Your Backend, Your VPS, 15 Minutes>
PocketBase pairs perfectly with a CloudCore Starter VPS:>
- Ubuntu 24.04 LTS pre-installed
- 2 vCPU, 4 GB RAM, 50 GB NVMe
- Unmetered bandwidth
- Root SSH access in under 60 seconds>
Deploy a VPS and run the commands in this guide end to end in about 25 minutes.