# Gebos — vendored Supabase compose stack. # # Vendored from the official self-hosting bundle # https://github.com/supabase/supabase/blob/master/docker/docker-compose.yml # with the deliberate deviations documented in ./README.md: # # * no `db` service — Postgres runs on db-host; every service connects to # ${POSTGRES_HOST}:${POSTGRES_PORT} and the `depends_on: db` blocks are # dropped # * no realtime / storage / imgproxy / functions / supavisor / analytics — # the pilot needs auth + rest + studio only; re-vendor from upstream when # one of them becomes needed # * Studio binds to ${STUDIO_BIND}:3000 (loopback; reach it via SSH tunnel) # * meta connects as supabase_admin, not `postgres`: upstream's `postgres` # is its cluster superuser, but on db-host that role carries a different # password. gebos-postgres-passwords makes supabase_admin SUPERUSER to # match the upstream image's semantics. # # Where variables come from (see nix/modules/gebos-supabase.nix): # * POSTGRES_HOST / POSTGRES_PORT / POSTGRES_DB / STUDIO_BIND — systemd # unit `environment` (non-secret deployment config, nix-managed) # * POSTGRES_PASSWORD / JWT_SECRET / ANON_KEY / SERVICE_ROLE_KEY — # sops-rendered EnvironmentFile (secrets) # * gebos constants (public URLs, org name) — hardcoded right here name: gebos-supabase services: studio: container_name: supabase-studio image: supabase/studio:2026.07.07-sha-a6a04f2 restart: unless-stopped ports: # Never 0.0.0.0 — Studio performs no authentication of its own in this # topology (the upstream basic-auth lives on Kong's dashboard route, # which we do not expose). Access = SSH tunnel to app-host. - ${STUDIO_BIND}:3000:3000/tcp healthcheck: test: [ "CMD-SHELL", "node -e \"fetch('http://localhost:3000/api/platform/profile').then((r) => {if (r.status !== 200) throw new Error(r.status)})\"" ] timeout: 10s interval: 5s retries: 3 start_period: 20s environment: HOSTNAME: "0.0.0.0" STUDIO_PG_META_URL: http://meta:8080 POSTGRES_HOST: ${POSTGRES_HOST} POSTGRES_PORT: ${POSTGRES_PORT} POSTGRES_DB: ${POSTGRES_DB} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_USER_READ_WRITE: supabase_admin PGRST_DB_SCHEMAS: public DEFAULT_ORGANIZATION_NAME: Gebos DEFAULT_PROJECT_NAME: Gebos SUPABASE_URL: http://kong:8000 SUPABASE_PUBLIC_URL: https://api.gebos.online SUPABASE_ANON_KEY: ${ANON_KEY} SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY} AUTH_JWT_SECRET: ${JWT_SECRET} # No Logflare/analytics stack in this deployment. ENABLED_FEATURES_LOGS_ALL: "false" kong: container_name: supabase-kong image: kong/kong:3.9.1 restart: unless-stopped ports: - 127.0.0.1:8000:8000/tcp # Caddy proxies api.gebos.online here healthcheck: test: ["CMD", "kong", "health"] interval: 5s timeout: 5s retries: 5 volumes: # The entrypoint substitutes $ENV_VARS in temp.yml and writes the result # to KONG_DECLARATIVE_CONFIG (Kong has no native env interpolation). - ./kong.yml:/home/kong/temp.yml:ro - ./kong-entrypoint.sh:/home/kong/kong-entrypoint.sh:ro environment: KONG_DATABASE: "off" KONG_DECLARATIVE_CONFIG: /usr/local/kong/kong.yml KONG_ROUTER_FLAVOR: expressions # https://github.com/supabase/cli/issues/14 KONG_DNS_ORDER: LAST,A,CNAME KONG_DNS_NOT_FOUND_TTL: 1 KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth,request-termination,ip-restriction,post-function KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k KONG_PROXY_ACCESS_LOG: /dev/stdout combined SUPABASE_ANON_KEY: ${ANON_KEY} SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY} # Opaque sb_* API keys — not used yet. Empty values make the entrypoint # fall back to legacy apikey pass-through and strip the empty # credentials from the declarative config. SUPABASE_PUBLISHABLE_KEY: ${SUPABASE_PUBLISHABLE_KEY:-} SUPABASE_SECRET_KEY: ${SUPABASE_SECRET_KEY:-} ANON_KEY_ASYMMETRIC: ${ANON_KEY_ASYMMETRIC:-} SERVICE_ROLE_KEY_ASYMMETRIC: ${SERVICE_ROLE_KEY_ASYMMETRIC:-} entrypoint: ["/bin/sh", "/home/kong/kong-entrypoint.sh"] auth: container_name: supabase-auth image: supabase/gotrue:v2.189.0 restart: unless-stopped healthcheck: test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:9999/health" ] timeout: 5s interval: 5s retries: 3 environment: GOTRUE_API_HOST: 0.0.0.0 GOTRUE_API_PORT: 9999 API_EXTERNAL_URL: https://api.gebos.online GOTRUE_DB_DRIVER: postgres # GoTrue owns the auth schema and runs its own migrations in it on # startup — supabase_auth_admin's ownership of that schema is enforced # by the gebos-postgres-passwords oneshot on db-host. GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} GOTRUE_SITE_URL: https://app.gebos.online GOTRUE_URI_ALLOW_LIST: "" # Tenants are invited by an admin (magic link), never self-signed-up. GOTRUE_DISABLE_SIGNUP: "true" GOTRUE_JWT_ADMIN_ROLES: service_role GOTRUE_JWT_AUD: authenticated GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated GOTRUE_JWT_EXP: "3600" GOTRUE_JWT_SECRET: ${JWT_SECRET} GOTRUE_JWT_ISSUER: https://api.gebos.online GOTRUE_EXTERNAL_EMAIL_ENABLED: "true" GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: "false" GOTRUE_MAILER_AUTOCONFIRM: "false" # TODO: no SMTP provider configured yet. Magic links (UVI-NFR-08, # passwordless-only) cannot send until these are filled in — add the # SMTP secrets to nix/secrets/secrets.yaml and wire them through # gebos-secrets.nix like the other supabase keys. GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_ADMIN_EMAIL:-} GOTRUE_SMTP_HOST: ${SMTP_HOST:-} GOTRUE_SMTP_PORT: ${SMTP_PORT:-587} GOTRUE_SMTP_USER: ${SMTP_USER:-} GOTRUE_SMTP_PASS: ${SMTP_PASS:-} GOTRUE_SMTP_SENDER_NAME: ${SMTP_SENDER_NAME:-Gebos} GOTRUE_MAILER_URLPATHS_INVITE: /auth/v1/verify GOTRUE_MAILER_URLPATHS_CONFIRMATION: /auth/v1/verify GOTRUE_MAILER_URLPATHS_RECOVERY: /auth/v1/verify GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: /auth/v1/verify GOTRUE_EXTERNAL_PHONE_ENABLED: "false" GOTRUE_SMS_AUTOCONFIRM: "false" rest: container_name: supabase-rest image: postgrest/postgrest:v14.12 restart: unless-stopped healthcheck: test: [ "CMD", "postgrest", "--ready" ] interval: 5s timeout: 5s retries: 3 environment: PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} PGRST_DB_SCHEMAS: public PGRST_DB_MAX_ROWS: 1000 PGRST_DB_EXTRA_SEARCH_PATH: public PGRST_DB_ANON_ROLE: anon PGRST_ADMIN_SERVER_PORT: 3001 PGRST_ADMIN_SERVER_HOST: localhost PGRST_JWT_SECRET: ${JWT_SECRET} PGRST_DB_USE_LEGACY_GUCS: "false" PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET} PGRST_APP_SETTINGS_JWT_EXP: "3600" command: [ "postgrest" ] meta: container_name: supabase-meta image: supabase/postgres-meta:v0.96.6 restart: unless-stopped environment: PG_META_PORT: 8080 PG_META_DB_HOST: ${POSTGRES_HOST} PG_META_DB_PORT: ${POSTGRES_PORT} PG_META_DB_NAME: ${POSTGRES_DB} PG_META_DB_USER: supabase_admin PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD}