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 sudo privileges.
  • A domain or subdomain (this guide uses api.your_domain) with a DNS A record pointing to your server's public IP.
  • Nginx installed, and UFW allowing SSH and the Nginx Full profile.

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.

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:

FieldTypeOptions
titlePlain textRequired
bodyRich editor
publishedBool
authorRelationCollection users, single

Open the API Rules tab and set:

RuleValueEffect
List/Searchpublished = trueAnyone can list published posts
Viewpublished = trueAnyone can read a published post
Create@request.auth.id != "" && author = @request.auth.idLogged-in users create posts as themselves
Updateauthor = @request.auth.idOnly the author edits
Deleteauthor = @request.auth.idOnly 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.