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-1xmachine 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 (
amsfor the Netherlands) so the app feels instant. - Simple TLS. Fly terminates HTTPS for
*.fly.devautomatically 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 = 1andmaxat 1.
Prerequisites
- A Fly.io account with a payment method.
- The
flyctlCLI installed and authenticated:
# macOSbrew install flyctlfly auth login- A local checkout of the monorepo. All commands below run from
apps/pocketbase:
cd /Users/stijn/Developer/wva-shb-partnership/padel/apps/pocketbaseThe 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.20ARG TARGETARCH=amd64
RUN apk add --no-cache ca-certificates unzip wget
WORKDIR /pbRUN 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 /pbCOPY --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:8090binds to all interfaces so Fly’s proxy can reach it.--dir=/pb/pb_datapoints the database and file storage at the mounted volume.pb_migrationsis copied in; on boot PocketBase applies any pending migrations againstpb_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 = trueredirects all HTTP traffic to HTTPS.auto_stop_machines = false+min_machines_running = 1keep the single instance always on, so SQLite is never abruptly stopped mid-write./api/healthis PocketBase’s built-in health endpoint and returns200when the server is ready.
Create the volume
Create the persistent volume before the first deploy, in the same region as the app:
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:
# Register the app; answer "No" if asked to overwrite fly.tomlfly launch --no-deploy
# Build the image and roll it outfly deployCreate 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:
fly ssh console# inside the machine:/pb/pocketbase admin create admin@rlpadelacademy.nl "a-strong-password" --dir=/pb/pb_dataexitImport 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:
EXPO_PUBLIC_PB_URL=https://rl-padel-pb.fly.dev node seed.mjsPoint 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):
EXPO_PUBLIC_PB_URL=https://rl-padel-pb.fly.devRebuild 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:
fly volumes snapshots list <volume-id>Manual copy via SFTP — pull the SQLite file and storage directory:
fly ssh sftp get /pb/pb_data/data.db ./backups/data.dbYou 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.
fly scale vm shared-cpu-2x --memory 1024 # bigger machine, still one instancefly scale count 1 # never more than 1CI/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:
fly tokens create deploy -x 999999h# copy the output into GitHub: Settings → Secrets and variables → Actionsconcurrency 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):
fly certs add api.rlpadelacademy.nlfly certs show api.rlpadelacademy.nl # shows the DNS records to addAdd 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.