Host PocketBase on Render
Deploying a SQLite-backed service on Render: concepts and patterns
PocketBase is an open-source backend written in Go that provides a SQLite database, authentication, file storage, real-time subscriptions, and an admin UI, all bundled into a single executable binary. This article explains how and why the deployment pattern works when you host PocketBase on Render, first as a standalone backend and then as the data layer behind a Next.js frontend. The examples here are simplified illustrations that you'll adapt to your own project's requirements. By the end, you'll understand the architectural decisions behind deploying a SQLite-backed service on Render, a pattern applicable well beyond PocketBase.
Why PocketBase's architecture matters for deployment
PocketBase is a single-binary backend-as-a-service. One process serves the REST and real-time APIs, runs the admin dashboard, manages authentication, handles file uploads, and reads from and writes to an embedded SQLite database.
The single-binary model simplifies what you deploy, but it introduces a critical constraint: SQLite stores all data as files on the local filesystem. If the filesystem is ephemeral (as it is by default on most managed cloud platforms), every redeployment or restart destroys your database and uploaded files. The deployment challenge isn't "how do I run a binary" but "how do I ensure the filesystem it writes to survives across deploys."
On Render, the answer is a persistent disk: a filesystem volume that you attach to a web service at a specified mount path. Data written to that path persists across deploys and restarts. For PocketBase, mount the disk at the path where PocketBase stores its pb_data directory, the location that holds the SQLite database files, migration records, and uploaded assets. Without the disk, your service appears to work on first deploy but loses all state the moment Render provisions a new instance.
Any application that writes state to the local filesystem rather than to an external database or object store needs the same treatment on Render.
Project structure and Dockerfile
A Dockerfile gives you explicit, version-controlled authority over what happens at build time versus runtime. For PocketBase, the build stage fetches the pre-compiled binary and prepares the filesystem layout. The run stage starts the binary with flags that point to the persistent mount path and bind to the port Render expects.
The --dir flag points PocketBase's data directory to /pb/pb_data, the path where you'll mount the persistent disk. The --http=0.0.0.0:${PORT:-10000} pattern binds to all interfaces on Render's expected port (default 10000). Render injects a PORT environment variable at runtime, and the shell form above keeps PocketBase aligned with that value if you change the port in service settings.
Pin PocketBase to a specific version so builds are deterministic. Check the PocketBase releases page for the current tag before you copy this snippet.
Configuring the Render web service
Render needs three things to run your PocketBase container: the service definition, the port contract, and a persistent disk.
When you create a web service on Render, you connect it to a Git repository containing your Dockerfile. Each git push to the linked branch triggers a new build-and-deploy cycle.
Port binding is the handshake between your service and Render's routing layer. Your process must bind to host 0.0.0.0 (not 127.0.0.1), or Render cannot reach it. The default expected port is 10000, and Render can usually detect another open port if you bind elsewhere. Binding only to localhost is the failure mode that keeps the service from becoming routable.
Health checks determine whether your service is ready to receive traffic. Render's default is a TCP socket probe to one of your service's open ports, which works for PocketBase without modification. Optionally set healthCheckPath: /api/health in render.yaml or the Dashboard for an HTTP health check against PocketBase's health endpoint, which returns 200.
The persistent disk configuration requires a name and a mount path. Set the mount path to /pb/pb_data, the same path passed to PocketBase's --dir flag. You can configure the disk either through the Render Dashboard or declaratively via a render.yaml file:
Persistent disks are billed based on provisioned size. Review Render's pricing page to choose an appropriate size. You can increase a disk's size after creation, but you can't decrease it, so start conservatively.
Making PocketBase see the real client IP
PocketBase determines who is calling it by reading the address on the incoming connection. That works when a browser connects to it directly, and it stops working the moment a proxy sits in between. On Render, one always does: public traffic reaches your container through Cloudflare and Render's load balancer, so the address PocketBase reads belongs to Render's proxy rather than your user. Every request in your application looks like it came from the same client.
PocketBase covers this with a trusted proxy setting, and the setting ships empty. Until you populate it, RealIP() returns the socket address, and three things quietly misbehave:
- The built-in rate limiter keys its buckets on that address, so all traffic shares a single bucket. One caller hammering an endpoint throttles everyone else, and per-IP limits do nothing.
- The superuser IP allowlist cannot tell one caller from another.
- Request and auth logs record the proxy address, which turns any later abuse investigation into guesswork.
Set the trusted proxy header to CF-Connecting-IP. Cloudflare writes that header on every request that reaches a Render web service, and it overwrites whatever the caller sent, so the value is both accurate and outside the caller's control. You can set it from the PocketBase dashboard under Settings > Application, or keep it in version control as a migration:
PocketBase looks for migrations in a pb_migrations directory alongside its data directory, so with --dir=/pb/pb_data the file belongs at /pb/pb_migrations, which is what the COPY line in the Dockerfile above puts there. Keep that directory outside the disk mount path so the image ships it.
Leave useLeftmostIP off. It only affects headers carrying a chain of addresses, and enabling it for X-Forwarded-For reintroduces the problem it looks like it solves. Cloudflare appends to X-Forwarded-For rather than replacing it, so a caller who sends their own copy of the header controls the leftmost entry and can claim any address. PocketBase's source carries the same warning about that field.
To confirm the setting took effect, authenticate as a superuser and call the health endpoint:
For superusers the response includes realIP and possibleProxyHeader. If realIP matches your own address rather than a Cloudflare one, the setting is working.
Extending to a full-stack app: Next.js and PocketBase
The single-service pattern above is the foundation for a common full-stack setup: a Next.js frontend backed by PocketBase. Next.js and PocketBase run as separate services on Render because they're independent runtime processes with different build pipelines, start commands, and resource requirements. Next.js is a Node.js application serving your frontend and API routes. PocketBase is the Go binary you configured above.
On Render, Next.js runs as a public web service with an HTTPS URL. PocketBase typically runs as a private service so its API and admin UI stay off the public internet while Next.js calls it over Render's private network. If you need the admin dashboard reachable from a browser without routing through Next.js, deploy PocketBase as a public web service instead. Note that private services only support Render's default TCP health checks, so the healthCheckPath option from the standalone setup applies only when PocketBase runs as a web service. The request flow is:
The browser talks only to Next.js. Next.js makes server-side requests to PocketBase using PocketBase's internal Render hostname, and PocketBase reads and writes its SQLite database on the attached disk.
Connecting Next.js to PocketBase with environment variables
Environment variables are how Next.js locates PocketBase without hardcoding an address. Store PocketBase's internal host and port in a variable and read it in your server-side code. The variable is named POCKETBASE_HOSTPORT because Render's hostport property returns a combined host:port value (for example pocketbase-service-ab1c:10000):
The same code runs locally or on Render by changing only the variable. Replace any localhost reference with PocketBase's internal hostname (private service) or its public onrender.com URL (public web service).
Client IP when PocketBase is a private service
The CF-Connecting-IP setting from the previous section applies to public web services. Requests to a private service arrive over Render's private network from your Next.js service and never pass through Cloudflare, so that header is absent and PocketBase falls back to the internal address of the Next.js instance.
In this topology Next.js is the service holding the real client IP, so it has to forward the value explicitly:
Then set PocketBase's trusted proxy header to X-Client-IP instead of CF-Connecting-IP. Trusting a header your own code sets is safe here only because a private service is unreachable from the internet. On a public web service, any caller could forge the same header.
Wiring both services with render.yaml
Infrastructure as Code lets you declare both services, their env vars, and the disk in one file:
The Next.js service uses fromService with property: hostport so Render injects PocketBase's internal hostname and port together (for example pocketbase-service-ab1c:10000).
The PocketBase service uses a community-maintained container image, pinned to ghcr.io/muchobien/pocketbase:0.39.6 (avoid :latest in production). Because this image's tags are maintained independently of PocketBase and can lag official releases, confirm the tag actually exists in the registry before deploying, or pin to an image digest. A missing tag causes an Image Pull Failed. This pinned version also won't automatically match the v0.39.6 you build from the Dockerfile earlier, and image-backed services don't auto-redeploy when a new image is pushed to the tag, so you trigger deploys manually from the Dashboard or a deploy hook.
The disk is mounted at /data, and PocketBase writes under /data/pb_data. This differs from the Dockerfile setup earlier, which mounts at /pb/pb_data, and either path works as long as --dir sits inside the mount path. Note that PocketBase resolves its migrations directory relative to the data directory, so here it looks in /data/pb_migrations, a path on the disk rather than in the image. Pass --migrationsDir explicitly if you want to ship migrations with this image.
One subtlety: this image ships its own ENTRYPOINT script that already invokes the pocketbase binary, and Render's dockerCommand overrides the image's CMD rather than its ENTRYPOINT, so the command is passed as arguments to that script. That's why it starts with serve rather than pocketbase serve. The entrypoint prepends the binary for you, and adding pocketbase would run pocketbase pocketbase serve and fail to start. If you'd rather not depend on the wrapper script, use the native Dockerfile from the project structure section above. Make sure the --dir path stays inside the disk mount path, or PocketBase writes to the ephemeral filesystem. If you use a monorepo, set the root directory for the Next.js service.
The deployment pattern in summary
The pattern reduces to five architectural decisions: package the binary in a Dockerfile, bind to Render's expected port, mount a persistent disk at PocketBase's data directory, tell PocketBase which header carries the client IP, and manage the rest of the configuration through environment variables. Each decision maps to a general principle (containerized build control, port-based service contracts, persistent storage for stateful workloads, proxy-aware request handling, and externalized configuration) that extends to any service you deploy on this platform.
This pattern fits internal tools and production workloads with modest traffic. Adding a Next.js frontend extends it to two coordinated services connected over the private network, and the same principles still apply. As your requirements evolve, you can add external object storage for uploads or migrate to a multi-service stack (for example an API backed by Render Postgres) when PocketBase's single-instance model no longer fits.