<!-- OSSAlt AI-readable guide source -->
<!-- Canonical: https://ossalt.com/guides/self-hosting-guide-pocketbase-2026 -->
<!-- Raw Markdown: https://ossalt.com/guides/self-hosting-guide-pocketbase-2026/raw.md -->
<!-- Source path: content/guides/self-hosting-guide-pocketbase-2026.mdx -->

---
og_image: "/images/guides/self-hosting-guide-pocketbase-2026.webp"
title: "Self-Hosting PocketBase: Minimal Backend 2026"
description: "Deploy PocketBase on your own VPS — single binary setup, collections, auth, real-time subscriptions, and production tips for self-hosted backends in 2026."
date: "2026-03-08"
author: "OSSAlt Team"
tags: ["pocketbase", "backend", "self-hosting", "guide"]
tier: 1
---

PocketBase is the simplest backend you can deploy — a single Go binary with a built-in database (SQLite), admin UI, auth, file storage, and real-time subscriptions. No Docker needed. Download, run, done.

## Why Self-Host PocketBase

Firebase's pricing model punishes success. The free Spark plan is generous for prototypes — 1 GB Firestore storage, 50K reads/day, 20K writes/day. But the moment you exceed those limits and move to the Blaze pay-as-you-go plan, costs can spike unpredictably. Firestore charges $0.06 per 100K reads and $0.18 per 100K writes; a modestly popular app hitting 10 million reads/month pays $6 in reads alone plus bandwidth charges. Supabase Cloud's free tier pauses inactive projects, forcing you to the $25/month Pro plan for any production app.

Self-hosting PocketBase costs the price of a VPS. A Hetzner CX11 at €3.29/month (or DigitalOcean's $6/month droplet) gives you a complete backend with zero per-operation pricing. Annual cost: under $80 versus Firebase's unpredictable scaling costs. For a side project hitting 5 million Firestore reads/month, that's $3/month in Firebase charges versus $3.29/month for a PocketBase server that handles the same load with far more predictable costs.

**Operational simplicity is PocketBase's headline advantage.** There is literally one file to manage. No Docker, no Kubernetes, no Redis, no separate database server. A single Go binary runs PocketBase — install it, run it as a systemd service, and you have a complete backend platform. Upgrades are as simple as replacing the binary and restarting the service.

**SQLite performance surprises people.** The common assumption is that SQLite can't handle real production load, but PocketBase's architecture (connection pooling, WAL mode enabled by default) handles thousands of concurrent reads comfortably. For most indie projects and small SaaS applications, SQLite's performance is not the bottleneck — network latency is. PocketBase's creator documented handling 10,000 concurrent connections on a $6/month VPS.

**JavaScript-extensible.** PocketBase supports custom hooks and routes written in JavaScript (via a built-in JS runtime), so you can add custom business logic, trigger webhooks on database events, or extend the API without rewriting the Go binary.

**When NOT to self-host PocketBase:** PocketBase uses SQLite, which has fundamental limitations for write-heavy workloads at scale. If your app has high-frequency concurrent writes (real-time collaborative editing, financial transactions at high volume, event streaming), PostgreSQL-backed alternatives like Supabase handle concurrent writes more gracefully. PocketBase also lacks built-in database clustering — it's a single-node solution.

## Prerequisites

PocketBase has the lowest barrier to entry of any tool in this guide. The prerequisites are minimal, but understanding them properly prevents the most common gotchas. Before committing to a VPS, review your [VPS options for self-hosting](/guides/self-hosting-vps-comparison-2026) — PocketBase's single-binary design means even the cheapest servers work well.

**Server specs:** PocketBase's resource requirements are remarkably low. For 100-1,000 users, 512 MB RAM is genuinely sufficient — the Go runtime is memory-efficient and SQLite's WAL mode handles reads without loading entire tables into memory. Even the $3.29/month Hetzner CX11 (2 vCPU, 2 GB RAM) is overkill for most PocketBase deployments. CPU utilization stays low unless you're running complex JavaScript hooks.

**No Docker required.** This is intentional — PocketBase ships as a statically compiled binary with no external dependencies. Download the appropriate binary for your architecture (`linux_amd64` for standard VPS servers), make it executable, and run it. The entire database, file storage, and configuration lives in a `pb_data` directory next to the binary.

**Operating system:** Any Linux distribution works. Ubuntu 22.04 LTS is the most documented choice. The systemd service setup (Step 7) uses standard Linux conventions compatible with any systemd-based distro.

**Caddy for HTTPS:** PocketBase runs on HTTP internally; Caddy provides SSL termination and proxying to HTTPS. Install Caddy with `sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https && curl -1sLf 'https://dl.cloudflare.com/caddy/stable/debian.deb.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg && echo "deb [signed-by=/usr/share/keyrings/caddy-stable-archive-keyring.gpg] https://dl.cloudflare.com/caddy/stable/debian any-main" | sudo tee /etc/apt/sources.list.d/caddy-stable.list && sudo apt update && sudo apt install caddy`.

**Backup strategy:** SQLite's entire state is in a single file — `pb_data/data.db`. This simplicity is a double-edged sword: one file to back up is convenient, but one corrupted file means total data loss. Set up automated backups before going to production.

**Skills required:** Basic Linux command line. PocketBase doesn't require Docker, Compose files, or container knowledge. If you can SSH into a server and run commands, you can deploy PocketBase.

## Requirements

- VPS with 512 MB RAM minimum
- Domain name (e.g., `api.yourdomain.com`)
- 5+ GB disk

## Step 1: Download and Run

```bash
# Download latest release
wget https://github.com/pocketbase/pocketbase/releases/latest/download/pocketbase_0.25.0_linux_amd64.zip
unzip pocketbase_0.25.0_linux_amd64.zip

# Run
./pocketbase serve --http=0.0.0.0:8090
```

That's it. Admin UI at `http://your-server:8090/_/`, API at `http://your-server:8090/api/`.

## Step 2: Reverse Proxy (Caddy)

```
# /etc/caddy/Caddyfile
api.yourdomain.com {
    reverse_proxy localhost:8090
}
```

```bash
sudo systemctl restart caddy
```

## Step 3: Create Admin Account

1. Open `https://api.yourdomain.com/_/`
2. Create your admin account on first visit

## Step 4: Create Collections (Tables)

In the admin UI:

1. Click **New collection**
2. Name it (e.g., `posts`)
3. Add fields:

| Field | Type | Options |
|-------|------|---------|
| title | Text | Required, max 200 |
| content | Editor | Rich text |
| published | Bool | Default false |
| author | Relation | → users collection |
| image | File | Max 5MB, jpg/png/webp |
| tags | JSON | Array of strings |

4. Configure **API Rules** (who can read/write):
   - List/Search: `@request.auth.id != ""`
   - View: `""` (public)
   - Create: `@request.auth.id != ""`
   - Update: `@request.auth.id = author.id`
   - Delete: `@request.auth.id = author.id`

## Step 5: Authentication

PocketBase includes built-in auth:

**Email/password (default):**
```javascript
import PocketBase from 'pocketbase'

const pb = new PocketBase('https://api.yourdomain.com')

// Sign up
const user = await pb.collection('users').create({
  email: 'user@example.com',
  password: 'password123',
  passwordConfirm: 'password123',
  name: 'John Doe',
})

// Login
const auth = await pb.collection('users').authWithPassword(
  'user@example.com',
  'password123'
)
console.log(auth.token)
```

**OAuth2 providers:**
Configure in Admin UI → Settings → Auth providers:
- Google, GitHub, Microsoft, Apple, Discord, GitLab, Facebook, Twitter, Spotify

```javascript
// OAuth2 login
const auth = await pb.collection('users').authWithOAuth2({ provider: 'google' })
```

## Step 6: Use the API

```javascript
import PocketBase from 'pocketbase'

const pb = new PocketBase('https://api.yourdomain.com')

// Create
const post = await pb.collection('posts').create({
  title: 'My First Post',
  content: '<p>Hello world</p>',
  published: true,
})

// List with filters
const posts = await pb.collection('posts').getList(1, 20, {
  filter: 'published = true',
  sort: '-created',
  expand: 'author',
})

// Real-time subscriptions
pb.collection('posts').subscribe('*', (e) => {
  console.log(e.action) // 'create', 'update', 'delete'
  console.log(e.record)
})

// File upload
const formData = new FormData()
formData.append('title', 'Post with image')
formData.append('image', fileInput.files[0])
const post = await pb.collection('posts').create(formData)

// Get file URL
const imageUrl = pb.files.getURL(post, post.image)
```

## Step 7: Set Up as a Systemd Service

```bash
sudo nano /etc/systemd/system/pocketbase.service
```

```ini
[Unit]
Description=PocketBase
After=network.target

[Service]
Type=simple
User=pocketbase
Group=pocketbase
WorkingDirectory=/opt/pocketbase
ExecStart=/opt/pocketbase/pocketbase serve --http=0.0.0.0:8090
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
```

```bash
sudo systemctl enable pocketbase
sudo systemctl start pocketbase
```

## Step 8: Configure SMTP

In Admin UI → **Settings** → **Mail settings**:

| Setting | Value |
|---------|-------|
| Sender address | noreply@yourdomain.com |
| Sender name | My App |
| SMTP host | smtp.resend.com |
| SMTP port | 587 |
| Username | resend |
| Password | re_your_api_key |

Required for email verification, password reset, and notifications.

## Production Hardening

**Backups:**
```bash
# Copy the SQLite database (daily cron)
# PocketBase stores everything in pb_data/
cp /opt/pocketbase/pb_data/data.db /backups/pocketbase-$(date +%Y%m%d).db

# Or use PocketBase's backup API
curl -X POST 'https://api.yourdomain.com/api/backups' \
  -H 'Authorization: admin-token'
```

PocketBase's single-file SQLite database is easy to back up but critical to protect. Use [automated server backups with restic](/guides/automated-server-backups-restic-rclone-2026) to push encrypted, deduplicated copies of `pb_data/` to offsite storage daily. SQLite's WAL mode means hot copies are consistent without stopping the service.

**Updates:**
```bash
# Download new version
wget https://github.com/pocketbase/pocketbase/releases/latest/download/pocketbase_NEW_VERSION_linux_amd64.zip
unzip -o pocketbase_*_linux_amd64.zip -d /opt/pocketbase/

# Restart
sudo systemctl restart pocketbase
# PocketBase auto-migrates the database
```

**Security:**
- Restrict admin UI access by IP if possible
- Always use HTTPS (Caddy handles this)
- Set proper API rules on all collections
- Enable email verification for users

**Performance:**
- PocketBase handles 10K+ concurrent connections
- SQLite is surprisingly fast for reads
- For write-heavy apps at scale, consider PostgreSQL-based alternatives

## Resource Usage

| Users | RAM | CPU | Disk |
|-------|-----|-----|------|
| 1-100 | 128 MB | 1 core | 1 GB |
| 100-1K | 256 MB | 1 core | 5 GB |
| 1K-10K | 512 MB | 2 cores | 20 GB |

PocketBase is the most lightweight backend you can deploy.

## VPS Recommendations

| Provider | Spec | Price |
|----------|------|-------|
| Hetzner | 2 vCPU, 2 GB RAM | €4.50/month |
| DigitalOcean | 1 vCPU, 1 GB RAM | $6/month |
| Linode | 1 vCPU, 1 GB RAM | $5/month |
| Fly.io | 1 vCPU, 256 MB | Free tier |

## Production Security Hardening

PocketBase's simplicity is a security asset — a smaller attack surface than multi-service deployments. But the admin UI at `/_/` provides complete database access and must be protected. Follow the [self-hosting security checklist](/guides/self-hosting-security-checklist-2026) and apply these PocketBase-specific measures.

**UFW firewall:** Block direct access to PocketBase's port; only Caddy handles external traffic.

```bash
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw deny 8090/tcp   # Block direct PocketBase access
sudo ufw enable
```

**Fail2ban for SSH:**

```bash
sudo apt install fail2ban -y
```

`/etc/fail2ban/jail.local`:

```ini
[sshd]
enabled = true
maxretry = 5
bantime = 3600
findtime = 600
```

**Run PocketBase as a non-root user:** The systemd service above specifies `User=pocketbase`. Create the dedicated user:

```bash
sudo useradd --system --no-create-home --shell /sbin/nologin pocketbase
sudo mkdir -p /opt/pocketbase
sudo chown -R pocketbase:pocketbase /opt/pocketbase
```

This ensures that even if PocketBase is compromised, the attacker cannot escalate to root privileges.

**Restrict admin UI by IP:** Use Caddy's `remote_ip` matcher to limit `/_/` access to your own IP addresses:

```
api.yourdomain.com {
    @admin path /_/*
    handle @admin {
        @allowed remote_ip 1.2.3.4/32 5.6.7.8/32
        handle @allowed {
            reverse_proxy localhost:8090
        }
        respond 403
    }
    reverse_proxy localhost:8090
}
```

**Disable SSH password auth:**

```
PasswordAuthentication no
PermitRootLogin no
```

Restart: `sudo systemctl restart sshd`

**Automatic security updates:**

```bash
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure --priority=low unattended-upgrades
```

**API rule discipline:** Every PocketBase collection defaults to allowing all operations without authentication. Before going live, review every collection's API rules and apply the principle of least privilege — only grant the minimum access required. Collections with empty string rules (`""`) are completely public.

## Troubleshooting Common Issues

**Admin UI inaccessible at `/_/`**

PocketBase's admin UI is served at `/_/` — note the underscore, not `/admin`. If Caddy is blocking this path, verify no rewrite rules are interfering. Also check that the PocketBase service is running: `sudo systemctl status pocketbase`. Confirm it's listening on 8090: `ss -tlnp | grep 8090`.

**"Failed to create admin" on first visit**

This usually means PocketBase started correctly but the `pb_data` directory is not writable. Check: `ls -la /opt/pocketbase/` — the `pb_data` directory should be owned by the `pocketbase` user. Fix with: `sudo chown -R pocketbase:pocketbase /opt/pocketbase/pb_data`.

**Real-time subscriptions disconnect frequently**

Real-time subscriptions use Server-Sent Events (SSE). Some reverse proxies and load balancers buffer SSE connections, causing apparent disconnections. In Caddy, this works correctly by default since Caddy supports streaming responses. If you're using Nginx or another proxy, ensure `proxy_buffering off` is set for SSE routes.

**SQLite "database is locked" errors**

This typically means multiple PocketBase processes are running simultaneously, both trying to write to the same `data.db`. Check: `ps aux | grep pocketbase`. If you see duplicate processes, stop all instances and restart the systemd service cleanly: `sudo systemctl stop pocketbase && sudo systemctl start pocketbase`. Enable WAL mode if not already active — PocketBase enables it by default, but verify with: `sqlite3 pb_data/data.db "PRAGMA journal_mode;"` (should return `wal`).

**Files uploaded through the API not persisting after service restart**

If files aren't in `pb_data/storage/`, they may have been uploaded to a different path. Verify the working directory in your systemd service matches where `pb_data` is expected. The `WorkingDirectory` in the service file should point to `/opt/pocketbase`. If files disappear after restart, the service may be writing to a temporary directory instead. Check `systemctl cat pocketbase` to confirm the working directory.

**Slow queries on large collections**

SQLite performs well for reads but can slow down with large datasets and complex filters. Add indexes to frequently-queried fields using PocketBase's admin UI: open a collection, go to **Indexes** tab, and add an index on the fields you filter by most often. For collections over 100K records, check query performance using SQLite's `EXPLAIN QUERY PLAN` directly on `pb_data/data.db`.

## Ongoing Maintenance and Operations

PocketBase's operational simplicity is its defining characteristic. There's genuinely less to manage than any other backend in this guide — no container orchestration, no multi-service dependencies, no WAL configuration for multiple databases.

**The backup routine is trivial.** PocketBase stores everything — schema, data, auth, files — in the `pb_data` directory. A complete backup is `cp -r /opt/pocketbase/pb_data /backups/pb_data_$(date +%Y%m%d)`. Schedule this daily with cron. For offsite redundancy, use restic to push the backup to Backblaze B2. The entire backup pipeline, including the offsite upload, typically completes in under 5 minutes for databases under 10 GB.

**Updates are file swaps.** Upgrading PocketBase is literally downloading a new binary and restarting the service. PocketBase handles database migrations automatically on startup — the new binary detects the current schema version and applies any needed migrations. There's no `npm install`, no `docker pull`, no migration command to run manually. This is an enormous operational advantage compared to every other tool in this guide.

**Schema changes via migrations.** PocketBase v0.21+ supports JavaScript migration files stored in `pb_migrations/`. Create a migration file to add a new collection field, change an index, or seed reference data. Migration files run automatically on startup, enabling a proper development workflow: create collection changes in the admin UI on a dev instance, export the migration, and apply it to production via deployment.

**Custom hooks for business logic.** PocketBase's `pb_hooks` directory supports JavaScript files that run on data events (create, update, delete) and HTTP routes. This is how you add business logic without modifying the Go binary. Example use cases: send a welcome email when a user registers, validate complex field constraints, trigger a webhook when an order is placed, or generate a thumbnail when an image is uploaded. The hooks runtime is synchronous and simple — ideal for lightweight server-side logic.

**Monitoring the single process.** With a single process to watch, monitoring is straightforward. A simple systemd `RestartSec=5` handles crash recovery. For uptime monitoring, point Uptime Kuma at `https://api.yourdomain.com/api/health` — PocketBase returns `200 OK` from this endpoint when healthy. Alert on non-200 responses.

**When to migrate away.** PocketBase's SQLite backbone is remarkably capable, but some workload patterns push past its limits. If you're seeing `database is locked` errors under production load, write throughput is exceeding SQLite's single-writer constraint. For apps that need more write concurrency — high-frequency event logging, real-time collaborative editing, financial transaction processing — migrate to a PostgreSQL-backed backend (Supabase self-hosted, or a standard PostgreSQL database with PostgREST). The migration is non-trivial but PocketBase's data export features make it manageable.

**Community and long-term viability.** PocketBase is maintained by a small team (primarily one developer) which is both a strength (opinionated, focused) and a risk (bus factor). The project has strong community momentum with 30K+ GitHub stars and active development, but it's worth watching how the project evolves. The self-hosted nature means your existing deployments continue working indefinitely regardless of the project's future — you're not dependent on a vendor staying in business.

---

*Compare backend platforms on [OSSAlt](https://www.ossalt.com) — features, scalability, and self-hosting options side by side.*

*See open source alternatives to PocketBase on [OSSAlt](https://www.ossalt.com/alternatives/pocketbase).*
