# How to Run PocketBase in a Sandbox With a Private HTTPS URL

> Download the PocketBase binary into a small Sandboxer sandbox, create a superuser with its CLI and run it as a systemd service on port 8090. sbx host gives the dashboard and API a private HTTPS URL. Clients send the __sbx_auth cookie plus PocketBase's own token. Records and a live realtime stream survived stop and resume.

Source: https://sandboxer.com/guides/run-pocketbase-in-a-sandbox. Updated 2026-10-27.

**Short answer:** Download the PocketBase binary into a small Sandboxer sandbox, create a superuser with its CLI and run it as a systemd service on port 8090. sbx host gives the dashboard and API a private HTTPS URL. Clients send the __sbx_auth cookie plus PocketBase's own token. Records and a live realtime stream survived stop and resume.

[PocketBase](https://pocketbase.io/docs/) is a backend in one Go binary: SQLite, auth, file storage, a REST API, realtime subscriptions and an admin dashboard. No Docker and no separate database, so it fits in a `small` sandbox with room to spare.

## What we ran

Everything below ran on production on October 27, 2026, in one `small` sandbox (2 vCPU, 4 GB RAM, 12 GB disk, $0.018/hour, region `eu-central`) with PocketBase 0.40.6. Tokens are replaced with `<token>`. Times are single runs unless the table says otherwise.

| Step | Result |
| - | - |
| `sbx new --type small` | 3.2 s to ready |
| Binary size | 32 MB, one file |
| `systemctl enable --now` to a passing `/api/health` | 165 ms |
| Memory after start | 28 MB resident |
| List records through the hosted URL, from our test machine | median 0.19 s over 20 calls (0.19 to 0.20 s) |
| Same call inside the sandbox on `localhost:8090` | median 0.3 ms over 20 calls |
| Realtime event through the hosted URL | arrived 197 ms after the create request was sent |
| `sbx stop --wait`, then `sbx resume` | 2.2 s and 1.3 s, `memoryRestored: true`, same PID |
| Kill the process, systemd restarts it | back after 5.1 s (`RestartSec=5s`) |

## 1. Install the binary and a superuser

PocketBase ships prebuilt zips on its [releases page](https://github.com/pocketbase/pocketbase/releases). Unpack it into its own folder; PocketBase keeps its data in `pb_data` next to the binary.

```bash
sbx new --name pocketbase --type small --no-env
id=sb_v6tllk1ul8    # the id that sbx new printed; its subdomain was early-stormy-puffin

sbx exec $id --timeout 600 "mkdir -p ~/pb && cd ~/pb \
  && curl -fsSLo pb.zip https://github.com/pocketbase/pocketbase/releases/download/v0.40.6/pocketbase_0.40.6_linux_amd64.zip \
  && python3 -m zipfile -e pb.zip . && chmod +x pocketbase && rm pb.zip && ./pocketbase --version"
```

```text
pocketbase version 0.40.6
```

`unzip` is also installed in the sandbox if you prefer it. `sbx exec` stops a command after 30 seconds by default, so the download gets `--timeout 600`.

Create the first superuser from the command line, before the server ever starts. Then nobody can claim the dashboard by opening it first:

```bash
sbx exec $id "cd ~/pb && ./pocketbase superuser upsert admin@example.com '<password>'"
```

```text
Successfully saved superuser "admin@example.com"!
```

## 2. Run it as a systemd service

This is the unit from PocketBase's [going to production](https://pocketbase.io/docs/going-to-production/) page, changed to run as the sandbox's `user` account and to listen on `0.0.0.0:8090` instead of serving a domain itself. TLS comes from the hosted URL, so PocketBase doesn't need its own certificate.

```ini
# pocketbase.service
[Unit]
Description = pocketbase

[Service]
Type             = simple
User             = user
Group            = user
LimitNOFILE      = 4096
Restart          = always
RestartSec       = 5s
StandardOutput   = append:/home/user/pb/std.log
StandardError    = append:/home/user/pb/std.log
WorkingDirectory = /home/user/pb
ExecStart        = /home/user/pb/pocketbase serve --http=0.0.0.0:8090

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

```bash
sbx scp pocketbase.service $id:/home/user/pb/
sbx exec $id "sudo cp ~/pb/pocketbase.service /etc/systemd/system/ && sudo systemctl daemon-reload && sudo systemctl enable --now pocketbase \
  && until curl -fs localhost:8090/api/health >/dev/null; do sleep 0.05; done; curl -s localhost:8090/api/health; ps -o pid,rss,cmd -C pocketbase"
```

```text
Created symlink /etc/systemd/system/multi-user.target.wants/pocketbase.service → /etc/systemd/system/pocketbase.service.
{"message":"API is healthy.","code":200,"data":{}}
    PID   RSS CMD
    657 27956 /home/user/pb/pocketbase serve --http=0.0.0.0:8090
```

From `enable --now` to a healthy API took 165 ms. `enabled` means PocketBase also starts on a boot from disk, and `Restart=always` brings it back if it dies. We killed it with `sudo systemctl kill -s KILL pocketbase`, and it answered again 5.1 seconds later, after the 5-second `RestartSec`.

## 3. Host the dashboard and API on one private URL

```bash
sbx host $id 8090 --title PocketBase
```

```text
{"ok":true,"type":"port.hosted","sandboxId":"sb_v6tllk1ul8","port":8090,"url":"https://early-stormy-puffin-8090.box.sandboxer.com?_token=<token>","access":"private",…}
```

The dashboard is at `/_/` and the API at `/api/` on the same host:

| Request | Result |
| - | - |
| `/_/` or `/api/health`, no token | `401` from the Sandboxer proxy; PocketBase never sees it |
| `/_/?_token=<token>` | `302` to `/_/`, sets the `__sbx_auth` cookie |
| `/_/` with the cookie | `200`, the dashboard; sign in as the superuser |
| `/api/health` with the cookie | `200`, `{"message":"API is healthy."…}` |

In a browser, open the full `?_token=` link once and the cookie does the rest.

## 4. Call the API from outside the sandbox

There are two gates. The proxy wants the `__sbx_auth` cookie (its value is the URL's token). PocketBase wants its own auth token in the `Authorization` header for anything its [API rules](https://pocketbase.io/docs/api-rules-and-filters/) don't open up. The proxy passes `Authorization` through untouched, so both work together. [The Gitea guide](/guides/run-gitea-in-a-sandbox) explains how to tell the proxy's `401` from the app's.

Sign in as the superuser through the hosted URL ([auth API](https://pocketbase.io/docs/api-records/#auth-with-password)):

```bash
B=https://early-stormy-puffin-8090.box.sandboxer.com
curl -s -o /dev/null -c jar "$B/?_token=<token>"          # trade the token for the cookie once

curl -s -b jar -H 'Content-Type: application/json' \
  -d '{"identity":"admin@example.com","password":"<password>"}' \
  $B/api/collections/_superusers/auth-with-password > auth.json
PB=$(python3 -c "import json; print(json.load(open('auth.json'))['token'])")
```

Create a collection with the [collections API](https://pocketbase.io/docs/api-collections/). The empty-string rules let anyone who gets past the proxy list, view and create notes. `null` keeps update and delete for superusers only:

```json
{"name":"notes","type":"base","fields":[{"name":"text","type":"text","required":true}],
 "listRule":"","viewRule":"","createRule":"","updateRule":null,"deleteRule":null}
```

```bash
curl -s -b jar -H "Authorization: $PB" -H 'Content-Type: application/json' -d @notes.json $B/api/collections
curl -s -b jar -H 'Content-Type: application/json' -d '{"text":"hello from my laptop"}' $B/api/collections/notes/records
curl -s -b jar "$B/api/collections/notes/records?fields=id,text"
```

```text
{"id":"pbc_3395098727","listRule":"","viewRule":"","createRule":"",…,"name":"notes","type":"base",…}
{"collectionId":"pbc_3395098727","collectionName":"notes","id":"doqgofal7a1xky6","text":"hello from my laptop"}
{"items":[{"id":"doqgofal7a1xky6","text":"hello from my laptop"}],"page":1,"perPage":30,"totalItems":1,"totalPages":1}
```

Creating a collection with the cookie but without `Authorization` got PocketBase's own answer:

```text
{"data":{},"message":"The request requires valid record authorization token.","status":401}
```

Over 20 list calls from our test machine, the median was 0.19 s. Inside the sandbox the same call took 0.3 ms, so the time is the network path to `eu-central`, not PocketBase. Code that runs in the sandbox should call `http://localhost:8090` directly.

## 5. Realtime works through the proxy

PocketBase pushes record changes over [server-sent events](https://pocketbase.io/docs/api-realtime/): open `/api/realtime`, read the `PB_CONNECT` event for a client id, then post the topics you want. We did this with `curl` from our test machine, through the private URL:

```bash
curl -sN -b jar $B/api/realtime > sse.log &        # keep the stream open
cid=$(grep -m1 '^id:' sse.log | cut -d: -f2)
curl -s -b jar -H 'Content-Type: application/json' \
  -d "{\"clientId\":\"$cid\",\"subscriptions\":[\"notes\"]}" $B/api/realtime    # 204
curl -s -b jar -H 'Content-Type: application/json' -d '{"text":"pushed over SSE"}' \
  $B/api/collections/notes/records
```

```text
event:PB_CONNECT
data:{"clientId":"<client id>"}

event:notes
data:{"record":{"collectionId":"pbc_3395098727","collectionName":"notes","id":"h6milz97ue5mem5","text":"pushed over SSE"},"action":"create"}
```

The event reached the open stream 197 ms after we sent the create request, about the same as one API round trip. The proxy doesn't buffer the stream.

## 6. Stop and resume: the stream stayed open

We left that `curl` stream running and stopped the sandbox:

```bash
sbx stop $id --wait     # 2.2 s
sbx resume $id          # 1.3 s
sbx info $id            # "memoryRestored": true
sbx exec $id "pgrep -x pocketbase; systemctl show -p NRestarts pocketbase"
```

```text
657
NRestarts=0
```

Same PID, no restarts: memory was restored, so PocketBase never noticed. Both notes were still there, the hosted URL and cookie still worked, and the `curl` stream we opened before the stop was still connected. A note created after the resume arrived on it. That was one test, and the pause was short. Write clients to reconnect and resubscribe anyway.

A resume boots from disk instead when you resize or no machine with the same CPU type is free. Then processes are gone but files stay, and the `enabled` unit starts PocketBase again on the same `pb_data`. Expect open realtime streams to drop in that case; we didn't force a boot from disk.

## 7. Take a backup out before you delete

PocketBase can zip its own data with the [backups API](https://pocketbase.io/docs/api-backups/) (superuser only). The zip lands in `pb_data/backups`, and `sbx scp` copies it out:

```bash
curl -s -b jar -H "Authorization: $PB" -H 'Content-Type: application/json' \
  -d '{"name":"before-delete.zip"}' $B/api/backups          # 204
sbx scp $id:/home/user/pb/pb_data/backups/before-delete.zip .
python3 -m zipfile -l before-delete.zip
```

```text
File Name                                             Modified             Size
data.db                                        2026-10-27 10:06:42       167936
auxiliary.db                                   2026-10-27 10:06:42        45056
types.d.ts                                     2026-10-27 10:05:46       802641
```

Then delete the sandbox. The hosted URL returned `404` right after:

```bash
sbx delete $id --yes
```

A `small` sandbox costs $0.018/hour while it runs and nothing while it is stopped.

## FAQ

### Do I need Docker for PocketBase?

No. It is one 32 MB binary with SQLite built in. A systemd unit gives you restart on crash and start on boot.

### Is `small` big enough?

For development and demos, yes. PocketBase used 28 MB of RAM after start, and a `small` sandbox has 4 GB. Moving to a smaller size isn't supported yet, so starting small is the safe choice. You can resize up when you need to.

### Should I make the URL public?

Only if your API rules are ready for strangers. `sbx host $id 8090 --public` drops the proxy's check, so the dashboard login page and every collection rule set to `""` are open to anyone with the URL. We kept ours private. Switching from public back to private mints a new token, so every client needs the new cookie.

### Can my frontend use the hosted URL?

Yes, if it sends the `__sbx_auth` cookie. A browser that opened the `?_token=` link once has it. For a server or script, send `Cookie: __sbx_auth=<token>` on every request, plus PocketBase's own `Authorization` header where your rules need it. We tested this with `curl`.

### Where is my data?

In `~/pb/pb_data` on the sandbox disk: `data.db` holds your collections and records. It survives stop, resume and auto-stop. It doesn't survive `sbx delete`, so take a backup first.

## Related

- [Run Supabase locally in a cloud sandbox](/guides/run-supabase-locally-in-a-cloud-sandbox)
- [Run Gitea in a sandbox](/guides/run-gitea-in-a-sandbox)
- [Give a sandbox port an HTTPS URL](/guides/give-a-sandbox-port-an-https-url)
- [Hosting](/docs/hosting)
