Self-host a backend in one binary: VPS, Docker, Kubernetes
Self-host your backend as one binary plus Postgres. Run inz serve on a server, in Docker Compose, or on Kubernetes with the Helm chart, all from one YAML file.
To self-host your backend with instancez, you run one binary, inz serve, pointed at a Postgres database. It serves your tables, auth, storage and functions from one instancez.yaml. Run it straight on a server, in a two-service Docker Compose file, or on Kubernetes with the Helm chart. Your frontend keeps using @supabase/supabase-js, and only the URL and key change.
Below are all three setups. We ran every command and config in this post against a real Postgres first, except the Kubernetes install, which we rendered with helm template and checked with inz validate.
What does self-hosting Supabase involve?
Supabase’s official Docker Compose file runs 11 services for one project:
studio, api-gw (Envoy), auth, rest (PostgREST), realtime, storage, imgproxy, meta, functions, db (Postgres) and supavisor.
Each one has its own image, its own configuration and its own upgrade cadence. The self-hosted edition is community-supported, and you set up SMTP, secrets and backups yourself.
That stack exists for good reasons: it covers the whole Supabase feature set, Realtime and Studio included. The cost is operational: more containers to watch, and more images to bump together on every upgrade.
What does self-hosting instancez involve?
One process and a Postgres database. The inz binary serves the REST API, auth, storage, SQL functions, JavaScript functions and an admin dashboard. It’s the same Apache-2.0 binary we run for instancez Cloud, not a cut-down community edition.
| Self-hosted Supabase | Self-hosted instancez | |
|---|---|---|
| Moving parts | 11 services in Docker Compose | inz plus Postgres |
| Where it runs | Docker | Bare binary, Docker, Kubernetes, AWS Lambda |
| Schema | SQL migrations you apply | One instancez.yaml, diffed and applied on start |
| Client | @supabase/supabase-js | @supabase/supabase-js, unchanged |
| Upgrades | Coordinate service versions | Replace one binary or one image tag |
The whole backend lives in one file. If you haven’t seen it yet, one YAML file backend walks through tables, RLS, auth and storage in instancez.yaml. For this post, we use the project that inz init scaffolds: a profiles table, a todos table with row-level security, an avatars storage bucket and one JavaScript function.
Before deploying anywhere, check the file offline:
inz validate
✓ Schema valid
Do code functions need anything extra?
Yes, one build step. inz serve never runs npm. If your instancez.yaml has a functions: block, build a bundle first. It packs the YAML and functions/ (with vendored dependencies) into one archive:
cd functions && npm install && cd .. # once, to create package-lock.json
inz bundle --output bundle.tar.gz
✓ Schema valid
✓ Bundle written: bundle.tar.gz
Then start the server with --bundle ./bundle.tar.gz instead of pointing it at the YAML. The bundle carries the config, so it’s the only file the server needs. Node.js has to be on the PATH wherever functions run. The Docker image already includes it.
No functions? Skip the bundle and run inz serve --migrate next to your instancez.yaml.
How do you run it on a server?
Install the binary:
curl -fsSL https://get.instancez.ai | sh
inz version
The script installs to ~/.local/bin. You can also download a release from GitHub and put it on your PATH.
Next to your instancez.yaml, create .production.env. Don’t commit it:
INSTANCEZ_DATABASE_URL=postgres://postgres:password@localhost:5432/mydb
INSTANCEZ_PUBLISHABLE_KEY=inz_publishable_your-key
INSTANCEZ_SECRET_KEY=inz_secret_your-key
INSTANCEZ_DATABASE_URL is a superuser DSN. On first start, instancez creates the roles it needs (instancez_owner, authenticator, anon, authenticated, service_role). If you can’t hand it a superuser, set INSTANCEZ_OWNER_DATABASE_URL and INSTANCEZ_AUTH_DATABASE_URL for pre-provisioned roles instead. The environment variable reference has the full list.
Start the server and check it:
inz serve --migrate
curl localhost:8080/ready
{"status":"ok"}
inz serve reads .production.env by itself when it loads instancez.yaml from disk. It skips that file in bundle mode, so export the variables first:
set -a; . ./.production.env; set +a
inz serve --migrate --bundle ./bundle.tar.gz
A few notes from running it:
--migratediffsinstancez.yamlagainst the database and applies the change on start. Migrations take a Postgres advisory lock and a lock timeout (--migrate-lock-timeout, default 5s). If the lock times out, the server keeps the last applied config.inz serverefuses to drop a table or column unless you start it with--allow-destructive. The error names what would be lost.- Put Nginx or Caddy in front for TLS and point the load balancer health check at
/ready. It returns 503 when Postgres is unreachable.
The self-hosted guide has the same steps.
How do you run it with Docker Compose?
Two services: Postgres and instancez. Images are multi-arch (amd64 and arm64) and there is no latest tag, so pin a version from the releases page. Note the tag has no leading v.
compose.yaml:
services:
postgres:
image: postgres:17-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: instancez
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 2s
timeout: 3s
retries: 10
instancez:
image: ghcr.io/instancez/instancez:0.0.10
ports:
- "8080:8080"
environment:
INSTANCEZ_DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD}@postgres:5432/instancez?sslmode=disable
INSTANCEZ_PUBLISHABLE_KEY: ${PUBLISHABLE_KEY}
INSTANCEZ_SECRET_KEY: ${SECRET_KEY}
volumes:
- ./bundle.tar.gz:/app/bundle.tar.gz
- ./uploads:/app/uploads
command: ["inz", "serve", "--migrate", "--bundle", "/app/bundle.tar.gz"]
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
volumes:
pgdata:
.env next to it (never commit this file):
POSTGRES_PASSWORD=change-me
PUBLISHABLE_KEY=inz_publishable_your-key
SECRET_KEY=inz_secret_your-key
The container runs as UID 1000, so create the uploads directory before the first start. Otherwise Docker creates it as root and storage can’t write to it:
mkdir -p uploads && sudo chown 1000:1000 uploads
docker compose up -d
No functions? Mount ./instancez.yaml:/app/instancez.yaml instead of the bundle and drop the --bundle arguments. To keep files off the host, switch providers.storage to an S3-compatible bucket (storage docs).
Upgrading means changing one image tag and running docker compose up -d again. The Docker guide also has a standalone docker run version.
Does supabase-js work against it unchanged?
Yes. This is trimmed from the script we ran against both the plain server and the Compose setups:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('http://localhost:8080', 'inz_publishable_your-key')
await supabase.auth.signUp({ email: '[email protected]', password: 'correct-horse-battery' })
const { data: { user } } = await supabase.auth.getUser()
await supabase.from('todos').insert({ title: 'ship it', user_id: user.id })
const { data } = await supabase.from('todos').select('title,status')
// [{ title: 'ship it', status: 'pending' }]
const { data: fn } = await supabase.functions.invoke('todos', { method: 'GET' })
await supabase.storage.from('avatars').upload('me.png', pngBytes, { contentType: 'image/png' })
An anonymous client gets [] from the same todos query, because the RLS policy only matches user_id = auth.uid(). Coming from a hosted Supabase project, the migration guide covers moving the data, and switching from Supabase covers the code side.
How do you deploy it on Kubernetes?
The repo ships a Helm chart at helm/instancez. By default it deploys instancez plus a bundled Postgres StatefulSet. It generates the publishable key, the secret key and the Postgres password on first install and keeps them across upgrades.
values.yaml:
image:
tag: "0.0.10"
config: |
version: 1
project:
name: my-app
tables:
notes:
rls_enabled: true
fields:
- name: id
type: bigserial
primary_key: true
- name: user_id
foreign_key:
references: auth.users.id
on_delete: cascade
- name: body
type: text
required: true
rls:
- operations: [select, insert, update, delete]
using: "user_id = auth.uid()"
with_check: "user_id = auth.uid()"
ingress:
enabled: true
className: nginx
host: api.example.com
The config value is your instancez.yaml. The chart writes it to a ConfigMap and mounts it at /app/instancez.yaml.
git clone --depth 1 https://github.com/instancez/instancez.git
helm install instancez ./instancez/helm/instancez -f values.yaml
kubectl get secret instancez -o jsonpath='{.data.publishableKey}' | base64 -d
The chart wires the liveness probe to /live and the readiness probe to /ready, so a pod only takes traffic once Postgres is reachable. To use a managed Postgres instead of the bundled one:
helm install instancez ./instancez/helm/instancez -f values.yaml \
--set postgres.enabled=false \
--set externalPostgres.url="postgres://superuser:pass@your-db:5432/instancez"
That DSN needs CREATEROLE CREATEDB BYPASSRLS, because instancez provisions its roles on startup. The Kubernetes guide covers TLS on the Ingress and password rotation.
What do you give up compared to self-hosted Supabase?
Self-hosted Supabase has features instancez doesn’t have yet:
- No Realtime or websocket subscriptions yet. Poll or refetch.
- No phone or SMS login. OAuth covers Google, GitHub and Apple.
- Image resize handles JPEG and PNG, not WebP or AVIF.
- Functions run on Node.js, so Deno Edge Functions need porting.
The full comparison has the rest, and Supabase alternatives puts both next to the other options. Want the same binary without a server at all? Running it on AWS Lambda is the next post. Still on your laptop? Local Supabase without Docker uses inz dev --embedded-pg, which needs no Postgres install.
FAQ
Do I need Docker to self-host instancez?
No. inz is a single binary. Docker and Kubernetes are options, not requirements. You do need a Postgres database.
Which Postgres versions work?
We tested these examples on Postgres 17 (postgres:17-alpine), the image our Compose and Helm examples use.
Is the self-hosted version different from instancez Cloud?
No. Cloud runs the same open-source runtime. The same instancez.yaml deploys to Cloud with inz cloud deploy.
How do I upgrade on a VPS or with Docker?
Replace the binary or bump the image tag, then restart. With --migrate, schema changes in instancez.yaml are applied on start. Dropping a table or column also needs --allow-destructive.