PocketBase is an open-source backend distributed as a single executable. It bundles an embedded SQLite database, user authentication, file storage, realtime subscriptions and a REST API with an admin dashboard, so there is no separate database server or runtime to install. In this tutorial you will install PocketBase on Ubuntu 24.04, run it as a hardened systemd service, publish it through Nginx with a Let's Encrypt certificate, create a collection with access rules, and set up backups.
Prerequisites
To follow this tutorial you need:
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS. PocketBase is very light: 1 vCPU and 512 MB of RAM are enough for small projects.
- A non-root user with
sudoprivileges. - A domain or subdomain (this guide uses
api.your_domain) with a DNSArecord pointing to your server's public IP. - Nginx installed, and UFW allowing SSH and the
Nginx Fullprofile.
Step 1 - Downloading PocketBase
PocketBase is published as a zip archive per platform on its GitHub releases page. Install the tools needed to find and extract the latest release:
sudo apt update
sudo apt install unzip jq curl
Look up the latest version tag through the GitHub API and store it in a variable (the tag has the form v0.xx.y, the file name drops the v):
PB_VERSION=$(curl -fsSL https://api.github.com/repos/pocketbase/pocketbase/releases/latest | jq -r .tag_name | sed 's/^v//')
echo "$PB_VERSION"
Download and unpack the Linux build for your architecture. Use linux_amd64 on x86_64 servers and linux_arm64 on ARM servers:
cd /tmp
curl -fLO "https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/pocketbase_${PB_VERSION}_linux_amd64.zip"
unzip "pocketbase_${PB_VERSION}_linux_amd64.zip" pocketbase
Create a dedicated system user without a login shell, then install the binary in /opt/pocketbase:
sudo useradd --system --home-dir /opt/pocketbase --shell /usr/sbin/nologin pocketbase
sudo install -d -o pocketbase -g pocketbase -m 0750 /opt/pocketbase
sudo install -o pocketbase -g pocketbase -m 0755 /tmp/pocketbase /opt/pocketbase/pocketbase
Confirm the binary runs:
/opt/pocketbase/pocketbase --version
pocketbase version 0.xx.y
Step 2 - Running PocketBase as a systemd service
Running PocketBase under systemd starts it at boot, restarts it on failure and sends its output to the journal. It will listen only on 127.0.0.1:8090, so the only public entry point is Nginx.
Create the unit file:
sudo nano /etc/systemd/system/pocketbase.service
Add the following content:
[Unit]
Description=PocketBase backend
After=network.target
[Service]
Type=simple
User=pocketbase
Group=pocketbase
WorkingDirectory=/opt/pocketbase
ExecStart=/opt/pocketbase/pocketbase serve --http=127.0.0.1:8090 --dir=/opt/pocketbase/pb_data
Restart=on-failure
RestartSec=5s
LimitNOFILE=4096
# Hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
[Install]
WantedBy=multi-user.target
--dir sets where PocketBase keeps its SQLite databases and uploaded files. Everything that needs to be backed up lives in /opt/pocketbase/pb_data.
Reload systemd and start the service:
sudo systemctl daemon-reload
sudo systemctl enable --now pocketbase
Check that it is running:
sudo systemctl status pocketbase --no-pager
● pocketbase.service - PocketBase backend
Loaded: loaded (/etc/systemd/system/pocketbase.service; enabled; preset: enabled)
Active: active (running) since ...
The health endpoint confirms the API answers locally:
curl -s http://127.0.0.1:8090/api/health
{"message":"API is healthy.","code":200,"data":{}}
Step 3 - Creating the superuser account
The dashboard at /_/ requires a superuser. Create one from the command line, pointing at the same data directory the service uses. Replace admin@your_domain and your_strong_password with your own values:
sudo -u pocketbase /opt/pocketbase/pocketbase superuser upsert admin@your_domain 'your_strong_password' --dir=/opt/pocketbase/pb_data
Successfully saved superuser "admin@your_domain"!
upsert creates the account or resets its password if it already exists, which is also how you recover a lost superuser password.
Step 4 - Publishing PocketBase through Nginx with HTTPS
Nginx terminates TLS and forwards requests to PocketBase. Realtime subscriptions use Server-Sent Events, long-lived HTTP responses, so the proxy must not close idle connections too early.
Create a server block:
sudo nano /etc/nginx/sites-available/pocketbase
server {
listen 80;
listen [::]:80;
server_name api.your_domain;
client_max_body_size 20M;
location / {
proxy_pass http://127.0.0.1:8090;
proxy_http_version 1.1;
proxy_set_header Connection '';
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;
proxy_read_timeout 360s;
}
}
client_max_body_size caps uploads through the proxy; raise it if your file fields accept larger files.
Enable the site, test the configuration and reload Nginx:
sudo ln -s /etc/nginx/sites-available/pocketbase /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Request a certificate with Certbot, which also adds the HTTPS listener and the HTTP to HTTPS redirect:
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d api.your_domain
Verify the public endpoint:
curl -s https://api.your_domain/api/health
{"message":"API is healthy.","code":200,"data":{}}
Open https://api.your_domain/_/ in your browser and log in with the superuser you created.
NoteBecause PocketBase sits behind a proxy, open Settings > Application in the dashboard and set the user IP proxy header to
X-Real-IP. Otherwise logs and rate limits see every client as127.0.0.1.
Step 5 - Creating a collection and access rules
Collections are PocketBase's tables. Each one gets REST endpoints automatically, and five API rules (list, view, create, update, delete) decide who can call them. A rule that is locked (empty lock icon) allows only superusers; an empty string allows everyone; any other value is a filter expression.
In the dashboard, click New collection, choose Base, name it posts and add these fields:
| Field | Type | Options |
|---|---|---|
title | Plain text | Required |
body | Rich editor | |
published | Bool | |
author | Relation | Collection users, single |
Open the API Rules tab and set:
| Rule | Value | Effect |
|---|---|---|
| List/Search | published = true | Anyone can list published posts |
| View | published = true | Anyone can read a published post |
| Create | @request.auth.id != "" && author = @request.auth.id | Logged-in users create posts as themselves |
| Update | author = @request.auth.id | Only the author edits |
| Delete | author = @request.auth.id | Only the author deletes |
Save the collection and test the public list endpoint:
curl -s "https://api.your_domain/api/collections/posts/records"
{"items":[],"page":1,"perPage":30,"totalItems":0,"totalPages":0}
Step 6 - Authenticating users and writing records
Every new PocketBase instance includes a users auth collection. Register a test user (use your own values):
curl -s -X POST "https://api.your_domain/api/collections/users/records" \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"your_strong_password","passwordConfirm":"your_strong_password"}'
Log in to obtain an auth token and the user's record ID:
curl -s -X POST "https://api.your_domain/api/collections/users/auth-with-password" \
-H "Content-Type: application/json" \
-d '{"identity":"[email protected]","password":"your_strong_password"}' | jq '{token, id: .record.id}'
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"id": "k3p9x2m1q8w7e6r"
}
Create a post, replacing your_token and your_user_id with the values returned above. The create rule rejects the request if author is not the logged-in user:
curl -s -X POST "https://api.your_domain/api/collections/posts/records" \
-H "Authorization: your_token" \
-H "Content-Type: application/json" \
-d '{"title":"Hello PocketBase","body":"<p>First post</p>","published":true,"author":"your_user_id"}' | jq '{id, title}'
{
"id": "a1b2c3d4e5f6g7h",
"title": "Hello PocketBase"
}
The anonymous list request from Step 5 now returns this post. Frontends usually talk to PocketBase through the official JavaScript SDK (npm install pocketbase), which also exposes realtime subscriptions:
import PocketBase from 'pocketbase';
const pb = new PocketBase('https://api.your_domain');
pb.collection('posts').subscribe('*', (e) => {
console.log(e.action, e.record.title); // "create", "update" or "delete"
});
Realtime events respect the same list and view rules, so anonymous subscribers only receive published posts.
Step 7 - Backing up PocketBase
All state is in /opt/pocketbase/pb_data: data.db holds your collections, auxiliary.db holds logs, and storage/ holds uploaded files. You have two practical options.
Built-in backups. In the dashboard, open Settings > Backups. You can create a backup on demand, enable a cron schedule with a retention count, and optionally store backups in any S3-compatible bucket. Each backup is a zip of pb_data, and restoring one from the same screen replaces the current data.
Off-server copy with a timer. If you already use a backup tool such as restic or Borg, point it at a consistent snapshot of the database instead of the live file. SQLite's online backup command produces one without stopping the service:
sudo apt install sqlite3
sudo install -d -o pocketbase -g pocketbase -m 0750 /var/backups/pocketbase
sudo -u pocketbase sqlite3 /opt/pocketbase/pb_data/data.db ".backup '/var/backups/pocketbase/data.db'"
Check the copy is readable:
sudo -u pocketbase sqlite3 /var/backups/pocketbase/data.db "PRAGMA integrity_check;"
ok
Include /var/backups/pocketbase and /opt/pocketbase/pb_data/storage in your file-level backup job. To restore, stop the service, put the files back into pb_data with pocketbase ownership and start it again.
Troubleshooting
The service exits immediately. Read the log with sudo journalctl -u pocketbase -n 50 --no-pager. A common cause is wrong ownership of pb_data after copying files as root; fix it with sudo chown -R pocketbase:pocketbase /opt/pocketbase.
502 Bad Gateway from Nginx. PocketBase is not listening on 127.0.0.1:8090. Confirm with sudo ss -ltnp | grep 8090 and check the --http value in the unit file.
Requests return 400 or 404 on create. A failing create rule returns an error instead of a 403 for security reasons. Test the same request with a superuser token to confirm it is a rule issue, then review the expression in the API Rules tab.
Realtime connection drops every minute. The proxy is closing the stream. Make sure the location block keeps proxy_http_version 1.1, an empty Connection header and a proxy_read_timeout above 300 seconds.
Conclusion
PocketBase is now running as a systemd service behind Nginx with HTTPS, with a posts collection protected by API rules, working user authentication and a backup routine. From here you can enable OAuth2 providers in the users collection settings, configure SMTP under Settings > Mail settings for verification and password reset emails, or extend the server with JavaScript hooks placed in /opt/pocketbase/pb_hooks.
