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.compointing 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-activereturnsactive./api/healthresponds 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
- SearXNG with Docker Compose + Caddy + Redis
- Typesense with Docker Compose + Caddy + API keys
- Linkwarden with Docker Compose + Caddy + PostgreSQL
Talk to us
If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.
Header image: Unsplash, no watermark.