Supabase Self-Hosted — Postgres 한 통에 BaaS 를 담는 방법
Supabase Self-Hosted — Postgres 한 통에 BaaS 를 담는 방법
Supabase 는 "오픈소스 Firebase" 라는 슬로건으로 2020 년 등장했습니다. Firebase 가 자체 NoSQL · 자체 Auth · 자체 Storage 처럼 하나의 거대한 매니지드 박스라면, Supabase 는 Postgres 를 중심에 두고 그 위에 작은 컴포넌트를 얹는 방식을 택했습니다. 매니지드 (supabase.com) 와 self-hosted 가 같은 컴포넌트로 동작합니다.
1. Supabase 에 대한 이야기
Supabase 의 "백엔드 서비스" 는 사실 단일 Postgres + 그 앞단의 마이크로서비스 묶음 입니다. 어떤 컴포넌트도 자체 데이터베이스를 따로 두지 않습니다.
- Auth 의 사용자 테이블도, Storage 의 객체 메타도, Realtime 의 변경 이벤트도 모두 같은 Postgres 의 다른 스키마 (
auth·storage·_realtime·_supabase·_analytics) 에 삽니다.
이게 SQL 한 줄로 권한·스토리지·인증을 동시에 다룰 수 있는 이유고, 동시에 self-hosted 의 배포 단위가 단순한 이유입니다.
2. 컴포넌트 14 개
| 컴포넌트 | 이미지 | 역할 |
|---|---|---|
| Postgres | supabase/postgres |
DB 본체. pgvector · pg_graphql · pg_cron · pg_net · pgaudit 사전 빌드. |
| Kong | kong |
API 게이트웨이. 외부 단일 진입점, JWT 검증·라우팅. |
| GoTrue | supabase/gotrue |
Auth — 이메일/OAuth/OTP/MFA. |
| PostgREST | postgrest/postgrest |
DB 스키마를 자동 REST API 화. |
| Realtime | supabase/realtime |
Postgres CDC → WebSocket. |
| Storage | supabase/storage-api |
객체 저장 API. S3 또는 file 백엔드. |
| imgproxy | darthsim/imgproxy |
이미지 변환. |
| postgres-meta | supabase/postgres-meta |
DB 스키마 메타 API. |
| Studio | supabase/studio |
대시보드 UI (Next.js). |
| Edge Runtime | supabase/edge-runtime |
Deno 기반 서버리스 함수. |
| Logflare | supabase/logflare |
로그 분석. |
| Vector | timberio/vector |
컨테이너 로그 → Logflare. |
| Supavisor | supabase/supavisor |
커넥션 풀러 (PgBouncer 대체). |
13~14 개 컨테이너가 한 묶음. 공식 docker-compose 가 정답 이며, 자체 작성하기보다 supabase/supabase/docker/ 의 compose 를 base 로 쓰는 게 실수가 적습니다.
3. JWT 시크릿 모델 — 가장 자주 막히는 지점
Supabase 의 Auth 와 PostgREST 는 같은 JWT_SECRET 으로 서명된 JWT 만 신뢰합니다. self-hosted 셋업의 90% 함정이 여기 있습니다.
세 변수가 모두 일관되어야 합니다:
JWT_SECRET— 32+ 자 임의 문자열. 모든 컴포넌트 환경변수에 같은 값.ANON_KEY—JWT_SECRET으로 서명된{role: "anon"}JWT.SERVICE_ROLE_KEY—JWT_SECRET으로 서명된{role: "service_role"}JWT.
공식 .env.example 의 데모 키들은 정해진 시크릿 (your-super-secret-jwt-token-with-at-least-32-characters-long) 으로 서명된 값. JWT_SECRET 만 바꾸고 키는 안 바꾸면 즉시 모든 호출이 401 · 403.
node -e '
const c = require("crypto");
const s = "여기에 새 JWT_SECRET";
const sign = (p) => {
const h = Buffer.from(JSON.stringify({alg:"HS256",typ:"JWT"})).toString("base64url");
const b = Buffer.from(JSON.stringify(p)).toString("base64url");
return `${h}.${b}.${c.createHmac("sha256",s).update(`${h}.${b}`).digest("base64url")}`;
};
const iat = Math.floor(Date.now()/1000), exp = iat + 60*60*24*365*5;
console.log("ANON_KEY=" + sign({role:"anon", iss:"supabase-demo", iat, exp}));
console.log("SERVICE_ROLE_KEY=" + sign({role:"service_role", iss:"supabase-demo", iat, exp}));
'
4. Storage — file 모드의 가치
STORAGE_BACKEND 환경변수로 백엔드 선택:
STORAGE_BACKEND=s3— S3 호환 엔드포인트 (매니지드 표준). MinIO 같은 자체 S3 가 더 필요.STORAGE_BACKEND=file— 컨테이너 볼륨에 직접 파일 저장. 외부 의존이 0 이라 self-hosted 에서 가장 단순.
매니지드 Supabase 는 s3 모드지만, self-hosted 에서 단순함 우선이면 file 이 정답. publicUrl · signed URL · RLS · imgproxy 변환 모두 동일하게 동작합니다.
5. Auth + Inbucket — 메일 발송 디스크에 받기
GoTrue 는 회원가입·비밀번호 재설정 시 SMTP 로 메일을 보냅니다. self-hosted 는 외부 SMTP 가 없을 때 Inbucket 같은 dev SMTP 서버를 컨테이너로 같이 띄우는 게 표준 패턴.
auth:
environment:
GOTRUE_SMTP_HOST: inbucket
GOTRUE_SMTP_PORT: 2500
inbucket:
image: inbucket/inbucket
ports:
- "9000:9000" # Web UI
- "2500:2500" # SMTP
회원가입 후 http://localhost:9000 에 들어가면 GoTrue 가 보낸 "Confirm Your Email" 이 그대로 떠 있습니다.
6. Kong — 단일 진입점
Kong 의 역할:
- 외부에서는 Kong 한 포트 (보통 8000 또는 54321) 만 보임.
- 경로별 내부 라우팅 (
/auth/v1/* → auth,/rest/v1/* → rest,/storage/v1/* → storage,/realtime/v1/* → realtime,/functions/v1/* → functions). - apikey/JWT 헤더로 anon · service_role 구분.
Kong 의 kong.yml 은 declarative config. 환경변수 placeholder ($ANON_KEY) 는 컨테이너 entrypoint 가 awk 로 치환. self-hosted 에서 Kong 이 /entrypoint.sh: No such file or directory 로 죽으면 보통 kong 이미지 버전이 바뀌면서 entrypoint 경로가 /docker-entrypoint.sh 로 이동했기 때문 (kong:3.x 이후).
7. Postgres — supabase/postgres 를 써야 하는 이유
postgres:15-alpine 같은 표준 이미지로는 self-hosted 가 동작하지 않습니다. 의존 확장:
pgvector— embedding.pg_graphql— GraphQL 자동 생성.pg_cron— 스케줄 작업.pg_net— DB 안에서 HTTP 호출 (Webhook).pgaudit— 감사 로그.pg_stat_statements.
이 확장들이 사전 빌드된 게 supabase/postgres 이미지. 표준 이미지에 직접 깔면 Realtime · Auth 가 init 단계에서 실패합니다.
8. 흔히 막히는 지점
| 증상 | 원인 |
|---|---|
| 모든 호출 401/403 | JWT_SECRET 과 ANON_KEY · SERVICE_ROLE_KEY 불일치. |
| 컨테이너 14 개 중 한두 개 unhealthy 인 채로 다른 컨테이너 hang | depends_on condition: service_healthy 줄줄이 막힘. healthcheck timing 또는 권한. |
Studio 의 healthcheck ECONNREFUSED 127.0.0.1:3000 |
Next.js 가 컨테이너 호스트네임 바인딩. HOSTNAME=0.0.0.0 필요. |
| Realtime healthcheck 영원히 403 | _supabase DB tenant seed 가 healthcheck 시점에 안 끝남. 무시 가능. |
| Pooler 가 Elixir SyntaxError | Windows git autocrlf 가 pooler.exs 에 CR. LF 변환 필요. |
Kong 의 plugin 'request-termination' not enabled |
KONG_PLUGINS 에 누락. 공식 compose 전체 플러그인 목록 확인. |
9. supabase-js 호출
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(
"http://localhost:54321",
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
// Auth
await supabase.auth.signUp({ email: "x@local.dev", password: "1234" });
// REST (자동 생성)
const { data } = await supabase.from("posts").select("*").limit(10);
// Storage
await supabase.storage.from("bucket").upload("k.png", file);
// Realtime
supabase
.channel("posts")
.on("postgres_changes", { event: "*", schema: "public", table: "posts" }, console.log)
.subscribe();
// Edge Function
await supabase.functions.invoke("hello", { body: { name: "world" } });
URL 한 줄만 매니지드/self-hosted 사이로 옮기면 같은 코드가 양쪽에서 동작합니다. 이게 Supabase 의 가장 큰 약속.
10. 한계
컨테이너 14 개의 메모리 발자국 — idle ~3 GB. 노트북에서 빠듯하면 analytics · vector 를 끄거나 매니지드.
버전 매트릭스가 빠르게 변함 — 각 컴포넌트의 호환 조합이 정해져 있습니다. 공식 compose 의 SHA-pinned 태그를 따라가는 게 안전.
CDN · 도메인 직접 해결 — Storage publicUrl 은 컨테이너 내부 호스트네임을 반환할 수 있어, 운영에서는 Caddy 리버스 프록시 + SUPABASE_PUBLIC_URL 환경변수로 외부 도메인.
백업 — pg_dump 한 번이 사실상의 백업. 다만 storage 의 파일 백엔드 (/var/lib/storage) 도 같이 보존.
하고픈 말
Supabase 의 self-hosted 는 컨테이너 14 개의 묶음이지만, 데이터는 한 Postgres 안에 삽니다. JWT 시크릿 일관성 + Storage file 모드 + Inbucket 의 조합으로 외부 의존을 0 으로 만드는 모양이 가장 단단합니다. 매니지드와 self-hosted 의 코드 호환은 URL 한 줄 — 이 가치를 위해 14 개의 컨테이너를 받아들이는 게 self-hosted 의 본질입니다.
Next
- firebase-emulator
- api-mocking-wiremock
Supabase 공식 · Self-hosting 가이드 · supabase-js · GoTrue · PostgREST · Realtime · Storage API · Inbucket 을 참고합니다.