VPS에서 Supabase Docker 자체 호스팅하기
공식 Supabase Docker Compose 스택을 VPS에 설치하고, 공개 데모 secret 교체 방법과 약 14개 서비스의 역할, 실제 RAM 요구량, 백업 및 업데이트 방법을 설명합니다.
구축하는 항목
Supabase를 자체 호스팅한다는 것은 공식 Docker Compose 스택을 자체 서버에서 실행하는 것을 의미합니다. 이 스택에는 Postgres, Postgres 앞단의 REST API, 인증 서비스, 파일 스토리지, realtime websockets, Studio 대시보드가 포함됩니다. 하나의 저장소를 clone하고 .env 파일 하나를 편집한 다음, 함께 사용자가 제어하는 Supabase 프로젝트처럼 동작하는 약 14개의 컨테이너를 시작합니다.
설치 과정은 짧습니다. 문제는 .env 파일에서 발생합니다. 이 파일에는 저장소에 공개된 데모 secret이 기본으로 포함되어 있습니다. 이러한 기본값으로 시작한 스택은 이를 발견한 모든 사용자에게 노출됩니다. 이 가이드에서는 교체해야 하는 secret, 각 서비스의 용도, 스택에 실제로 필요한 메모리 용량, 데이터베이스를 삭제하지 않고 업데이트하는 방법을 설명합니다.
Compose가 처음이라면 먼저 VPS에서 Docker Compose 기초를 읽으십시오. 아래 내용은 docker compose version이 이미 버전을 출력한다고 가정합니다.
스택에 실제로 포함된 구성 요소
Supabase는 하나의 프로그램이 아닙니다. Compose 파일은 하나의 네트워크에서 서로 분리된 여러 서비스를 시작합니다. 각 서비스의 역할을 알면 많은 컨테이너 이름을 보고도 문제를 진단할 수 있습니다.
db은 Supabase 확장이 로드된 PostgreSQL입니다. 다른 모든 서비스가 이 서비스와 통신합니다. 이 컨테이너의 상태가 비정상이면 다른 모든 서비스도 실패합니다.kong는 API 게이트웨이입니다. port 8000에서 수신하고/rest/v1/,/auth/v1/,/storage/v1/를 올바른 백엔드로 라우팅합니다. 외부에 노출해야 하는 컨테이너는 이 컨테이너뿐입니다.rest은 PostgREST입니다. Postgres 스키마를 읽고 이를 REST API로 제공합니다. 따라서 코드를 작성하지 않아도 새 테이블이 새 endpoint가 됩니다.auth은 GoTrue입니다. 사용자를 식별하는 JSON web token (JWT)을 발급합니다.storage과imgproxy는 파일 업로드와 이미지 크기 조정을 처리합니다.realtime은 데이터베이스 변경 사항을 websocket을 통해 스트리밍합니다.studio과meta는 대시보드와 대시보드 뒤에서 동작하는 admin API입니다.analytics(Logflare)와vector는 로그를 수집하고,supavisor는 Postgres connection pooler입니다.
아래의 리소스 수가 이러한 구성에 따라 정해진 이유가 여기에 있습니다. 데이터베이스만 실행하는 것이 아닙니다. 데이터베이스와 12개의 지원 서비스를 함께 실행하는 것입니다.
크기 산정: 8 GB RAM을 계획합니다
새로 설치한 직후인 2026년 7월 기준으로, 자체 데이터나 트래픽이 없는 상태에서 스택은 약 2.5~3 GB의 상주 메모리를 사용합니다. analytics 서비스와 Studio Node.js 프로세스가 각각 가장 많은 메모리를 사용하는 구성 요소입니다. 2 GB 서버에서는 컨테이너가 시작된 다음 커널의 out of memory killer가 그중 하나를 종료합니다. 일반적으로 analytics 또는 db이 종료되며, 컨테이너가 exit code 137로 계속 재시작되는 증상이 나타납니다.
실제로 사용할 환경에는 8 GB RAM과 4 vCPU를 할당합니다. 무거운 쿼리와 Studio 세션을 동시에 실행하면 느려질 수 있다는 점을 감수한다면, 4 GB로 1인 개발 인스턴스를 운영할 수 있습니다. 디스크도 중요합니다. Postgres, storage volume, log data가 모두 project directory 아래에 저장되기 때문입니다. 40 GB로 시작하고 디스크 사용량을 모니터링합니다.
설치: 공식 저장소 복제
지원되는 방법은 주 저장소에서 docker 디렉터리를 사용자가 만든 프로젝트 디렉터리로 복사합니다. 이렇게 분리해야 나중에 실행되는 git pull가 사용자의 .env를 덮어쓰지 않습니다.
git clone --depth 1 https://github.com/supabase/supabase
mkdir supabase-project
cp -rf supabase/docker/* supabase-project
cp supabase/docker/.env.example supabase-project/.env
cd supabase-project
docker compose pulldocker compose pull는 수 GB의 이미지를 다운로드합니다. 모든 서비스가 Pulled로 표시되면 명령이 완료된 것입니다. 여기서 manifest unknown 오류가 발생하면 업스트림에서 고정된 이미지 태그를 삭제한 것입니다. 태그를 직접 수정하지 말고 저장소의 최신 복사본을 가져와야 합니다.
첫 시작 전에 변경해야 하는 보안 값
스택을 시작하기 전에 이 작업을 수행해야 합니다. 시작한 후에 수행하면 안 됩니다. 이러한 값 중 일부는 최초 부팅 시 데이터에 기록되므로, 나중에 변경하려면 데이터베이스를 재설정해야 합니다.
리포지토리에는 모든 값을 올바르게 생성하는 generator가 포함되어 있습니다. 여기에는 새 JWT secret으로 서명해야 하는 두 개의 API key도 포함됩니다.
sh utils/generate-keys.sh --update-env이 스크립트는 JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY, SECRET_KEY_BASE, REALTIME_DB_ENC_KEY, VAULT_ENC_KEY, PG_META_CRYPTO_KEY의 새 값과 Logflare token을 .env에 기록합니다. 이 작업에는 일반적인 Ubuntu image에 포함된 openssl가 필요합니다.
이 스크립트가 설정하지 않는 값이 2개 있습니다. .env에서 직접 편집해야 합니다.
POSTGRES_PASSWORD. 문자와 숫자만 사용합니다. 여기에 구두점을 넣으면 여러 서비스가 문자열을 결합해 생성하는 connection string이 손상됩니다. 이 오류는 parsing 오류가 아니라 authentication 오류처럼 나타나므로, 잘못된 위치를 조사하게 됩니다.DASHBOARD_USERNAME및DASHBOARD_PASSWORD. 이는 Studio의 basic authentication credential입니다. 기본으로 제공되는 password는 실제로this_password_is_insecure_and_should_be_updated입니다.
ANON_KEY 및 SERVICE_ROLE_KEY를 임의로 만들 수 없는 이유를 이해해야 합니다. 두 값 모두 JWT_SECRET으로 서명된 JWT입니다. gateway는 모든 요청에서 해당 signature를 확인하므로, secret과 일치하지 않는 key는 {"message":"Invalid authentication credentials"}와 함께 거부됩니다. 이는 self-hosting에서 가장 흔한 실패 원인입니다. 운영자가 JWT_SECRET만 변경하고 demo key는 그대로 두는 경우입니다. 항상 세 값을 함께 생성해야 합니다.
SERVICE_ROLE_KEY는 root password처럼 취급해야 합니다. 이 값은 row level security를 완전히 우회합니다. server side code에만 저장해야 하며, 다른 곳에는 두면 안 됩니다.
SITE_URL 및 API_EXTERNAL_URL는 사용자가 실제로 접속할 주소로 설정해야 합니다. 예를 들어 https://supabase.example.com입니다. Auth는 이 값을 사용해 email confirmation link와 OAuth callback link를 생성합니다. 따라서 http://localhost:8000로 그대로 두면 모든 사용자가 자신의 machine으로 이동하게 됩니다.
그런 다음 설정값을 확인합니다.
sh run.sh secrets시작하고 정상 상태인지 확인
sh run.sh start
docker compose psrun.sh start는 docker compose up -d --wait를 감싸므로 상태 확인이 통과될 때까지 반환되지 않습니다. 모든 서비스에 running (healthy) 또는 running이 표시되어야 합니다. 첫 번째 부팅에는 2~4분이 걸립니다. Postgres가 다른 항목이 연결하기 전에 초기화 스크립트를 실행하기 때문입니다.
컨테이너가 재시작 중이면 서비스 이름으로 로그를 확인합니다.
docker compose logs db
docker compose logs auth이제 Studio는 port 8000에서 실행됩니다. 설정한 dashboard username과 password를 입력하라는 메시지가 표시됩니다.
공개 인터넷에 port 8000을 노출하지 않습니다
Kong은 port 8000에서 암호화되지 않은 HTTP를 사용합니다. 모든 API key와 모든 사용자 password가 평문으로 네트워크를 통과합니다. Studio 자격 증명은 암호화가 아닌 base64 인코딩을 사용하는 basic authentication입니다.
Kong 앞에 reverse proxy를 배치하고, 그곳에서 TLS(transport layer security)를 종료합니다. 또한 Kong을 loopback address에 바인딩하여 다른 대상이 Kong에 접근하지 못하게 합니다. docker-compose.yml에서 kong port mapping은 127.0.0.1:8000:8000가 되며, proxy는 해당 대상으로 요청을 전달합니다. 여러 Compose 앱 앞에 Traefik 배치에서 인증서 설정을 설명합니다.
방화벽에서도 나머지 port를 차단해야 합니다. Docker는 자체 iptables 규칙을 작성하여 port를 게시하므로, 단순한 ufw 설정에서는 이를 확인할 수 없습니다. 이 문제는 Docker 컨테이너가 ufw 규칙을 무시하는 이유에서 설명합니다.
디렉터리가 아니라 데이터베이스를 백업합니다
Postgres 데이터는 ./volumes/db/data의 bind mount에 저장됩니다. 컨테이너가 실행 중일 때 해당 디렉터리를 복사하면 일관성이 보장되지 않은 복사본이 생성됩니다. Postgres는 쓰기 작업을 버퍼링하며, 디스크의 파일은 checkpoint 시점에만 일관된 상태가 되기 때문입니다. 이 복사본을 복원하면 대개 작동하지만, 마지막 트랜잭션 일부가 조용히 손실될 수도 있습니다. 백업에서는 이것이 가장 심각한 실패 유형입니다.
대신 dump를 생성합니다. pg_dumpall는 컨테이너 내부에서 실행되며 일관된 스냅샷을 생성합니다.
docker exec -t supabase-db pg_dumpall -U postgres > supabase-$(date +%F).sql신뢰하기 전에 파일이 비어 있지 않은지 확인합니다. 그런 다음 일정에 따라 해당 dump를 서버 외부로 전송합니다. restic을 사용한 암호화된 오프사이트 백업이 이 작업에 사용됩니다. 동시에 .env도 백업합니다. JWT_SECRET가 손실되면 발급된 모든 토큰이 무효화되고 저장된 암호화 secret을 읽을 수 없게 됩니다.
업로드된 파일은 ./volumes/storage에 저장되며 일반 파일이므로 단순 복사로 충분합니다.
데이터 손실 없이 업데이트
Supabase는 docker-compose.yml에서 이미지 버전을 고정하므로, 직접 변경하기 전에는 아무것도 변경되지 않습니다. 항상 먼저 덤프를 생성합니다.
docker compose pull
sh run.sh recreaterecreate은 스택을 중지한 후 새 이미지로 다시 시작합니다. 데이터는 컨테이너 내부가 아니라 호스트의 bind mount에 있으므로 유지됩니다. 주요 버전으로 업그레이드하기 전에는 저장소에서 CHANGELOG.md를 확인합니다. Postgres 주요 버전 업그레이드는 자동으로 수행되지 않으며, 덤프와 복원이 필요합니다.
Compose 파일 자체의 변경 사항을 적용하려면 upstream 저장소를 다시 clone하고 해당 저장소의 docker 디렉터리를 프로젝트에 복사합니다. 이때 .env를 덮어쓰지 않도록 주의합니다.
데이터베이스를 포함한 모든 항목을 삭제하는 전체 초기화는 별도의 스크립트로 수행하며, 확인을 요청합니다.
sh reset.shFAQ
API 호출에서 "Invalid authentication credentials"가 반환되는 이유는 무엇입니까?
ANON_KEY 또는 SERVICE_ROLE_KEY이 현재 .env에 있는 JWT_SECRET로 서명되지 않았습니다. 게이트웨이는 모든 요청의 서명을 확인하고, 일치하지 않으면 요청을 거부합니다. sh utils/generate-keys.sh --update-env로 세 값을 함께 다시 생성한 다음 sh run.sh recreate을 실행하여 서비스가 새 값을 읽도록 합니다.
2 GB VPS에서 self-hosted Supabase를 실행할 수 있습니까?
안정적으로 실행하기는 어렵습니다. 2026년 7월 기준으로 약 14개의 서비스를 실행하므로 스택은 유휴 상태에서도 약 3 GB를 사용합니다. 따라서 2 GB 서버에서는 out of memory killer가 컨테이너를 종료하고 docker compose ps에서 exit code 137이 표시됩니다. production에는 8 GB를 사용하고, 개인 개발에는 4 GB를 최소 사양으로 사용합니다.
self-hosted Supabase에 edge functions가 포함됩니까?
예. Compose 파일에는 Deno 기반 functions runtime이 포함되어 있으며, ./volumes/functions 아래에 배치한 항목을 제공합니다. hosted platform의 전역 deployment network는 포함되지 않습니다. 따라서 functions는 한 위치의 단일 서버에서 실행됩니다.
Postgres 데이터베이스에 직접 연결하려면 어떻게 해야 합니까?
서버 자체에서 interactive shell을 사용하려면 docker exec -it supabase-db psql -U postgres을 사용합니다. 외부 클라이언트에서는 port 5432의 Supavisor를 통해 사용자 postgres.<POOLER_TENANT_ID>와 POSTGRES_PASSWORD로 연결합니다. 해당 port를 인터넷에 개방하지 마십시오. VPN 또는 SSH tunnel을 통해 접근합니다.
auth 확인 이메일의 링크가 localhost로 연결된 이유는 무엇입니까?
.env의 SITE_URL 및 API_EXTERNAL_URL이 기본값으로 남아 있었습니다. auth service는 이 두 값으로 모든 확인 및 password reset 링크를 생성하므로, 설정된 주소를 그대로 전송합니다. 두 값을 실제 public URL로 설정한 다음 stack을 다시 생성합니다.