# cdrdpyj — single-file dev stack # # Backend only (recommended, pair with `pnpm dev` + portless on the host): # docker compose up -d # # Everything in Docker (backend + Astro + Payload): # docker compose --profile apps up -d --build # # Studio dashboard: http://localhost:8000 (user/pass from .env) # Postgres: postgresql://postgres:@localhost:5432/postgres # # The Supabase services are a trimmed self-hosted stack (db, meta, studio, # kong, auth, rest, storage, imgproxy). Realtime / Edge Functions / Supavisor # pooler are intentionally omitted to keep local dev light; the DB is still # initialised with the full official role/JWT setup so they can be added later. name: cdrdpyj services: ##################################################################### # Postgres — the Supabase database (also used directly by Payload CMS) ##################################################################### db: container_name: cdrdpyj-db image: supabase/postgres:17.6.1.136 restart: unless-stopped ports: # Exposed so host-run apps (Payload via portless) can connect. - ${POSTGRES_PORT}:5432 volumes: - ./supabase/volumes/db/realtime.sql:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:Z - ./supabase/volumes/db/webhooks.sql:/docker-entrypoint-initdb.d/init-scripts/98-webhooks.sql:Z - ./supabase/volumes/db/roles.sql:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:Z - ./supabase/volumes/db/jwt.sql:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:Z # Pre-create the dedicated schema Payload CMS uses - ./supabase/volumes/db/payload.sql:/docker-entrypoint-initdb.d/init-scripts/99-payload.sql:Z - ./supabase/volumes/db/_supabase.sql:/docker-entrypoint-initdb.d/migrations/97-_supabase.sql:Z - ./supabase/volumes/db/logs.sql:/docker-entrypoint-initdb.d/migrations/99-logs.sql:Z - ./supabase/volumes/db/pooler.sql:/docker-entrypoint-initdb.d/migrations/99-pooler.sql:Z - ./supabase/volumes/db/data:/var/lib/postgresql/data:Z - db-config:/etc/postgresql-custom healthcheck: test: ["CMD", "pg_isready", "-U", "postgres", "-h", "localhost"] interval: 5s timeout: 5s retries: 10 environment: POSTGRES_HOST: /var/run/postgresql PGPORT: ${POSTGRES_PORT} POSTGRES_PORT: ${POSTGRES_PORT} PGPASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} PGDATABASE: ${POSTGRES_DB} POSTGRES_DB: ${POSTGRES_DB} JWT_SECRET: ${JWT_SECRET} JWT_EXP: ${JWT_EXPIRY} command: - postgres - -c - config_file=/etc/postgresql/postgresql.conf - -c - log_min_messages=fatal ##################################################################### # postgres-meta — powers Studio's table/SQL editor ##################################################################### meta: container_name: cdrdpyj-meta image: supabase/postgres-meta:v0.96.6 restart: unless-stopped depends_on: db: condition: service_healthy environment: PG_META_PORT: 8080 PG_META_DB_HOST: ${POSTGRES_HOST} PG_META_DB_PORT: ${POSTGRES_PORT} PG_META_DB_NAME: ${POSTGRES_DB} PG_META_DB_USER: postgres PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD} CRYPTO_KEY: ${PG_META_CRYPTO_KEY} ##################################################################### # Studio — the Supabase dashboard ##################################################################### studio: container_name: cdrdpyj-studio image: supabase/studio:2026.07.07-sha-a6a04f2 restart: unless-stopped depends_on: meta: condition: service_started healthcheck: test: [ "CMD-SHELL", "node -e \"fetch('http://localhost:3000/api/platform/profile').then((r) => {if (r.status !== 200) throw new Error(r.status)})\"" ] timeout: 10s interval: 5s retries: 3 start_period: 20s environment: HOSTNAME: "0.0.0.0" STUDIO_PG_META_URL: http://meta:8080 POSTGRES_PORT: ${POSTGRES_PORT} POSTGRES_HOST: ${POSTGRES_HOST} POSTGRES_DB: ${POSTGRES_DB} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_USER_READ_WRITE: postgres PG_META_CRYPTO_KEY: ${PG_META_CRYPTO_KEY} PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS} DEFAULT_ORGANIZATION_NAME: ${STUDIO_DEFAULT_ORGANIZATION} DEFAULT_PROJECT_NAME: ${STUDIO_DEFAULT_PROJECT} OPENAI_API_KEY: ${OPENAI_API_KEY} SUPABASE_URL: http://kong:8000 SUPABASE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL} SUPABASE_ANON_KEY: ${ANON_KEY} SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY} AUTH_JWT_SECRET: ${JWT_SECRET} SUPABASE_PUBLISHABLE_KEY: ${SUPABASE_PUBLISHABLE_KEY} SUPABASE_SECRET_KEY: ${SUPABASE_SECRET_KEY} ENABLED_FEATURES_LOGS_ALL: "false" SNIPPETS_MANAGEMENT_FOLDER: /app/snippets EDGE_FUNCTIONS_MANAGEMENT_FOLDER: /app/edge-functions volumes: - ./supabase/volumes/snippets:/app/snippets:z - ./supabase/volumes/functions:/app/edge-functions:ro,z ##################################################################### # Kong — API gateway (Auth / REST / Storage all sit behind :8000) ##################################################################### kong: container_name: cdrdpyj-kong image: kong/kong:3.9.1 restart: unless-stopped networks: default: aliases: - api-gw healthcheck: test: ["CMD", "kong", "health"] interval: 5s timeout: 5s retries: 5 depends_on: studio: condition: service_healthy ports: - ${KONG_HTTP_PORT}:8000/tcp - ${KONG_HTTPS_PORT}:8443/tcp volumes: - ./supabase/volumes/api/kong.yml:/home/kong/temp.yml:ro,z - ./supabase/volumes/api/kong-entrypoint.sh:/home/kong/kong-entrypoint.sh:ro,z environment: KONG_DATABASE: "off" KONG_DECLARATIVE_CONFIG: /usr/local/kong/kong.yml KONG_ROUTER_FLAVOR: expressions KONG_DNS_ORDER: LAST,A,CNAME KONG_DNS_NOT_FOUND_TTL: 1 KONG_DNS_VALID_TTL: 5 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 SUPABASE_ANON_KEY: ${ANON_KEY} SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY} SUPABASE_PUBLISHABLE_KEY: ${SUPABASE_PUBLISHABLE_KEY:-} SUPABASE_SECRET_KEY: ${SUPABASE_SECRET_KEY:-} ANON_KEY_ASYMMETRIC: ${ANON_KEY_ASYMMETRIC:-} SERVICE_ROLE_KEY_ASYMMETRIC: ${SERVICE_ROLE_KEY_ASYMMETRIC:-} DASHBOARD_USERNAME: ${DASHBOARD_USERNAME} DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD} entrypoint: ["/bin/sh", "/home/kong/kong-entrypoint.sh"] ##################################################################### # Auth (GoTrue) ##################################################################### auth: container_name: cdrdpyj-auth image: supabase/gotrue:v2.189.0 restart: unless-stopped depends_on: db: condition: service_healthy healthcheck: test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:9999/health"] timeout: 5s interval: 5s retries: 3 environment: GOTRUE_API_HOST: 0.0.0.0 GOTRUE_API_PORT: 9999 API_EXTERNAL_URL: ${API_EXTERNAL_URL} GOTRUE_DB_DRIVER: postgres GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} GOTRUE_SITE_URL: ${SITE_URL} GOTRUE_URI_ALLOW_LIST: ${ADDITIONAL_REDIRECT_URLS} GOTRUE_DISABLE_SIGNUP: ${DISABLE_SIGNUP} GOTRUE_JWT_ADMIN_ROLES: service_role GOTRUE_JWT_AUD: authenticated GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated GOTRUE_JWT_EXP: ${JWT_EXPIRY} GOTRUE_JWT_SECRET: ${JWT_SECRET} GOTRUE_JWT_ISSUER: ${API_EXTERNAL_URL} GOTRUE_EXTERNAL_EMAIL_ENABLED: ${ENABLE_EMAIL_SIGNUP} GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: ${ENABLE_ANONYMOUS_USERS} GOTRUE_MAILER_AUTOCONFIRM: ${ENABLE_EMAIL_AUTOCONFIRM} GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_ADMIN_EMAIL} GOTRUE_SMTP_HOST: ${SMTP_HOST} GOTRUE_SMTP_PORT: ${SMTP_PORT} GOTRUE_SMTP_USER: ${SMTP_USER} GOTRUE_SMTP_PASS: ${SMTP_PASS} GOTRUE_SMTP_SENDER_NAME: ${SMTP_SENDER_NAME} GOTRUE_MAILER_URLPATHS_INVITE: ${MAILER_URLPATHS_INVITE} GOTRUE_MAILER_URLPATHS_CONFIRMATION: ${MAILER_URLPATHS_CONFIRMATION} GOTRUE_MAILER_URLPATHS_RECOVERY: ${MAILER_URLPATHS_RECOVERY} GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: ${MAILER_URLPATHS_EMAIL_CHANGE} GOTRUE_EXTERNAL_PHONE_ENABLED: ${ENABLE_PHONE_SIGNUP} GOTRUE_SMS_AUTOCONFIRM: ${ENABLE_PHONE_AUTOCONFIRM} ##################################################################### # REST (PostgREST) — auto REST API over the public schema ##################################################################### rest: container_name: cdrdpyj-rest image: postgrest/postgrest:v14.12 restart: unless-stopped depends_on: db: condition: service_healthy healthcheck: test: ["CMD", "postgrest", "--ready"] interval: 5s timeout: 5s retries: 3 environment: PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS} PGRST_DB_MAX_ROWS: ${PGRST_DB_MAX_ROWS:-1000} PGRST_DB_EXTRA_SEARCH_PATH: ${PGRST_DB_EXTRA_SEARCH_PATH:-public} PGRST_DB_ANON_ROLE: anon PGRST_ADMIN_SERVER_PORT: 3001 PGRST_ADMIN_SERVER_HOST: localhost PGRST_JWT_SECRET: ${JWT_SECRET} PGRST_DB_USE_LEGACY_GUCS: "false" PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET} PGRST_APP_SETTINGS_JWT_EXP: ${JWT_EXPIRY} command: ["postgrest"] ##################################################################### # Storage + imgproxy — file storage with on-the-fly image transforms ##################################################################### storage: container_name: cdrdpyj-storage image: supabase/storage-api:v1.60.4 restart: unless-stopped depends_on: db: condition: service_healthy rest: condition: service_started imgproxy: condition: service_started healthcheck: test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://storage:5000/status"] timeout: 5s interval: 5s retries: 3 environment: ANON_KEY: ${ANON_KEY} SERVICE_KEY: ${SERVICE_ROLE_KEY} POSTGREST_URL: http://rest:3000 AUTH_JWT_SECRET: ${JWT_SECRET} DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} STORAGE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL} REQUEST_ALLOW_X_FORWARDED_PATH: "true" FILE_SIZE_LIMIT: 52428800 STORAGE_BACKEND: file GLOBAL_S3_BUCKET: ${GLOBAL_S3_BUCKET} FILE_STORAGE_BACKEND_PATH: /var/lib/storage TENANT_ID: ${STORAGE_TENANT_ID} REGION: ${REGION} ENABLE_IMAGE_TRANSFORMATION: "true" IMGPROXY_URL: http://imgproxy:5001 S3_PROTOCOL_ACCESS_KEY_ID: ${S3_PROTOCOL_ACCESS_KEY_ID} S3_PROTOCOL_ACCESS_KEY_SECRET: ${S3_PROTOCOL_ACCESS_KEY_SECRET} volumes: - ./supabase/volumes/storage:/var/lib/storage:z imgproxy: container_name: cdrdpyj-imgproxy image: darthsim/imgproxy:v3.30.1 restart: unless-stopped volumes: - ./supabase/volumes/storage:/var/lib/storage:z healthcheck: test: ["CMD", "imgproxy", "health"] timeout: 5s interval: 5s retries: 3 environment: IMGPROXY_BIND: ":5001" IMGPROXY_LOCAL_FILESYSTEM_ROOT: / IMGPROXY_USE_ETAG: "true" IMGPROXY_AUTO_WEBP: ${IMGPROXY_AUTO_WEBP} IMGPROXY_MAX_SRC_RESOLUTION: 16.8 ##################################################################### # Applications (opt-in: `docker compose --profile apps up`) # The recommended dev loop runs these on the host via portless # (`pnpm dev`) instead, for hot-reload + *.localhost URLs. ##################################################################### cms: container_name: cdrdpyj-cms profiles: ["apps"] build: context: . dockerfile: apps/cms/Dockerfile restart: unless-stopped depends_on: db: condition: service_healthy ports: - "3000:3000" environment: NODE_ENV: production DATABASE_URI: postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} PAYLOAD_SECRET: ${PAYLOAD_SECRET} PAYLOAD_PUBLIC_SERVER_URL: http://localhost:3000 web: container_name: cdrdpyj-web profiles: ["apps"] build: context: . dockerfile: apps/web/Dockerfile restart: unless-stopped depends_on: - cms ports: - "4321:4321" environment: NODE_ENV: production HOST: 0.0.0.0 PORT: 4321 # Server-side fetches go container-to-container; browser links use localhost. PAYLOAD_API_URL: http://cms:3000/api PUBLIC_PAYLOAD_API_URL: http://localhost:3000/api PUBLIC_SUPABASE_URL: ${SUPABASE_PUBLIC_URL} PUBLIC_SUPABASE_ANON_KEY: ${ANON_KEY} volumes: db-config: