SSD Nodes Learn Hosting plans →
가이드 Matt Connor작성자 Matt Connor · 업데이트됨 2026-10-13

VPS에서 Supabase 자체 호스팅하기: Docker 설치 가이드

공식 Supabase Docker 스택을 VPS에 구축하는 방법을 안내합니다. 공개 데모 비밀 값 교체, 11개 서비스 역할, 필요한 RAM, 백업과 DB를 보존하는 업데이트 방법을 설명합니다.

구축할 항목

Supabase를 자체 호스팅한다는 것은 공식 Docker Compose 스택을 자체 서버에서 실행한다는 뜻이다. 여기에는 Postgres, REST API, 인증 서비스, 파일 스토리지, realtime websockets, Studio 대시보드가 포함된다. 저장소 하나를 clone하고 .env 파일 하나를 편집한 다음, 총 11개의 컨테이너를 시작한다. 이 컨테이너들은 함께 사용자가 관리하는 Supabase 프로젝트처럼 동작한다.

설치 자체는 간단하다. 문제는 .env 파일에서 발생한다. 이 파일에는 저장소에 공개된 데모용 비밀 값이 포함되어 있다. 이러한 기본값으로 시작한 스택은 해당 값을 찾은 누구에게나 노출된다. 이 가이드에서는 교체해야 하는 비밀 값, 각 서비스의 역할, 스택에 실제로 필요한 메모리 용량, 데이터베이스를 삭제하지 않고 업데이트하는 방법을 설명한다.

Compose가 익숙하지 않다면 먼저 VPS에서 Docker Compose 기초를 읽는다. 아래 내용은 docker compose version이 이미 버전을 출력한다고 가정한다.

스택에 실제로 포함된 구성 요소

Supabase는 하나의 프로그램이 아니다. Compose 파일은 하나의 네트워크에서 여러 개의 독립 서비스를 시작한다. 각 서비스의 역할을 알면 컨테이너 이름만 나열된 상태에서도 문제를 진단할 수 있다.

  • db은 Supabase 확장이 로드된 PostgreSQL이다. 다른 모든 서비스가 이 데이터베이스와 통신한다. 이 컨테이너가 비정상이면 다른 서비스도 모두 실패한다.
  • api-gw은 API gateway다. August 2026 release부터 Envoy를 실행한다. 그전에는 kong이라는 서비스였으며, Kong도 opt-in override로 계속 사용할 수 있다. 이 서비스는 port 8000에서 수신하고 /rest/v1/, /auth/v1/, /storage/v1/을 올바른 backend로 라우팅한다. 외부에 공개해야 하는 컨테이너는 이 컨테이너뿐이다.
  • rest은 PostgREST다. Postgres schema를 읽어 REST API로 제공하므로 코드를 작성하지 않아도 새 table이 새 endpoint가 된다.
  • auth은 GoTrue다. 사용자를 식별하는 JSON web token (JWT)을 발급한다.
  • storage과 imgproxy은 file upload와 image resizing을 처리한다.
  • realtime은 websocket을 통해 database 변경 사항을 스트리밍한다.
  • studio과 meta은 dashboard와 그 뒤에서 동작하는 admin API다.
  • functions은 Deno 기반 edge runtime이고, supavisor은 Postgres connection pooler다.

Logs와 analytics(Logflare 및 vector)는 June 2026에 기본 Compose 파일에서 제외되었다. 이제는 sh run.sh config add logs로 활성화하는 opt-in override 파일에 포함된다. 이는 여러 Compose 파일로 하나의 스택 실행하기에서 설명한 Compose 파일 계층화와 같은 방식이다.

이 목록을 보면 아래의 resource 수치가 그렇게 산정된 이유를 알 수 있다. 데이터베이스 하나만 실행하는 것이 아니다. 데이터베이스와 10개의 지원 서비스를 함께 실행하는 것이다.

메모리 8 GB를 기준으로 계획한다

2026년 7월 기준으로 새로 설치한 환경에서 이 스택은 사용자 데이터나 트래픽이 없는 상태에서 상주 메모리 약 2.5~3 GB를 사용했다. 이 측정에서는 analytics 서비스와 Studio Node.js 프로세스가 단일 프로세스 기준으로 메모리를 가장 많이 사용했다. 이제 analytics는 기본 스택에 포함되지 않으므로 logs override를 활성화할 때만 메모리를 사용한다. 2 GB 서버에서는 컨테이너가 시작된 뒤 kernel out of memory killer가 컨테이너 하나를 종료한다. 이때 컨테이너가 exit code 137로 계속 재시작하는 증상이 나타난다. 2026년 10월에 확인한 Supabase의 자체 self-hosting 문서에서는 최소 사양으로 RAM 4 GB, CPU 코어 2개, SSD 40 GB를 제시한다. 권장 사양은 RAM 8 GB, CPU 코어 4개, SSD 80 GB다.

중요한 용도로 사용할 환경이라면 RAM 8 GB와 vCPU 4개를 할당한다. 무거운 쿼리와 Studio 세션을 동시에 실행할 때 느려질 수 있다는 점을 감수한다면 4 GB로 개인 개발 인스턴스를 운영할 수 있다. 디스크도 중요하다. Postgres, storage volume, 그리고 logs를 활성화한 경우 log data가 모두 project directory 아래에 저장되기 때문이다. 우선 40 GB로 시작하고 사용량을 모니터링한다. 요금제를 선택하기 전에 서비스 수를 계산하는 습관은 self-host하는 모든 환경에서 중요하다. PhotoPrism과 Immich에도 실제 RAM 최소 요구 사항이 있기 때문이다. 2026년 10월에 확인한 Immich quick start 페이지에서는 RAM 6 GB와 CPU 코어 2개 이상을 요구한다. 아직 프로젝트에 이보다 큰 서버가 필요하지 않다면 Supabase free cloud plan에 실제로 포함되는 항목과 비교해 본다. 500 MB 미만을 사용하고 일주일 동안 유휴 상태로 유지되지 않는 소규모 애플리케이션이라면 무료로 실행할 수 있다.

설치: 공식 저장소 복제

지원되는 경로를 사용하면 메인 저장소에서 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 pull

docker compose pull은 수 기가바이트의 이미지를 다운로드합니다. 모든 서비스가 Pulled로 표시되면서 완료되어야 합니다. 여기서 manifest unknown 오류가 발생한다면 고정된 이미지 태그가 업스트림에서 제거된 것입니다. 이 경우 태그를 직접 수정하기보다 저장소의 최신 사본을 다시 내려받아 해결해야 합니다.

첫 실행 전에 변경해야 하는 비밀 정보

스택을 시작하기 전에 이 작업을 수행해야 하며, 시작한 후에는 수행하면 안 된다. 이 값 중 일부는 최초 부팅 시 데이터에 기록되므로 나중에 변경하려면 데이터베이스를 초기화해야 한다.

저장소에는 새 JWT secret으로 서명해야 하는 2개의 API key를 포함해 모든 값을 올바르게 생성하는 generator가 포함되어 있다.

sh utils/generate-keys.sh --update-env

해당 스크립트는 .env에 JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY, SECRET_KEY_BASE, REALTIME_DB_ENC_KEY, VAULT_ENC_KEY, PG_META_CRYPTO_KEY, Logflare 토큰, S3 프로토콜 키, MINIO_ROOT_PASSWORD의 새 값을 기록한다. 2026년 10월 기준으로는 POSTGRES_PASSWORD와 DASHBOARD_PASSWORD에도 임의의 값을 기록한다. 이 스크립트에는 openssl이 필요하며, openssl은 일반적인 Ubuntu 이미지에 기본으로 포함되어 있다. 이제 데이터베이스 암호도 다시 작성하므로, --update-env과 함께 실행하는 시점은 최초 시작 전으로 제한해야 한다. 이미 데이터가 있는 스택에서는 절대 실행하지 않는다.

.env에서 두 값은 직접 확인할 필요가 있다:

  • POSTGRES_PASSWORD. 스크립트가 이 값을 무작위 16진수로 채우므로 안전하다. 직접 지정하려면 문자와 숫자만 사용한다. 여기에 구두점을 넣으면 여러 서비스가 문자열을 연결해 생성하는 연결 문자열이 손상된다. 이때 오류가 구문 분석 오류가 아니라 인증 오류처럼 표시되므로, 잘못된 위치에서 원인을 찾게 된다.
  • DASHBOARD_USERNAME 및 DASHBOARD_PASSWORD. Studio의 기본 인증 자격 증명이다. 스크립트는 무작위 비밀번호를 설정하지만 사용자 이름은 기본값인 supabase로 둔다. 배포 시 기본 비밀번호는 말 그대로 this_password_is_insecure_and_should_be_updated이므로, 스크립트를 건너뛰었다면 지금 변경한다.

ANON_KEY와 SERVICE_ROLE_KEY를 임의로 만들 수 없는 이유를 이해해야 한다. 둘 다 JWT_SECRET으로 서명된 JWT이다. 게이트웨이는 .env에 있는 정확한 키만 허용한다. 게이트웨이 뒤의 서비스는 JWT_SECRET을 기준으로 서명을 검증한다. 따라서 일치하지 않는 키는 HTTP 401로 거부된다. 기본 Envoy 게이트웨이는 401 Unauthorized로 응답한다. 현재 Kong override도 동일하게 응답한다. 이 override는 Kong 3.9를 실행하며 {"message":"Unauthorized"}을 반환한다. 2026년 3월 이전 설치에서는 Kong 2.8을 실행했으며 {"message":"Invalid authentication credentials"}로 응답한다. 이는 셀프 호스팅에서 가장 흔한 실패 원인이다. 운영자가 JWT_SECRET를 변경했지만 데모 키는 그대로 두었기 때문이다. 세 항목은 항상 함께 생성해야 한다.

SERVICE_ROLE_KEY는 root 비밀번호와 동일하게 취급해야 합니다. 이 값은 row-level security를 완전히 우회합니다. 서버 측 코드에만 사용해야 하며, 다른 곳에는 두어서는 안 됩니다.

SITE_URL를 애플리케이션 주소로 설정하고, API_EXTERNAL_URL를 끝에 /auth/v1이 붙은 API의 공개 주소로 설정합니다. 예를 들면 https://supabase.example.com/auth/v1과 같습니다. SUPABASE_PUBLIC_URL는 경로를 제외한 동일한 공개 주소로 설정합니다. Auth는 이 값을 사용해 이메일 확인 링크와 OAuth 콜백 링크를 생성합니다. 따라서 localhost 기본값을 그대로 두면 모든 사용자가 자신의 컴퓨터로 연결됩니다.

그다음 현재 설정을 확인한다:

sh run.sh secrets

서비스를 시작하고 상태 확인하기

sh run.sh start
docker compose ps

run.sh start은 docker compose up -d --wait를 감싸고 있으므로, 상태 확인이 통과될 때까지 반환되지 않습니다. 모든 서비스는 running (healthy) 또는 running 상태를 보여야 합니다. Postgres가 다른 서비스가 연결되기 전에 초기화 스크립트를 실행하므로, 첫 부팅에는 2분에서 4분 정도 소요됩니다.

컨테이너가 계속 재시작된다면, 서비스 이름으로 로그를 확인하십시오:

docker compose logs db
docker compose logs auth

이제 Studio는 8000 포트에서 실행되며, 설정한 대시보드 사용자 이름과 비밀번호를 요구합니다.

공개 인터넷에 포트 8000을 노출하지 않는다

8000에서 수신 대기하는 gateway는 일반 HTTP를 사용한다. 모든 API key와 모든 사용자 password가 네트워크를 평문으로 통과한다. Studio 자격 증명도 암호화가 아니라 base64 인코딩을 사용하는 basic authentication이다.

gateway 앞에 reverse proxy를 두고 그 지점에서 TLS(transport layer security)를 종료한다. 다른 프로세스가 gateway에 접근하지 못하도록 gateway를 loopback 주소에 바인딩한다. docker-compose.yml에서는 api-gw 서비스 아래의 port 줄을 127.0.0.1:${API_GW_HTTP_PORT:-${KONG_HTTP_PORT:-8000}}:8000/tcp로 변경하고, proxy가 해당 포트로 전달하도록 설정한다. 2026년 8월 Envoy로 전환하기 전 설치한 경우에는 같은 port 줄이 대신 kong라는 서비스 아래에 있다. 인증서 설정은 여러 Compose 앱 앞에 Traefik 배치하기에서 설명한다. 이 proxy는 이 stack부터 90년대 비디오 대여점처럼 재구성한 Jellyfin library 같은 가벼운 서비스까지 서버의 다른 모든 항목 앞에도 배치하게 된다. 각 서비스에는 추가로 공개 포트를 여는 대신 hostname이 필요하다. dashboard를 본인만 사용한다면 proxy를 생략하고 SSH tunnel을 통해 loopback 포트에 접근하면 된다. self-hosted open-kritt도 동일한 방식으로 scanning interface를 공개 인터넷에서 완전히 차단한다.

Docker는 자체 iptables 규칙을 작성해 포트를 publish하므로, 일반적인 ufw 설정만으로는 해당 규칙이 보이지 않는다. 따라서 나머지 포트도 firewall에서 차단해야 한다. 이 함정은 Docker container가 ufw 규칙을 무시하는 이유에서 설명한다.

데이터베이스를 백업하고 디렉터리는 백업하지 않는다

Postgres 데이터는 ./volumes/db/data의 bind mount에 저장된다. 컨테이너가 실행 중일 때 이 디렉터리를 복사하면 일관성이 깨진 복사본이 생성된다. Postgres는 쓰기 작업을 버퍼링하며, 디스크의 파일은 checkpoint 시점에만 일관된 상태가 되기 때문이다. 이 복사본으로 복원하면 대개는 작동하지만, 마지막 트랜잭션 일부가 조용히 손실될 수도 있다. 백업에서 발생할 수 있는 가장 위험한 유형의 장애다.

대신 dump를 생성한다. pg_dumpall는 컨테이너 내부에서 실행되며 일관된 snapshot을 생성한다.

docker exec -t supabase-db pg_dumpall -U postgres > supabase-$(date +%F).sql

신뢰하기 전에 파일이 비어 있지 않은지 확인한다. 그런 다음 일정에 따라 이 dump를 서버 외부로 전송한다. restic을 사용한 암호화 오프사이트 백업이 이를 담당한다. 예약된 dump 작업이 조용히 실패하면 백업이 전혀 없는 것과 구분할 수 없다. 따라서 cron 또는 systemd 작업이 0이 아닌 종료 코드로 끝날 때 휴대전화로 알림을 보내도록 설정한다. .env도 같은 시점에 백업한다. JWT_SECRET을 잃으면 발급된 모든 token이 무효화되고 저장된 암호화 secret을 읽을 수 없게 된다.

업로드된 파일은 ./volumes/storage에 저장된다. 일반 파일이므로 단순히 복사해도 된다.

데이터를 잃지 않고 업데이트하기

Supabase는 docker-compose.yml에서 이미지 버전을 고정하므로, 직접 변경하기 전에는 아무것도 변경되지 않는다. 직접 조합하는 모든 스택에서도 버전 고정을 적용하는 것이 좋다. 따라서 셀프 호스팅 RustDesk 릴레이는 변경되는 태그를 따르지 않고 두 서버 이미지를 고정한다. 업그레이드는 시간을 낼 수 있는 날 직접 선택해 수행해야 한다. 매번 먼저 dump를 생성한다.

docker compose pull
sh run.sh recreate

recreate는 스택을 중지한 뒤 새 이미지로 다시 시작한다. 데이터가 유지되는 이유는 데이터가 컨테이너 내부가 아니라 호스트의 bind mount에 저장되기 때문이다. 주요 버전을 업그레이드하기 전에는 저장소의 CHANGELOG.md를 확인해야 한다. Postgres 주요 버전 업그레이드는 별도의 단계이며 recreate는 이를 수행하지 않기 때문이다. Postgres 17은 2026년 6월부터 기본 버전이다. 기존 Postgres 15 설치는 sudo bash utils/upgrade-pg17.sh로 Postgres 17로 전환할 수 있다. 이 명령은 pg_upgrade를 사용해 데이터를 제자리에서 마이그레이션하고 각 주요 단계마다 확인을 요청한다. 모든 컨테이너가 실행 중이어야 하며, 필요한 여유 디스크 공간은 데이터베이스 크기의 2배에 5 GB를 더한 값 이상이어야 한다. Supabase는 시작하기 전에 별도의 백업을 직접 생성할 것을 여전히 권장한다.

Compose 파일 자체의 변경 사항을 적용하려면 프로젝트 디렉터리에서 sh update.sh를 실행한다. 먼저 변경될 내용을 확인하려면 sh update.sh --dry-run를 실행한다. 이 명령은 upstream 파일과 사용자가 수정한 내용을 병합한다. .env의 기존 값은 덮어쓰지 않고 새 키만 추가한다. 또한 데이터베이스는 포함하지 않지만, 기록하기 전에 backups/ 아래에 설정을 보관한다. .supabase-version 파일이 없는 설치에는 --from과 설치를 시작한 release가 필요하다. 특히 2026년 8월 release에서는 kong 서비스를 api-gw로 교체했으며 docker-compose.yml 업데이트가 필요하므로 이 내용이 중요하다. 수동으로 처리하는 방법도 여전히 사용할 수 있다. upstream 저장소를 다시 clone하고 해당 저장소의 docker 디렉터리를 프로젝트에 복사한다. 이때 .env를 덮어쓰지 않도록 주의한다.

데이터베이스를 포함한 모든 항목을 삭제하고 .env를 기본값으로 되돌리는 전체 reset은 별도의 스크립트로 수행한다. 기존 설정은 .env.old로 보관되며, 실행 전에 확인을 요청한다.

sh reset.sh

FAQ

API 호출이 "Invalid authentication credentials"를 반환하는 이유는 무엇인가?

이 정확한 메시지는 2026년 3월 이전에 설치한 스택에서 gateway로 사용되는 Kong 2.8이 반환한다. 기본 Envoy gateway와 Kong 3.9를 실행하는 현재 Kong override는 같은 문제를 일반적인 401 Unauthorized로 보고한다. 어느 경우든 전송한 key가 .env의 ANON_KEY 또는 SERVICE_ROLE_KEY과 일치하지 않거나, 해당 key가 현재 .env에 있는 JWT_SECRET로 서명되지 않은 것이다. 데이터를 한 번도 저장하지 않은 스택에서는 sh utils/generate-keys.sh --update-env로 세 값을 함께 다시 생성한다. 실행 중인 스택에서는 flag 없이 script를 실행하고 no라고 답한다. 그러면 새 값만 출력되므로 JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY을 직접 .env에 복사하고 POSTGRES_PASSWORD은 그대로 둔다. 그런 다음 sh run.sh recreate을 실행하여 서비스가 새 값을 읽게 한다.

2 GB VPS에서 self-hosted Supabase를 실행할 수 있는가?

안정적으로 실행하기는 어렵다. Supabase의 자체 self-hosting 문서에서는 2026년 10월 기준 최소 RAM으로 4 GB를 제시한다. 2026년 7월 기준으로는 현재 opt-in 방식인 logging services를 포함한 측정에서 스택이 유휴 상태일 때도 거의 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 file에는 Deno 기반 functions runtime이 포함되어 있으며, ./volumes/functions 아래에 배치한 항목을 모두 제공한다. hosted platform의 global deployment network는 포함되지 않는다. 따라서 functions는 한 위치의 단일 서버에서 실행된다.

Postgres database에 직접 연결하려면 어떻게 해야 하는가?

서버 자체에서 interactive shell을 사용하려면 docker exec -it supabase-db psql -U postgres을 사용한다. 외부 client에서는 port 5432의 Supavisor를 통해 postgres.<POOLER_TENANT_ID> user와 POSTGRES_PASSWORD을 사용하여 연결한다. 해당 port를 인터넷에 공개하지 않는다. VPN 또는 SSH tunnel을 통해 접근한다.

auth confirmation email이 localhost로 연결되는 이유는 무엇인가?

.env의 SITE_URL과 API_EXTERNAL_URL이 기본값으로 남아 있다. auth service는 이 두 값으로 모든 confirmation link와 password reset link를 생성하므로, 설정된 주소를 그대로 전송한다. 두 값을 실제 public URL로 설정한 뒤 스택을 다시 생성한다.

이 글이 도움이 되었나요?