Skip to Content

Production Guide: Deploy PocketBase with systemd + NGINX + SQLite + UFW on Ubuntu

Run PocketBase as a hardened single-binary backend with TLS, backups, firewall rules, and operational checks.

PocketBase is a strong fit when a small team needs a real backend quickly: authentication, REST and realtime APIs, file storage, admin UI, and SQLite in one portable binary. That simplicity is also why production setup deserves care. A demo command on a developer laptop is not the same as a service with controlled permissions, TLS, backups, logs, firewall rules, and a repeatable upgrade path.

This guide deploys PocketBase on Ubuntu behind NGINX with systemd managing the process and UFW limiting network exposure. It is intentionally boring: the database lives in one known directory, the service listens only on localhost, NGINX handles the public edge, and backups are testable with standard Linux tools. Use this pattern for internal tools, customer portals, prototypes that have become important, or line-of-business apps where a full Kubernetes stack would be unnecessary overhead.

Architecture and flow overview

The flow is simple. Public users connect to https://pb.example.com. NGINX terminates TLS and proxies requests to PocketBase on 127.0.0.1:8090. PocketBase stores application data under /opt/pocketbase/data, including its SQLite database and uploaded files. systemd starts the binary after networking is available and restarts it if the process exits unexpectedly. A daily backup job uses SQLite's online backup command and packages the data directory for restore testing.

This design keeps PocketBase private from the public network. Only ports 22, 80, and 443 are allowed through UFW, and only NGINX can talk to the application port. NGINX also gives you one place to enforce request limits, websocket headers, certificate renewal, and future access controls such as IP allowlists or an SSO gateway.

Prerequisites

  • Ubuntu 22.04 or 24.04 VPS with root or sudo access.
  • A DNS record such as pb.example.com pointing to the server.
  • At least 1 vCPU, 1 GB RAM, and enough disk for uploaded files and backups.
  • A plan for off-server backups. Local backups are useful for fast recovery, but they are not disaster recovery by themselves.
  • A replacement strategy for the example domain and email values before running commands.

Step-by-step deployment

1) Install packages and create a service user

Start by installing baseline tools and creating a locked-down user. Running PocketBase as its own account limits the blast radius if an application bug or bad plugin writes somewhere unexpected.

sudo apt update
sudo apt install -y nginx ufw unzip curl sqlite3 jq ca-certificates
sudo useradd --system --create-home --home-dir /opt/pocketbase --shell /usr/sbin/nologin pocketbase
sudo install -d -o pocketbase -g pocketbase /opt/pocketbase/{bin,data,backups}

If the copy button is unavailable, select the command block manually and copy it.

2) Download and install the PocketBase binary

Pin the PocketBase version rather than pulling a moving target. Review release notes before changing the version in production, especially when authentication, migrations, or file storage behavior changes.

PB_VERSION="0.31.0"
cd /tmp
curl -fL "https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/pocketbase_${PB_VERSION}_linux_amd64.zip" -o pocketbase.zip
unzip -o pocketbase.zip pocketbase
sudo install -o root -g root -m 0755 pocketbase /opt/pocketbase/bin/pocketbase
/opt/pocketbase/bin/pocketbase --version

If the copy button is unavailable, select the command block manually and copy it.

3) Store runtime settings outside the unit file

The environment file separates deployment-specific values from the systemd unit. Keep secrets and public URLs out of shell history where possible, restrict the file to root and the service group, and document who can modify it.

sudo tee /etc/pocketbase.env >/dev/null <<'EOF'
PB_HOST=127.0.0.1
PB_PORT=8090
PUBLIC_URL=https://pb.example.com
EOF
sudo chmod 0640 /etc/pocketbase.env
sudo chown root:pocketbase /etc/pocketbase.env

If the copy button is unavailable, select the command block manually and copy it.

4) Create the systemd service

The service binds to localhost only and uses systemd hardening options that still allow writes to the PocketBase data and backup directories. If you add a custom public directory, create it under /opt/pocketbase/public and keep ownership explicit.

sudo tee /etc/systemd/system/pocketbase.service >/dev/null <<'EOF'
[Unit]
Description=PocketBase application server
Documentation=https://pocketbase.io/docs/
After=network-online.target
Wants=network-online.target

[Service]
User=pocketbase
Group=pocketbase
EnvironmentFile=/etc/pocketbase.env
WorkingDirectory=/opt/pocketbase
ExecStart=/opt/pocketbase/bin/pocketbase serve --http=${PB_HOST}:${PB_PORT} --dir=/opt/pocketbase/data --publicDir=/opt/pocketbase/public
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
ReadWritePaths=/opt/pocketbase/data /opt/pocketbase/backups
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now pocketbase
systemctl status pocketbase --no-pager

If the copy button is unavailable, select the command block manually and copy it.

5) Configure NGINX as the public reverse proxy

PocketBase uses realtime connections, so the proxy configuration includes websocket headers and a longer read timeout. Increase client_max_body_size if your application accepts larger uploads.

sudo tee /etc/nginx/sites-available/pocketbase >/dev/null <<'EOF'
server {
    listen 80;
    server_name pb.example.com;

    client_max_body_size 100m;

    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;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 300;
    }
}
EOF
sudo ln -sfn /etc/nginx/sites-available/pocketbase /etc/nginx/sites-enabled/pocketbase
sudo nginx -t
sudo systemctl reload nginx

If the copy button is unavailable, select the command block manually and copy it.

6) Add TLS and firewall rules

Certbot can write the HTTPS server block for NGINX. UFW then exposes only SSH and the web ports. If SSH is restricted by your provider firewall, match those rules before enabling UFW.

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d pb.example.com --redirect --agree-tos -m [email protected] --no-eff-email
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw --force enable
sudo ufw status verbose

If the copy button is unavailable, select the command block manually and copy it.

7) Add backups and retention

SQLite can be backed up safely while PocketBase is running by using the .backup command. The tarball also includes uploaded files, migrations, and supporting data. In production, sync the resulting archive to object storage or another server and rehearse restores monthly.

sudo tee /usr/local/sbin/backup-pocketbase >/dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
stamp=$(date -u +%Y%m%dT%H%M%SZ)
dest="/opt/pocketbase/backups/pocketbase-${stamp}.tar.gz"
sudo -u pocketbase sqlite3 /opt/pocketbase/data/data.db ".backup '/opt/pocketbase/backups/data-${stamp}.db'"
tar -C /opt/pocketbase -czf "$dest" data backups/data-${stamp}.db
find /opt/pocketbase/backups -type f -mtime +14 -delete
echo "$dest"
EOF
sudo chmod 0750 /usr/local/sbin/backup-pocketbase
sudo /usr/local/sbin/backup-pocketbase
sudo tee /etc/cron.d/pocketbase-backup >/dev/null <<'EOF'
17 2 * * * root /usr/local/sbin/backup-pocketbase >/var/log/pocketbase-backup.log 2>&1
EOF

If the copy button is unavailable, select the command block manually and copy it.

Configuration and secrets handling best practices

Treat PocketBase as an application platform, not just a binary. Keep the admin account protected with a unique password and MFA where available. Do not hard-code SMTP credentials or third-party API keys inside application code if you can inject them through environment variables or a secret manager. Restrict write access to /etc/pocketbase.env, /etc/systemd/system/pocketbase.service, and /opt/pocketbase/data.

Use separate collections for internal and public data, and review PocketBase API rules before exposing a collection to the internet. The most common production mistake is creating a collection that works during testing and forgetting that list or view rules are now public. Log administrative changes, document collection rules in your repository, and export migrations so the server can be rebuilt without clicking through the admin UI.

For file uploads, set realistic size limits at both NGINX and the application layer. If the app will store user-generated content, add malware scanning or moderation upstream. PocketBase is compact, but your operational obligations still depend on the data you collect.

Verification checklist

Verify from the server first, then from the public internet. Local checks prove the service and database are healthy; public checks prove DNS, TLS, proxy headers, and firewall policy are correct.

systemctl is-active pocketbase
curl -fsS http://127.0.0.1:8090/api/health | jq .
curl -I https://pb.example.com/api/health
sudo journalctl -u pocketbase -n 80 --no-pager
sudo certbot renew --dry-run

If the copy button is unavailable, select the command block manually and copy it.

  • systemctl is-active returns active.
  • /api/health responds locally and through HTTPS.
  • The PocketBase admin setup page loads only through the intended domain.
  • Uploads succeed for a file near your expected maximum size.
  • A backup archive is created and can be listed with tar -tzf.
  • Certificate renewal dry-run passes without manual steps.

Common issues and fixes

NGINX shows 502 Bad Gateway

Check systemctl status pocketbase and confirm the service is listening on 127.0.0.1:8090. A typo in /etc/pocketbase.env, wrong binary path, or missing write permission on /opt/pocketbase/data will usually appear in journalctl -u pocketbase.

Realtime features do not update in the browser

Make sure the NGINX location includes Upgrade and Connection headers and that no upstream firewall or CDN is blocking websocket traffic. Test directly with the browser developer tools network panel while triggering a realtime event.

Uploads fail with 413 errors

Increase client_max_body_size in the NGINX server block and reload NGINX. Then verify that your PocketBase collection and application code allow the expected size. Keep the limit intentional so a single user cannot fill the disk.

Backups exist but restores have missing files

Database-only backups are not enough if your app accepts uploads. Restore tests should include the SQLite database and the uploaded files under the data directory. Keep at least one off-server copy so a disk failure does not remove the app and every backup at once.

Admin setup is exposed longer than expected

Create the first admin immediately after deployment. For sensitive environments, temporarily restrict the NGINX location by IP while bootstrapping the instance, then remove or adjust the restriction after credentials and access rules are configured.

FAQ

Is PocketBase production ready?

It can be production ready for the right workload: small to medium apps, internal tools, portals, and prototypes with clear data boundaries. The operational model is different from a distributed database stack, so capacity planning and backups matter.

Should I use SQLite for a public application?

Yes, if the workload is appropriate and write concurrency is modest. SQLite is reliable when placed on local disk, backed up correctly, and monitored. If you expect heavy concurrent writes or multi-region requirements, choose a different architecture.

Can PocketBase run behind Cloudflare?

Yes. Keep NGINX as the origin proxy, preserve websocket support, and configure trusted proxy headers carefully. If you rely on Cloudflare Access, document how administrators can reach the service during an incident.

How do I upgrade safely?

Read release notes, take a backup, stop the service, replace the binary, start the service, and run health checks. For important apps, rehearse the upgrade on a copy of production data before changing the live server.

What should I monitor?

Monitor process health, TLS renewal, disk usage, backup age, HTTP error rates, and application-specific signals such as failed logins or queue-like workflows. PocketBase is small, so disk exhaustion is often the first serious failure mode.

Can I run multiple PocketBase apps on one server?

Yes, but give each app its own user, data directory, port, systemd unit, NGINX server block, and backup job. Do not share one writable directory across unrelated apps.

Internal links

Talk to us

If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.

Contact Us

Header image: Unsplash, no watermark.

Production Guide: Deploy Immich with Docker Compose + Caddy + PostgreSQL + Redis on Ubuntu
A production-oriented self-hosted photo and video library with TLS, background workers, machine learning, backups, and recovery checks.