codingstairs
노트에듀라이프연락
⌕검색⌘K
koen

Navigation

  • Intro
  • Blog
  • Life

연락하기

로그인 없이도 보낼 수 있어요. 답변이 필요하면 이메일을 함께 적어 주세요.

  • 익명 폼으로 의견 남기기 →
  • ✉ warragon112@gmail.com
  • 카카오톡 오픈채팅 ↗

© 2026 codingstairs

  • 노트
  • 에듀
  • 검색
  • 라이프
  • 연락
  • 약관
  • RSS
  • GitHub
노트›cloud

Supabase Self-Hosted — Postgres 한 통에 BaaS 를 담는 방법

2026-04-28 게시· 2026-05-18 갱신·0회 조회

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 의 역할:

  1. 외부에서는 Kong 한 포트 (보통 8000 또는 54321) 만 보임.
  2. 경로별 내부 라우팅 (/auth/v1/* → auth, /rest/v1/* → rest, /storage/v1/* → storage, /realtime/v1/* → realtime, /functions/v1/* → functions).
  3. 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 을 참고합니다.

cloud 카테고리의 다른 글

카테고리 전체 보기 →
  • title 템플릿 단일 소스 — 자식 페이지가 박지 않게 한다
  • GitHub Pages — 저장소를 정적 사이트로
  • Replit — 브라우저 기반 개발·배포 통합 플랫폼
  • HTTP API Mocking — WireMock · MockServer · Prism · MSW
  • Firebase Local Emulator Suite — Firebase 한 묶음을 노트북에
  • LocalStack 과 MiniStack — 로컬에서 AWS 흉내내기