nixos/hosts/web-arm/modules/supabase/default.nix

452 lines
15 KiB
Nix

{ config, lib, pkgs, ... }:
let
kongEntrypoint = pkgs.writeTextFile {
name = "kong-entrypoint.sh";
executable = true;
text = builtins.readFile ./kong-entrypoint.sh;
};
envGenerateScript = pkgs.writeShellScript "supabase-env-generate"
(builtins.readFile ./env-generate.sh);
# Common extra options for all containers to join the supabase network
supabaseNet = [ "--network=supabase-net" ];
in
{
# --- SOPS secret ---
sops.secrets.supabase-env = { };
# --- Persistent data directories ---
systemd.tmpfiles.rules = [
"d /var/lib/supabase/db/data 0700 root root -"
"d /var/lib/supabase/storage 0755 root root -"
"d /var/lib/supabase/functions 0755 root root -"
"d /var/lib/supabase/snippets 0755 root root -"
];
# --- Systemd services: network, env generation, and container ordering ---
systemd.services =
let
containerNames = [
"supabase-db"
"supabase-analytics"
"supabase-auth"
"supabase-rest"
"supabase-realtime"
"supabase-storage"
"supabase-imgproxy"
"supabase-meta"
"supabase-studio"
"supabase-kong"
"supabase-vector"
"supabase-pooler"
"supabase-functions"
];
mkContainerDeps = name: {
"podman-${name}" = {
after = [ "init-supabase-network.service" "supabase-env-generate.service" ];
requires = [ "init-supabase-network.service" "supabase-env-generate.service" ];
};
};
in
lib.mkMerge (map mkContainerDeps containerNames ++ [
{
init-supabase-network = {
description = "Create supabase-net Podman network";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
# '-' prefix tells systemd to ignore non-zero exit (network may already exist)
ExecStart = "-${pkgs.podman}/bin/podman network create supabase-net";
};
};
supabase-env-generate = {
description = "Generate Supabase per-container env files from SOPS secrets";
wantedBy = [ "multi-user.target" ];
path = [ pkgs.jq ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${envGenerateScript} ${config.sops.secrets.supabase-env.path}";
};
};
# Seed the edge-runtime's bootstrap `main` function. The container's
# entrypoint requires `/home/deno/functions/main/index.ts` to exist;
# without it edge-runtime fails with "could not find an appropriate
# entrypoint". Re-seed on every activation so updates to the bootstrap
# are picked up, while leaving user-authored functions untouched.
supabase-functions-seed = {
description = "Seed Supabase edge-functions main bootstrap";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
install -d -m 0755 /var/lib/supabase/functions/main
install -m 0644 ${./functions/main/index.ts} /var/lib/supabase/functions/main/index.ts
'';
};
podman-supabase-functions = {
after = [ "supabase-functions-seed.service" ];
requires = [ "supabase-functions-seed.service" ];
};
}
]);
# --- Containers ---
virtualisation.oci-containers.containers = {
# 1. PostgreSQL
supabase-db = {
image = "supabase/postgres:15.8.1.085";
environment = {
POSTGRES_HOST = "/var/run/postgresql";
PGPORT = "5432";
POSTGRES_PORT = "5432";
PGDATABASE = "postgres";
POSTGRES_DB = "postgres";
JWT_EXP = "3600";
};
environmentFiles = [ "/run/supabase/db.env" ];
volumes = [
"/var/lib/supabase/db/data:/var/lib/postgresql/data"
"${./sql/_supabase.sql}:/docker-entrypoint-initdb.d/migrations/97-_supabase.sql:ro"
"${./sql/realtime.sql}:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:ro"
"${./sql/logs.sql}:/docker-entrypoint-initdb.d/migrations/99-logs.sql:ro"
"${./sql/pooler.sql}:/docker-entrypoint-initdb.d/migrations/99-pooler.sql:ro"
"${./sql/webhooks.sql}:/docker-entrypoint-initdb.d/init-scripts/98-webhooks.sql:ro"
"${./sql/roles.sql}:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:ro"
"${./sql/jwt.sql}:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:ro"
"supabase-db-config:/etc/postgresql-custom"
];
cmd = [
"postgres"
"-c" "config_file=/etc/postgresql/postgresql.conf"
"-c" "log_min_messages=fatal"
];
extraOptions = supabaseNet ++ [
"--network-alias=db"
"--shm-size=2g"
];
};
# 2. Analytics (Logflare)
supabase-analytics = {
image = "supabase/logflare:1.31.2";
dependsOn = [ "supabase-db" ];
environment = {
LOGFLARE_NODE_HOST = "127.0.0.1";
DB_USERNAME = "supabase_admin";
DB_DATABASE = "_supabase";
DB_HOSTNAME = "db";
DB_PORT = "5432";
DB_SCHEMA = "_analytics";
LOGFLARE_SINGLE_TENANT = "true";
LOGFLARE_SUPABASE_MODE = "true";
POSTGRES_BACKEND_SCHEMA = "_analytics";
LOGFLARE_FEATURE_FLAG_OVERRIDE = "multibackend=true";
};
environmentFiles = [ "/run/supabase/analytics.env" ];
extraOptions = supabaseNet ++ [
"--network-alias=analytics"
];
};
# 3. Auth (GoTrue)
supabase-auth = {
image = "supabase/gotrue:v2.186.0";
dependsOn = [ "supabase-db" "supabase-analytics" ];
environment = {
GOTRUE_API_HOST = "0.0.0.0";
GOTRUE_API_PORT = "9999";
API_EXTERNAL_URL = "https://supabase.cloonar.com";
GOTRUE_DB_DRIVER = "postgres";
GOTRUE_SITE_URL = "https://supabase.cloonar.com";
GOTRUE_URI_ALLOW_LIST = "";
GOTRUE_DISABLE_SIGNUP = "false";
GOTRUE_JWT_ADMIN_ROLES = "service_role";
GOTRUE_JWT_AUD = "authenticated";
GOTRUE_JWT_DEFAULT_GROUP_NAME = "authenticated";
GOTRUE_JWT_EXP = "3600";
GOTRUE_EXTERNAL_EMAIL_ENABLED = "true";
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED = "false";
GOTRUE_MAILER_AUTOCONFIRM = "true";
GOTRUE_SMTP_ADMIN_EMAIL = "admin@cloonar.com";
GOTRUE_SMTP_HOST = "supabase-mail";
GOTRUE_SMTP_PORT = "2500";
GOTRUE_SMTP_USER = "";
GOTRUE_SMTP_PASS = "";
GOTRUE_SMTP_SENDER_NAME = "Supabase";
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";
};
environmentFiles = [ "/run/supabase/auth.env" ];
extraOptions = supabaseNet ++ [
"--network-alias=auth"
];
};
# 4. REST (PostgREST)
supabase-rest = {
image = "postgrest/postgrest:v14.6";
dependsOn = [ "supabase-db" ];
environment = {
PGRST_DB_SCHEMAS = "public,storage,graphql_public";
PGRST_DB_MAX_ROWS = "1000";
PGRST_DB_EXTRA_SEARCH_PATH = "public";
PGRST_DB_ANON_ROLE = "anon";
PGRST_DB_USE_LEGACY_GUCS = "false";
PGRST_APP_SETTINGS_JWT_EXP = "3600";
};
environmentFiles = [ "/run/supabase/rest.env" ];
cmd = [ "postgrest" ];
extraOptions = supabaseNet ++ [
"--network-alias=rest"
];
};
# 5. Realtime
supabase-realtime = {
image = "supabase/realtime:v2.76.5";
dependsOn = [ "supabase-db" ];
environment = {
PORT = "4000";
DB_HOST = "db";
DB_PORT = "5432";
DB_USER = "supabase_admin";
DB_NAME = "postgres";
DB_AFTER_CONNECT_QUERY = "SET search_path TO _realtime";
DB_ENC_KEY = "supabaserealtime";
ERL_AFLAGS = "-proto_dist inet_tcp";
DNS_NODES = "''";
RLIMIT_NOFILE = "10000";
APP_NAME = "realtime";
SEED_SELF_HOST = "true";
RUN_JANITOR = "true";
DISABLE_HEALTHCHECK_LOGGING = "true";
};
environmentFiles = [ "/run/supabase/realtime.env" ];
extraOptions = supabaseNet ++ [
# Hostname must be realtime-dev.supabase-realtime for tenant ID parsing
"--hostname=realtime-dev.supabase-realtime"
"--network-alias=realtime-dev.supabase-realtime"
];
};
# 6. Storage
supabase-storage = {
image = "supabase/storage-api:v1.44.2";
dependsOn = [ "supabase-db" "supabase-rest" "supabase-imgproxy" ];
environment = {
POSTGREST_URL = "http://rest:3000";
STORAGE_PUBLIC_URL = "https://supabase.cloonar.com";
REQUEST_ALLOW_X_FORWARDED_PATH = "true";
FILE_SIZE_LIMIT = "52428800";
STORAGE_BACKEND = "file";
GLOBAL_S3_BUCKET = "stub";
FILE_STORAGE_BACKEND_PATH = "/var/lib/storage";
TENANT_ID = "stub";
REGION = "stub";
ENABLE_IMAGE_TRANSFORMATION = "true";
IMGPROXY_URL = "http://imgproxy:5001";
};
environmentFiles = [ "/run/supabase/storage.env" ];
volumes = [
"/var/lib/supabase/storage:/var/lib/storage"
];
extraOptions = supabaseNet ++ [
"--network-alias=storage"
];
};
# 7. Imgproxy
supabase-imgproxy = {
image = "darthsim/imgproxy:v3.30.1";
environment = {
IMGPROXY_BIND = ":5001";
IMGPROXY_LOCAL_FILESYSTEM_ROOT = "/";
IMGPROXY_USE_ETAG = "true";
IMGPROXY_AUTO_WEBP = "true";
IMGPROXY_MAX_SRC_RESOLUTION = "16.8";
};
volumes = [
"/var/lib/supabase/storage:/var/lib/storage"
];
extraOptions = supabaseNet ++ [
"--network-alias=imgproxy"
];
};
# 8. Meta (pg-meta)
supabase-meta = {
image = "supabase/postgres-meta:v0.95.2";
dependsOn = [ "supabase-db" ];
environment = {
PG_META_PORT = "8080";
PG_META_DB_HOST = "db";
PG_META_DB_PORT = "5432";
PG_META_DB_NAME = "postgres";
PG_META_DB_USER = "supabase_admin";
};
environmentFiles = [ "/run/supabase/meta.env" ];
extraOptions = supabaseNet ++ [
"--network-alias=meta"
];
};
# 9. Studio
supabase-studio = {
image = "supabase/studio:2026.03.16-sha-5528817";
dependsOn = [ "supabase-analytics" ];
environment = {
HOSTNAME = "::";
STUDIO_PG_META_URL = "http://meta:8080";
POSTGRES_PORT = "5432";
POSTGRES_HOST = "db";
POSTGRES_DB = "postgres";
PGRST_DB_SCHEMAS = "public,storage,graphql_public";
PGRST_DB_MAX_ROWS = "1000";
PGRST_DB_EXTRA_SEARCH_PATH = "public";
DEFAULT_ORGANIZATION_NAME = "Default Organization";
DEFAULT_PROJECT_NAME = "Default Project";
SUPABASE_URL = "http://kong:8000";
SUPABASE_PUBLIC_URL = "https://supabase.cloonar.com";
NEXT_PUBLIC_ENABLE_LOGS = "true";
NEXT_ANALYTICS_BACKEND_PROVIDER = "postgres";
LOGFLARE_URL = "http://analytics:4000";
SNIPPETS_MANAGEMENT_FOLDER = "/app/snippets";
EDGE_FUNCTIONS_MANAGEMENT_FOLDER = "/app/edge-functions";
};
environmentFiles = [ "/run/supabase/studio.env" ];
volumes = [
"/var/lib/supabase/snippets:/app/snippets"
"/var/lib/supabase/functions:/app/edge-functions"
];
extraOptions = supabaseNet ++ [
"--network-alias=studio"
];
};
# 10. Kong (API Gateway)
supabase-kong = {
image = "kong/kong:3.9.1";
dependsOn = [ "supabase-studio" ];
environment = {
KONG_DATABASE = "off";
KONG_DECLARATIVE_CONFIG = "/usr/local/kong/kong.yml";
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";
};
environmentFiles = [ "/run/supabase/kong.env" ];
ports = [
"127.0.0.1:8000:8000"
"127.0.0.1:8443:8443"
];
volumes = [
"${./kong.yml}:/home/kong/temp.yml:ro"
"${kongEntrypoint}:/home/kong/kong-entrypoint.sh:ro"
];
entrypoint = "/home/kong/kong-entrypoint.sh";
extraOptions = supabaseNet ++ [
"--network-alias=kong"
];
};
# 11. Vector (log collection)
supabase-vector = {
image = "timberio/vector:0.53.0-alpine";
environment = { };
environmentFiles = [ "/run/supabase/vector.env" ];
volumes = [
"${./vector.yml}:/etc/vector/vector.yml:ro"
"/var/run/docker.sock:/var/run/docker.sock:ro"
];
cmd = [ "--config" "/etc/vector/vector.yml" ];
extraOptions = supabaseNet ++ [
"--network-alias=vector"
"--security-opt=label=disable"
];
};
# 12. Pooler (Supavisor)
supabase-pooler = {
image = "supabase/supavisor:2.7.4";
dependsOn = [ "supabase-db" ];
environment = {
PORT = "4000";
CLUSTER_POSTGRES = "true";
REGION = "local";
ERL_AFLAGS = "-proto_dist inet_tcp";
POOLER_POOL_MODE = "transaction";
POSTGRES_PORT = "5432";
POSTGRES_DB = "postgres";
POOLER_TENANT_ID = "default-tenant";
POOLER_DEFAULT_POOL_SIZE = "20";
POOLER_MAX_CLIENT_CONN = "100";
DB_POOL_SIZE = "10";
};
environmentFiles = [ "/run/supabase/pooler.env" ];
volumes = [
"${./pooler.exs}:/etc/pooler/pooler.exs:ro"
];
cmd = [
"/bin/sh" "-c"
"/app/bin/migrate && /app/bin/supavisor eval \"$(cat /etc/pooler/pooler.exs)\" && /app/bin/server"
];
extraOptions = supabaseNet ++ [
"--network-alias=pooler"
];
};
# 13. Edge Functions
supabase-functions = {
image = "supabase/edge-runtime:v1.71.2";
dependsOn = [ "supabase-kong" ];
environment = {
SUPABASE_URL = "http://kong:8000";
SUPABASE_PUBLIC_URL = "https://supabase.cloonar.com";
VERIFY_JWT = "false";
};
environmentFiles = [ "/run/supabase/functions.env" ];
volumes = [
"/var/lib/supabase/functions:/home/deno/functions"
"supabase-deno-cache:/root/.cache/deno"
];
cmd = [ "start" "--main-service" "/home/deno/functions/main" ];
extraOptions = supabaseNet ++ [
"--network-alias=functions"
];
};
};
# --- Nginx reverse proxy ---
services.nginx.virtualHosts."supabase.cloonar.com" = {
forceSSL = true;
enableACME = true;
acmeRoot = null;
locations."/" = {
proxyPass = "http://127.0.0.1:8000";
proxyWebsockets = true;
extraConfig = ''
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
client_max_body_size 50M;
'';
};
};
}