Skip to content

Deploying PocketBase to Fly.io

The RL Padel Academy backend is a single PocketBase 0.22 Go binary that bundles an HTTP API, an SQLite database, file storage, and an admin UI. This shape makes Fly.io an excellent host: you run one container with one attached persistent volume, in a region close to your users, for a few dollars a month.

Why Fly.io for PocketBase

  • Single binary, single container. PocketBase has no external dependencies — no managed Postgres, no Redis, no object store. Fly runs the exact binary you tested locally.
  • Persistent volumes. SQLite and uploaded files live on disk. Fly volumes give you durable, snapshot-backed block storage that survives deploys and restarts.
  • Cheap. A shared-cpu-1x machine with 256–512 MB RAM and a 1 GB volume comfortably runs a club-sized workload.
  • Global + low latency. Pick a region near your members (ams for the Netherlands) so the app feels instant.
  • Simple TLS. Fly terminates HTTPS for *.fly.dev automatically and issues certificates for custom domains.

Important constraint: PocketBase uses embedded SQLite. It must run as a single instance. Never scale to two machines sharing one volume — you will corrupt the database. Keep min_machines_running = 1 and max at 1.

Prerequisites

  • A Fly.io account with a payment method.
  • The flyctl CLI installed and authenticated:
Terminal window
# macOS
brew install flyctl
fly auth login
  • A local checkout of the monorepo. All commands below run from apps/pocketbase:
Terminal window
cd /Users/stijn/Developer/wva-shb-partnership/padel/apps/pocketbase

The Dockerfile

We pin PocketBase to v0.22.20 for linux/amd64 and download it in a builder stage so the final image stays small. Place this at apps/pocketbase/Dockerfile:

# syntax=docker/dockerfile:1
# ---- builder: download the pinned PocketBase binary ----
FROM alpine:3.20 AS builder
ARG PB_VERSION=0.22.20
ARG TARGETARCH=amd64
RUN apk add --no-cache ca-certificates unzip wget
WORKDIR /pb
RUN wget -q "https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/pocketbase_${PB_VERSION}_linux_${TARGETARCH}.zip" \
-O /tmp/pb.zip \
&& unzip /tmp/pb.zip -d /pb \
&& rm /tmp/pb.zip \
&& chmod +x /pb/pocketbase
# ---- runtime ----
FROM alpine:3.20
RUN apk add --no-cache ca-certificates
WORKDIR /pb
COPY --from=builder /pb/pocketbase /pb/pocketbase
# Bundle migrations (and hooks) so a fresh volume self-initialises.
COPY pb_migrations /pb/pb_migrations
EXPOSE 8090
CMD ["/pb/pocketbase", "serve", "--http=0.0.0.0:8090", "--dir=/pb/pb_data"]

Notes:

  • --http=0.0.0.0:8090 binds to all interfaces so Fly’s proxy can reach it.
  • --dir=/pb/pb_data points the database and file storage at the mounted volume.
  • pb_migrations is copied in; on boot PocketBase applies any pending migrations against pb_data. If you keep your schema as migrations, the database self-initialises on first run.

The fly.toml

Place this at apps/pocketbase/fly.toml. Replace rl-padel-pb with your chosen app name (it must be globally unique).

app = "rl-padel-pb"
primary_region = "ams"
[build]
dockerfile = "Dockerfile"
[[mounts]]
source = "pb_data"
destination = "/pb/pb_data"
[http_service]
internal_port = 8090
force_https = true
auto_stop_machines = false
auto_start_machines = true
min_machines_running = 1
[[http_service.checks]]
interval = "15s"
timeout = "2s"
grace_period = "10s"
method = "GET"
path = "/api/health"
[[vm]]
size = "shared-cpu-1x"
memory = "512mb"

Key points:

  • force_https = true redirects all HTTP traffic to HTTPS.
  • auto_stop_machines = false + min_machines_running = 1 keep the single instance always on, so SQLite is never abruptly stopped mid-write.
  • /api/health is PocketBase’s built-in health endpoint and returns 200 when the server is ready.

Create the volume

Create the persistent volume before the first deploy, in the same region as the app:

Terminal window
fly volumes create pb_data --size 1 --region ams

--size 1 is 1 GB; grow later with fly volumes extend.

First deploy

Initialise the Fly app without deploying yet (so it picks up your existing fly.toml), then deploy:

Terminal window
# Register the app; answer "No" if asked to overwrite fly.toml
fly launch --no-deploy
# Build the image and roll it out
fly deploy

Create the first superadmin

Once the machine is running, create the initial superadmin. Either open the install UI at https://<app>.fly.dev/_/ and follow the prompts, or do it over SSH:

Terminal window
fly ssh console
# inside the machine:
/pb/pocketbase admin create admin@rlpadelacademy.nl "a-strong-password" --dir=/pb/pb_data
exit

Import the schema

The schema lives in apps/pocketbase/pb_schema.json (11 collections). Import it from the admin UI: log in at https://<app>.fly.dev/_/, then Settings → Import collections, paste the contents of pb_schema.json, review the diff, and apply. After import, run the seed against the live URL if you want demo data:

Terminal window
EXPO_PUBLIC_PB_URL=https://rl-padel-pb.fly.dev node seed.mjs

Point the app at Fly

Set the app’s backend URL to the Fly hostname. In the mobile app’s environment (apps/frontend/.env or the relevant EAS profile):

Terminal window
EXPO_PUBLIC_PB_URL=https://rl-padel-pb.fly.dev

Rebuild or push an OTA update so clients pick up the new URL.

Backups and scaling

PocketBase writes everything to the pb_data volume, so backups mean snapshotting that volume or copying it off the machine.

Volume snapshots are taken automatically by Fly daily and retained for 5 days. List and restore:

Terminal window
fly volumes snapshots list <volume-id>

Manual copy via SFTP — pull the SQLite file and storage directory:

Terminal window
fly ssh sftp get /pb/pb_data/data.db ./backups/data.db

You can also use PocketBase’s own scheduled backups under Settings → Backups in the admin UI, optionally targeting S3-compatible storage for off-site copies.

Scaling: scale up (more CPU/RAM, bigger volume), never out. Do not run two instances against one SQLite volume.

Terminal window
fly scale vm shared-cpu-2x --memory 1024 # bigger machine, still one instance
fly scale count 1 # never more than 1

CI/CD with GitHub Actions

Deploy automatically when anything under apps/pocketbase lands on main. Create .github/workflows/deploy-pocketbase.yml:

name: Deploy PocketBase
on:
push:
branches: [main]
paths:
- "apps/pocketbase/**"
- ".github/workflows/deploy-pocketbase.yml"
jobs:
deploy:
name: Deploy to Fly.io
runs-on: ubuntu-latest
concurrency:
group: deploy-pocketbase
cancel-in-progress: false
steps:
- uses: actions/checkout@v4
- uses: superfly/flyctl-actions/setup-flyctl@master
- name: Deploy
working-directory: apps/pocketbase
run: flyctl deploy --remote-only
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

Create the deploy token and store it as a repository secret named FLY_API_TOKEN:

Terminal window
fly tokens create deploy -x 999999h
# copy the output into GitHub: Settings → Secrets and variables → Actions

concurrency prevents two overlapping deploys from fighting over the single machine.

Custom domain + TLS

To serve the backend from your own domain (e.g. api.rlpadelacademy.nl):

Terminal window
fly certs add api.rlpadelacademy.nl
fly certs show api.rlpadelacademy.nl # shows the DNS records to add

Add the displayed records at your DNS provider — typically a CNAME to rl-padel-pb.fly.dev plus the _acme-challenge record Fly provides for certificate validation. Fly issues and renews a Let’s Encrypt certificate automatically once DNS resolves. After the certificate is active, update EXPO_PUBLIC_PB_URL to https://api.rlpadelacademy.nl and rebuild/OTA the app.