Full production-ready docker-compose stack (db, kong, auth, rest, realtime, storage, imgproxy, meta, functions, studio) targeting api.caterium.ru, plus bootstrap script for a fresh Cloud Server, Caddy reverse-proxy config (HTTPS, WebSocket, upload limits), and dump/restore/verify/storage-sync scripts for moving off the managed Supabase project (cksuehzcimitsxmeloes). Does not touch public/ or any live runtime behavior — frontend cutover is documented separately in frontend-cutover.md and only applied after Etap 8 verification. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
5.8 KiB
Self-hosted Supabase for Caterium (Timeweb Cloud Server)
Target: https://api.caterium.ru — replaces https://cksuehzcimitsxmeloes.supabase.co.
This directory is deployable as-is on a fresh Ubuntu 22.04/24.04 Timeweb Cloud Server (VPS with root).
It CANNOT run on the existing timeweb-caterium shared-hosting SSH account (ci503744@vh470.timeweb.ru) —
that account has no root, sudo is disabled (no_new_privileges), and there is no Docker daemon. Shared
hosting is fine for serving the static frontend (public/) but architecturally cannot host Postgres/Kong/
GoTrue/PostgREST/Realtime/Storage containers.
What's here
| File | Purpose |
|---|---|
bootstrap-server.sh |
Run once (as root) on a fresh Cloud Server. Installs Docker, Docker Compose plugin, Caddy, ufw rules, clones this config, brings the stack up. |
docker-compose.yml |
The Supabase stack: db, kong, auth, rest, realtime, storage, meta, functions, studio, imgproxy. |
.env.example |
Every secret/setting the stack needs. Copy to .env, fill in, chmod 600. Never commit the real .env. |
volumes/api/kong.yml |
Kong declarative routing for /rest/v1, /auth/v1, /storage/v1, /realtime/v1, /functions/v1. |
volumes/db/init/ |
One-time SQL run by the Postgres container on first boot (roles, pg_cron/extensions, JWT hook wiring). |
Caddyfile |
Reverse proxy for api.caterium.ru: automatic HTTPS (Let's Encrypt), WebSocket upgrade for Realtime, large upload limits for Storage, header forwarding. |
scripts/01-dump-old.sh |
Runs against the OLD Supabase project. Produces roles.sql, schema.sql, data.sql via supabase db dump. |
scripts/02-restore-new.sh |
Restores those dumps into the new self-hosted Postgres via psql. |
scripts/03-verify-counts.sql |
Row-count comparison old vs new (auth.users, orders, order_items, clients, catalog_items, storage.objects). |
scripts/04-deploy-edge-function.sh |
Copies supabase/functions/caterium-create-employee and caterium-platform-auth-admin into the running functions container's mounted volume and reloads it. |
scripts/05-storage-sync.sh |
Copies Storage objects (binary files) from the old project's Storage S3-compatible/API to the new bucket, preserving paths. |
Order of operations
- Provision the Cloud Server (see "Manual step required" below). Point DNS
api.caterium.ruA-record at its IP. scpthis wholeops/supabase-selfhosted/directory to the server, orgit clonethe repo there.sudo bash bootstrap-server.sh— installs Docker/Caddy, brings up the stack with placeholder secrets.- Fill in real secrets in
.env(JWT secret, DB password, SMTP, dashboard user/pass) — generate fresh ones withopenssl rand -base64 32unless told to reuse the old JWT secret.docker compose up -dagain to apply. - Run
scripts/01-dump-old.sh(needsOLD_DB_URL— the old project's Postgres connection string with password, from Supabase Dashboard → Project Settings → Database → Connection string → URI, "session pooler" disabled, direct connection). - Run
scripts/02-restore-new.shagainst the new stack'sdbcontainer. - Run
scripts/03-verify-counts.sqlold vs new and compare against the expected counts (14 / 11 / 53 / 52 / 460 / 18). - Run
scripts/05-storage-sync.shto copy the 18 Storage objects and their metadata/policies. - Run
scripts/04-deploy-edge-function.shto bringcaterium-create-employeeonline, verify with a test call. - Only after 5-9 pass: flip
DEFAULT_SUPABASE_URLinpublic/app-runtime.js, ship thelocalStoragemigration shim (seeops/supabase-selfhosted/frontend-cutover.md), commit tomain, let CI promote toproduction.
Manual step required right now (nothing beyond this is blocked)
I do not have a Timeweb Cloud API token or root SSH credentials to any Timeweb Cloud Server (VPS) — only to the existing shared-hosting account used for the static frontend, which cannot run Docker. To unblock steps 1-10:
Either
- (A) In the Timeweb Cloud panel (https://timeweb.cloud/), create a Cloud Server: Ubuntu 22.04, minimum 2 vCPU / 4 GB RAM / 50 GB NVMe (recommended: 4 vCPU / 8 GB for headroom), public IPv4, and add my SSH public key as an authorized key OR give me the root password so I can add my own key. Then tell me the server's IP.
- (B) Generate a Timeweb Cloud API token (panel → API → "Токены доступа" → create token with Cloud Servers
write scope) and give it to me; I will provision the server via the Timeweb API myself
(
POST https://api.timeweb.cloud/api/v1/servers).
Whichever you pick, also add a DNS A-record: api.caterium.ru → the new server's IP (in whatever DNS provider
currently hosts caterium.ru's zone — outside Cloudflare per your instruction not to put Cloudflare in the
backend path).
Separately, to actually migrate the data (step 5), I need the OLD Supabase project's direct Postgres
connection string with the real database password (Supabase Dashboard → Project Settings → Database →
Connection string → URI, non-pooled). I cannot retrieve this myself — it's not in any file on this machine and
I won't ask you to paste a password into a browser form on my behalf. Paste the connection string here in chat,
or set it as OLD_DB_URL in the shell before I run scripts/01-dump-old.sh for you. If you'd rather I don't
see the raw password in chat, run scripts/01-dump-old.sh yourself with OLD_DB_URL exported and send me the
three resulting .sql files (or their location on this machine) and I'll take it from there.
I will use a freshly generated JWT secret for the new stack (per your instruction that this is acceptable) —
existing sessions will be invalidated and all 14 users will need to log in once more with their existing
email+password (which migrate unchanged, since auth.users.encrypted_password is copied byte-for-byte from
the dump).