# Deploy Supabase with the supabase-selfhost template

Source: https://docs.quake.ai/resources/deployments/deploy-supabase-selfhost-template
Markdown: https://docs.quake.ai/resources/deployments/deploy-supabase-selfhost-template.md

---

# Deploy Supabase with the supabase-selfhost template

Stand up the full self-hosted [Supabase](https://supabase.com/docs/guides/self-hosting) backend on a single Quake AI instance using the [validated OpenTofu template](/docs/platform/validation#how-infrastructure-templates-are-checked) `supabase-selfhost`. You apply the template, read the generated credentials, point a domain at the host and serve it over HTTPS, set the public URL, sign in to Studio, create a table, and query it through the REST API.

Supabase keeps your application's database, auth, storage, and realtime layer on infrastructure you own. You run it yourself; this is a self-hosted backend you operate, not the managed Supabase cloud.

<Figure size="md" caption="What you'll build: the full Supabase stack on a single instance, reached over HTTPS through a Caddy reverse proxy, with Postgres and uploads on an attached volume">

```d2
direction: right

client: App client {shape: person}
fip: Floating IP
instance: Ubuntu instance {
  caddy: Caddy\nreverse proxy
  kong: Kong\nAPI gateway
  studio: Studio
  auth: Auth
  rest: REST
  realtime: Realtime
  storage: Storage
  db: Postgres
  caddy -> kong: proxies 443 to 8000
  kong -> auth
  kong -> rest
  kong -> realtime
  kong -> storage
  rest -> db
  auth -> db
}

client -> fip: HTTPS
fip -> instance.caddy
```

</Figure>

<PricingCompanion
  components={[
    { kind: "template", slug: "supabase-selfhost", required: true },
  ]}
/>

## Prerequisites

You need:

- OpenTofu 1.6.0 or later (or Terraform 1.6.0 or later) installed locally.
- Your OpenStack credentials sourced into the shell (`source openrc.sh`). See [the OpenStack CLI guide](/docs/tools/openstack-cli).
- An SSH keypair that already exists in your project. Record its name for the `key_name` variable.
- A copy of the `supabase-selfhost` template directory from [the template reference page](/resources/iac-templates/supabase-selfhost).
- A domain you can point at the instance. Supabase Auth builds callback URLs from a stable public URL.

## Step 1: Apply the template

Copy the template's example variables file and set `key_name`:

```bash
cp terraform.tfvars.example terraform.tfvars
```

```hcl
key_name = "YOUR_KEY_NAME"
```

Initialize, preview, and apply:

```bash
tofu init
tofu plan
tofu apply
```

OpenTofu provisions a private network, a router, a security group, a block volume mounted at `/opt/supabase`, an instance, and a floating IP. On first boot, cloud-init installs Docker Engine, clones the Supabase stack, generates every secret into `/opt/supabase/.env`, and runs `docker compose up -d`.

Read the outputs and record `floating_ip`, `api_url`, and `studio_url`:

```bash
tofu output
```

## Step 2: Read the generated credentials

Every secret was generated on the instance. SSH in and read them once:

```bash
ssh ubuntu@YOUR_FLOATING_IP
sudo cat /opt/supabase/.env
```

Record `ANON_KEY`, `SERVICE_ROLE_KEY`, `DASHBOARD_USERNAME`, and `DASHBOARD_PASSWORD`, and store them in your secret manager. The `anon` key is safe to ship in client code; the `service_role` key bypasses row-level security and stays server-side only.

The stack takes a few minutes to pull and start on first boot. Confirm the containers are healthy:

```bash
cd /opt/supabase
sudo docker compose ps
```

## Step 3: Point a domain at the host and serve HTTPS with Caddy

Supabase Auth builds absolute callback links from its public URL, so it needs a domain with TLS before it serves production traffic.

1. Create a DNS **A record** for your domain (for example `api.example.com`) pointing at `YOUR_FLOATING_IP`. Follow [How to point a domain at a Quake AI resource](/docs/network/how-to/point-domain-to-quake-ai). Wait until it resolves:

```bash
dig +short api.example.com
```

2. On the instance, create `/opt/supabase/Caddyfile`:

```text
api.example.com {
  reverse_proxy 127.0.0.1:8000
}
```

3. Add Caddy to `/opt/supabase/docker-compose.yml`:

```yaml
services:
  caddy:
    image: caddy:2
    restart: unless-stopped
    network_mode: host
    volumes:
      - /opt/supabase/Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
volumes:
  caddy_data:
```

For background on certificates, see [How to issue and auto-renew a TLS certificate with Let's Encrypt](/docs/network/how-to/lets-encrypt-certificate).

## Step 4: Set the public URL and restart

Edit `/opt/supabase/.env` and set the three URL variables to your HTTPS address:

```text
API_EXTERNAL_URL=https://api.example.com
SUPABASE_PUBLIC_URL=https://api.example.com
SITE_URL=https://api.example.com
```

Restart the stack so the services pick up the new URLs:

```bash
cd /opt/supabase
sudo docker compose up -d
```



`ANON_KEY`, `SERVICE_ROLE_KEY`, and the Postgres password live in `/opt/supabase/.env` on the instance. Keep them there and in your secret manager, never in `terraform.tfvars` or the repo.



## Step 5: Sign in to Studio and create a table

1. Open Studio. Over the reverse proxy, route a second hostname (for example `studio.example.com`) to port 3000, or tunnel to it over SSH for setup: `ssh -L 3000:localhost:3000 ubuntu@YOUR_FLOATING_IP` and open `http://localhost:3000`.
2. Sign in with `DASHBOARD_USERNAME` and `DASHBOARD_PASSWORD` from `/opt/supabase/.env`.
3. Open the **Table Editor**, create a table named `notes` with a `text` column called `body`, and insert a row.

## Step 6: Query the REST API

Supabase generates a REST endpoint for every table. Query `notes` through the API gateway with your `anon` key:

```bash
curl "https://api.example.com/rest/v1/notes?select=*" \
  -H "apikey: YOUR_ANON_KEY" \
  -H "Authorization: Bearer YOUR_ANON_KEY"
```

The row you inserted in Studio comes back as JSON. Point a Supabase client library at `https://api.example.com` with the `anon` key to build against the same API from your application.

## What you built

- **Applied the `supabase-selfhost` template** to provision a network, security group, data volume, instance, and floating IP, with the full Supabase stack started by cloud-init
- **Read the generated credentials** from `/opt/supabase/.env`
- **Served the stack over HTTPS** by pointing a domain at the floating IP and routing it through a Caddy reverse proxy
- **Set the public URL** so Auth callbacks resolve
- **Created a table in Studio and queried it** through the auto-generated REST API

## Scope of this deployment

This template runs a single-VM Supabase host, not the managed Supabase cloud. The instance is CPU-only and runs in one region, and it runs every Supabase service as a container on one host. You operate the instance, Docker, Postgres, and the data volume yourself: back them up, patch them, and snapshot the volume before you resize or rebuild. For high availability, move Postgres onto its own instance and run the stateless services behind a load balancer.

## Next steps

- [Supabase self-host template](/resources/iac-templates/supabase-selfhost): the template reference, parameters, and resource map
- [Self-managed PostgreSQL template](/resources/iac-templates/self-managed-postgres): the database to point at when you outgrow the bundled one
- [How to store application secrets and inject them at runtime](/docs/security/how-to/inject-app-secrets): manage the API keys and database password
- [Security hardening checklist](/docs/security/hardening-checklist): tighten SSH access and exposure before you serve real traffic

## Clean up

When you no longer need the deployment, destroy everything the template created:

```bash
tofu destroy
```

Then remove the DNS A records you created in step 3. Because Postgres, storage uploads, and the analytics data all live on the instance and its attached volume, `tofu destroy` removes them along with the infrastructure. Export any data you want to keep first with `pg_dump` against the database.
