Ops: add self-hosted Supabase stack for Timeweb Cloud migration
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>
This commit is contained in:
parent
62c37ebc2d
commit
88b7f55db4
92
ops/supabase-selfhosted/.env.example
Normal file
92
ops/supabase-selfhosted/.env.example
Normal file
@ -0,0 +1,92 @@
|
||||
# Copy to .env, fill in real values, `chmod 600 .env`. Never commit the real file.
|
||||
# Generate secrets with: openssl rand -base64 32 (or -hex 32 for hex-only fields)
|
||||
|
||||
############
|
||||
# Postgres #
|
||||
############
|
||||
POSTGRES_HOST=db
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_DB=postgres
|
||||
POSTGRES_PASSWORD=REPLACE_WITH_STRONG_RANDOM_PASSWORD
|
||||
|
||||
#######
|
||||
# JWT #
|
||||
#######
|
||||
# 40+ char random string. New secret per README (old sessions invalidate; users log in again).
|
||||
JWT_SECRET=REPLACE_WITH_openssl_rand_base64_40
|
||||
JWT_EXPIRY=3600
|
||||
# Derive ANON_KEY / SERVICE_ROLE_KEY from JWT_SECRET with the Supabase JWT generator
|
||||
# (https://supabase.com/docs/guides/self-hosting/docker#generate-api-keys) — role: anon / service_role.
|
||||
ANON_KEY=REPLACE_WITH_GENERATED_ANON_JWT
|
||||
SERVICE_ROLE_KEY=REPLACE_WITH_GENERATED_SERVICE_ROLE_JWT
|
||||
|
||||
########
|
||||
# Kong #
|
||||
########
|
||||
KONG_HTTP_PORT=8000
|
||||
KONG_HTTPS_PORT=8443
|
||||
|
||||
#########
|
||||
# Studio #
|
||||
#########
|
||||
STUDIO_DEFAULT_ORGANIZATION=Caterium
|
||||
STUDIO_DEFAULT_PROJECT=caterium-selfhosted
|
||||
STUDIO_PORT=3000
|
||||
# Basic-auth in front of Studio via Caddy — separate from Supabase dashboard user below.
|
||||
DASHBOARD_USERNAME=REPLACE_ME
|
||||
DASHBOARD_PASSWORD=REPLACE_WITH_STRONG_RANDOM_PASSWORD
|
||||
|
||||
########
|
||||
# API #
|
||||
########
|
||||
API_EXTERNAL_URL=https://api.caterium.ru
|
||||
SUPABASE_PUBLIC_URL=https://api.caterium.ru
|
||||
SITE_URL=https://app.caterium.ru
|
||||
ADDITIONAL_REDIRECT_URLS=https://app.caterium.ru/**
|
||||
|
||||
#########
|
||||
# Auth #
|
||||
#########
|
||||
DISABLE_SIGNUP=false
|
||||
ENABLE_EMAIL_SIGNUP=true
|
||||
ENABLE_EMAIL_AUTOCONFIRM=false
|
||||
ENABLE_PHONE_SIGNUP=false
|
||||
ENABLE_ANONYMOUS_USERS=false
|
||||
|
||||
########
|
||||
# SMTP #
|
||||
########
|
||||
SMTP_ADMIN_EMAIL=noreply@caterium.ru
|
||||
SMTP_HOST=REPLACE_ME
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=REPLACE_ME
|
||||
SMTP_PASS=REPLACE_ME
|
||||
SMTP_SENDER_NAME=Caterium
|
||||
|
||||
###########
|
||||
# Storage #
|
||||
###########
|
||||
STORAGE_BACKEND=file
|
||||
FILE_SIZE_LIMIT=52428800
|
||||
STORAGE_FILE_SIZE_LIMIT=52428800
|
||||
|
||||
###########
|
||||
# Realtime #
|
||||
###########
|
||||
REALTIME_ENCRYPTION_KEY=REPLACE_WITH_openssl_rand_hex_16
|
||||
REALTIME_SECRET_KEY_BASE=REPLACE_WITH_openssl_rand_base64_64
|
||||
|
||||
##############
|
||||
# Functions #
|
||||
##############
|
||||
FUNCTIONS_VERIFY_JWT=false
|
||||
|
||||
##############
|
||||
# pg_meta #
|
||||
##############
|
||||
PGMETA_PORT=8080
|
||||
|
||||
############
|
||||
# Postgres image extras
|
||||
############
|
||||
POOLER_TENANT_ID=caterium
|
||||
53
ops/supabase-selfhosted/Caddyfile
Normal file
53
ops/supabase-selfhosted/Caddyfile
Normal file
@ -0,0 +1,53 @@
|
||||
{
|
||||
email admin@caterium.ru
|
||||
}
|
||||
|
||||
api.caterium.ru {
|
||||
encode gzip zstd
|
||||
|
||||
# Storage uploads (photos, docs) — match FILE_SIZE_LIMIT in .env (50 MB default).
|
||||
request_body {
|
||||
max_size 50MB
|
||||
}
|
||||
|
||||
# WebSocket upgrade for Realtime (Kong proxies to the realtime service).
|
||||
@websockets {
|
||||
header Connection *Upgrade*
|
||||
header Upgrade websocket
|
||||
}
|
||||
reverse_proxy @websockets 127.0.0.1:8000
|
||||
|
||||
reverse_proxy 127.0.0.1:8000 {
|
||||
header_up Host {host}
|
||||
header_up X-Real-IP {remote_host}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
flush_interval -1
|
||||
}
|
||||
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||
X-Content-Type-Options "nosniff"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
}
|
||||
|
||||
log {
|
||||
output file /var/log/caddy/api.caterium.ru.log {
|
||||
roll_size 50mb
|
||||
roll_keep 5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Supabase Studio (admin dashboard) — not part of the public API surface.
|
||||
# Kept on a separate host, gated by HTTP basic auth (DASHBOARD_USERNAME/PASSWORD from .env,
|
||||
# hash with `caddy hash-password`), and NOT referenced by the Caterium frontend at all.
|
||||
# Point a DNS record at this host only if/when you want browser access; otherwise reach
|
||||
# Studio via `ssh -L 3000:127.0.0.1:3000 timeweb-caterium-vps` and open http://127.0.0.1:3000.
|
||||
studio.caterium.ru {
|
||||
basic_auth {
|
||||
# Replace with: caddy hash-password --plaintext '<DASHBOARD_PASSWORD>'
|
||||
{$DASHBOARD_USERNAME} {$DASHBOARD_PASSWORD_HASH}
|
||||
}
|
||||
reverse_proxy 127.0.0.1:3000
|
||||
}
|
||||
72
ops/supabase-selfhosted/README.md
Normal file
72
ops/supabase-selfhosted/README.md
Normal file
@ -0,0 +1,72 @@
|
||||
# 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
|
||||
|
||||
1. Provision the Cloud Server (see "Manual step required" below). Point DNS `api.caterium.ru` A-record at its IP.
|
||||
2. `scp` this whole `ops/supabase-selfhosted/` directory to the server, or `git clone` the repo there.
|
||||
3. `sudo bash bootstrap-server.sh` — installs Docker/Caddy, brings up the stack with placeholder secrets.
|
||||
4. Fill in real secrets in `.env` (JWT secret, DB password, SMTP, dashboard user/pass) — generate fresh ones with
|
||||
`openssl rand -base64 32` unless told to reuse the old JWT secret. `docker compose up -d` again to apply.
|
||||
5. Run `scripts/01-dump-old.sh` (needs `OLD_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).
|
||||
6. Run `scripts/02-restore-new.sh` against the new stack's `db` container.
|
||||
7. Run `scripts/03-verify-counts.sql` old vs new and compare against the expected counts (14 / 11 / 53 / 52 / 460 / 18).
|
||||
8. Run `scripts/05-storage-sync.sh` to copy the 18 Storage objects and their metadata/policies.
|
||||
9. Run `scripts/04-deploy-edge-function.sh` to bring `caterium-create-employee` online, verify with a test call.
|
||||
10. Only after 5-9 pass: flip `DEFAULT_SUPABASE_URL` in `public/app-runtime.js`, ship the `localStorage` migration
|
||||
shim (see `ops/supabase-selfhosted/frontend-cutover.md`), commit to `main`, let CI promote to `production`.
|
||||
|
||||
## 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).
|
||||
86
ops/supabase-selfhosted/bootstrap-server.sh
Normal file
86
ops/supabase-selfhosted/bootstrap-server.sh
Normal file
@ -0,0 +1,86 @@
|
||||
#!/bin/bash
|
||||
# Run as root, once, on a fresh Ubuntu 22.04/24.04 Timeweb Cloud Server (VPS).
|
||||
# Usage: sudo bash bootstrap-server.sh
|
||||
set -euo pipefail
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Run as root: sudo bash bootstrap-server.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
echo "== Installing Docker Engine + Compose plugin =="
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq ca-certificates curl gnupg ufw
|
||||
install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
|
||||
chmod a+r /etc/apt/keyrings/docker.asc
|
||||
. /etc/os-release
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu ${VERSION_CODENAME} stable" \
|
||||
> /etc/apt/sources.list.d/docker.list
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||||
systemctl enable --now docker
|
||||
|
||||
echo "== Installing Caddy (reverse proxy, automatic HTTPS) =="
|
||||
apt-get install -y -qq debian-keyring debian-archive-keyring apt-transport-https
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' > /etc/apt/sources.list.d/caddy-stable.list
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq caddy
|
||||
mkdir -p /var/log/caddy
|
||||
cp "$SELF_DIR/Caddyfile" /etc/caddy/Caddyfile
|
||||
systemctl enable --now caddy
|
||||
|
||||
echo "== Firewall =="
|
||||
ufw allow OpenSSH
|
||||
ufw allow 80/tcp
|
||||
ufw allow 443/tcp
|
||||
ufw --force enable
|
||||
|
||||
echo "== Preparing app directory =="
|
||||
APP_DIR=/opt/caterium-supabase
|
||||
mkdir -p "$APP_DIR"
|
||||
cp -r "$SELF_DIR"/* "$APP_DIR"/
|
||||
mkdir -p "$APP_DIR/volumes/db/data" "$APP_DIR/volumes/storage"
|
||||
|
||||
if [ ! -f "$APP_DIR/.env" ]; then
|
||||
cp "$APP_DIR/.env.example" "$APP_DIR/.env"
|
||||
chmod 600 "$APP_DIR/.env"
|
||||
echo ""
|
||||
echo "!! $APP_DIR/.env created from template with PLACEHOLDER secrets."
|
||||
echo "!! Edit it with real values (see README.md) BEFORE exposing this server publicly."
|
||||
echo "!! Then run: cd $APP_DIR && docker compose up -d"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "== systemd unit for auto-restart on reboot/crash =="
|
||||
cat > /etc/systemd/system/caterium-supabase.service <<'UNIT'
|
||||
[Unit]
|
||||
Description=Caterium self-hosted Supabase stack
|
||||
Requires=docker.service
|
||||
After=docker.service network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
WorkingDirectory=/opt/caterium-supabase
|
||||
ExecStart=/usr/bin/docker compose up -d
|
||||
ExecStop=/usr/bin/docker compose down
|
||||
TimeoutStartSec=0
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNIT
|
||||
systemctl daemon-reload
|
||||
systemctl enable caterium-supabase.service
|
||||
|
||||
echo ""
|
||||
echo "Bootstrap complete. Next steps:"
|
||||
echo "1. Edit $APP_DIR/.env with real secrets (openssl rand -base64 32 for each REPLACE_ME)."
|
||||
echo "2. cd $APP_DIR && docker compose up -d"
|
||||
echo "3. docker compose ps # confirm all services are healthy"
|
||||
echo "4. Confirm DNS: api.caterium.ru A-record points at this server's IP (Caddy needs it for Let's Encrypt)."
|
||||
echo "5. curl -I https://api.caterium.ru/rest/v1/ # should return 200/401, not a connection error"
|
||||
198
ops/supabase-selfhosted/docker-compose.yml
Normal file
198
ops/supabase-selfhosted/docker-compose.yml
Normal file
@ -0,0 +1,198 @@
|
||||
name: caterium-supabase
|
||||
|
||||
services:
|
||||
db:
|
||||
image: supabase/postgres:15.6.1.146
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:5432:5432"
|
||||
volumes:
|
||||
- ./volumes/db/data:/var/lib/postgresql/data
|
||||
- ./volumes/db/init:/docker-entrypoint-initdb.d
|
||||
environment:
|
||||
POSTGRES_HOST: /var/run/postgresql
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
JWT_EXP: ${JWT_EXPIRY}
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready", "-U", "postgres", "-h", "localhost"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
|
||||
auth:
|
||||
image: supabase/gotrue:v2.164.0
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
GOTRUE_API_HOST: 0.0.0.0
|
||||
GOTRUE_API_PORT: 9999
|
||||
API_EXTERNAL_URL: ${API_EXTERNAL_URL}
|
||||
GOTRUE_SITE_URL: ${SITE_URL}
|
||||
GOTRUE_URI_ALLOW_LIST: ${ADDITIONAL_REDIRECT_URLS}
|
||||
GOTRUE_DISABLE_SIGNUP: ${DISABLE_SIGNUP}
|
||||
GOTRUE_DB_DRIVER: postgres
|
||||
GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
||||
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_EXTERNAL_EMAIL_ENABLED: ${ENABLE_EMAIL_SIGNUP}
|
||||
GOTRUE_MAILER_AUTOCONFIRM: ${ENABLE_EMAIL_AUTOCONFIRM}
|
||||
GOTRUE_EXTERNAL_PHONE_ENABLED: ${ENABLE_PHONE_SIGNUP}
|
||||
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: ${ENABLE_ANONYMOUS_USERS}
|
||||
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}
|
||||
|
||||
rest:
|
||||
image: postgrest/postgrest:v12.2.8
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
||||
PGRST_DB_SCHEMAS: public,storage,graphql_public
|
||||
PGRST_DB_ANON_ROLE: anon
|
||||
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}
|
||||
|
||||
realtime:
|
||||
image: supabase/realtime:v2.34.47
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
PORT: 4000
|
||||
DB_HOST: db
|
||||
DB_PORT: 5432
|
||||
DB_USER: supabase_admin
|
||||
DB_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
DB_NAME: ${POSTGRES_DB}
|
||||
DB_AFTER_CONNECT_QUERY: "SET search_path TO _realtime"
|
||||
DB_ENC_KEY: ${REALTIME_ENCRYPTION_KEY}
|
||||
API_JWT_SECRET: ${JWT_SECRET}
|
||||
SECRET_KEY_BASE: ${REALTIME_SECRET_KEY_BASE}
|
||||
ERL_AFLAGS: -proto_dist inet_tcp
|
||||
DNS_NODES: "''"
|
||||
RLIMIT_NOFILE: "10000"
|
||||
APP_NAME: realtime
|
||||
SEED_SELF_HOST: "true"
|
||||
|
||||
storage:
|
||||
image: supabase/storage-api:v1.19.3
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
rest:
|
||||
condition: service_started
|
||||
volumes:
|
||||
- ./volumes/storage:/var/lib/storage
|
||||
environment:
|
||||
ANON_KEY: ${ANON_KEY}
|
||||
SERVICE_KEY: ${SERVICE_ROLE_KEY}
|
||||
POSTGREST_URL: http://rest:3000
|
||||
PGRST_JWT_SECRET: ${JWT_SECRET}
|
||||
DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
||||
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT}
|
||||
STORAGE_BACKEND: ${STORAGE_BACKEND}
|
||||
FILE_STORAGE_BACKEND_PATH: /var/lib/storage
|
||||
TENANT_ID: caterium
|
||||
REGION: local
|
||||
GLOBAL_S3_BUCKET: caterium
|
||||
|
||||
imgproxy:
|
||||
image: darthsim/imgproxy:v3.24
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./volumes/storage:/var/lib/storage:ro
|
||||
environment:
|
||||
IMGPROXY_LOCAL_FILESYSTEM_ROOT: /
|
||||
IMGPROXY_USE_ETAG: "true"
|
||||
|
||||
meta:
|
||||
image: supabase/postgres-meta:v0.87.1
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
PG_META_PORT: 8080
|
||||
PG_META_DB_HOST: db
|
||||
PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
|
||||
functions:
|
||||
image: supabase/edge-runtime:v1.67.4
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ../../supabase/functions:/home/deno/functions:Z
|
||||
command: ["start", "--main-service", "/home/deno/functions"]
|
||||
environment:
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
SUPABASE_URL: http://kong:8000
|
||||
SUPABASE_ANON_KEY: ${ANON_KEY}
|
||||
SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
|
||||
SUPABASE_DB_URL: postgres://postgres:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
||||
VERIFY_JWT: ${FUNCTIONS_VERIFY_JWT}
|
||||
|
||||
studio:
|
||||
image: supabase/studio:20250203-e853533
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- meta
|
||||
- rest
|
||||
ports:
|
||||
- "127.0.0.1:${STUDIO_PORT}:3000"
|
||||
environment:
|
||||
STUDIO_PG_META_URL: http://meta:8080
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
DEFAULT_ORGANIZATION_NAME: ${STUDIO_DEFAULT_ORGANIZATION}
|
||||
DEFAULT_PROJECT_NAME: ${STUDIO_DEFAULT_PROJECT}
|
||||
SUPABASE_URL: http://kong:8000
|
||||
SUPABASE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL}
|
||||
SUPABASE_ANON_KEY: ${ANON_KEY}
|
||||
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
|
||||
|
||||
kong:
|
||||
image: kong:2.8.1
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- auth
|
||||
- rest
|
||||
- realtime
|
||||
- storage
|
||||
- functions
|
||||
- meta
|
||||
ports:
|
||||
- "127.0.0.1:${KONG_HTTP_PORT}:8000/tcp"
|
||||
volumes:
|
||||
- ./volumes/api/kong.yml:/home/kong/kong.yml.template:ro
|
||||
entrypoint: >
|
||||
bash -c 'set -e; apt-get -qq update >/dev/null 2>&1 || true;
|
||||
command -v envsubst >/dev/null || (apt-get -qq install -y gettext-base >/dev/null 2>&1);
|
||||
envsubst < /home/kong/kong.yml.template > /home/kong/kong.yml;
|
||||
exec /docker-entrypoint.sh kong docker-start'
|
||||
environment:
|
||||
KONG_DATABASE: "off"
|
||||
KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml
|
||||
KONG_DNS_ORDER: LAST,A,CNAME
|
||||
KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth
|
||||
KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k
|
||||
KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k
|
||||
SUPABASE_ANON_KEY: ${ANON_KEY}
|
||||
SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
|
||||
103
ops/supabase-selfhosted/frontend-cutover.md
Normal file
103
ops/supabase-selfhosted/frontend-cutover.md
Normal file
@ -0,0 +1,103 @@
|
||||
# Frontend cutover (Etap 9-11) — apply only after Etap 8 counts match
|
||||
|
||||
Two changes to `public/app-runtime.js`, both required together. Do not commit either until
|
||||
`scripts/03-verify-counts.sql` shows matching counts on old vs new (14/11/53/52/460/18) and
|
||||
the Etap 10 smoke test has passed against `https://api.caterium.ru` directly (before touching
|
||||
the frontend).
|
||||
|
||||
## 1. Point the default at the new backend
|
||||
|
||||
```js
|
||||
// public/app-runtime.js, line ~1989-1990
|
||||
const DEFAULT_SUPABASE_URL = 'https://api.caterium.ru';
|
||||
const DEFAULT_SUPABASE_KEY = '<NEW_ANON_KEY_FROM_.env>';
|
||||
```
|
||||
|
||||
## 2. Auto-migrate devices that already have the OLD url cached in `localStorage`
|
||||
|
||||
Every Caterium device has `sunCloudV2Config` (and possibly the legacy `sunEnterpriseSettingsV1`)
|
||||
in `localStorage`, which `loadConfig()` reads *before* falling back to `DEFAULT_SUPABASE_URL`.
|
||||
Just changing the default does nothing for those devices — they keep talking to
|
||||
`cksuehzcimitsxmeloes.supabase.co` forever unless we rewrite the stored value once. Insert this
|
||||
right after the existing `DEFAULT_SUPABASE_KEY` line, and call it from inside `loadConfig()`
|
||||
before the `return { ... }`:
|
||||
|
||||
```js
|
||||
const DEFAULT_SUPABASE_URL = 'https://api.caterium.ru';
|
||||
const DEFAULT_SUPABASE_KEY = '<NEW_ANON_KEY_FROM_.env>';
|
||||
const LEGACY_SUPABASE_URL = 'https://cksuehzcimitsxmeloes.supabase.co';
|
||||
|
||||
function migrateLegacySupabaseUrl(value) {
|
||||
// One-time, silent migration: any device still pointed at the retired managed
|
||||
// Supabase project is switched to the self-hosted backend automatically. Users
|
||||
// are never asked to clear localStorage or reconfigure anything.
|
||||
if (String(value.url || '').trim().replace(/\/$/, '') === LEGACY_SUPABASE_URL) {
|
||||
value.url = DEFAULT_SUPABASE_URL;
|
||||
value.key = DEFAULT_SUPABASE_KEY;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
```
|
||||
|
||||
Then in `loadConfig()`, wrap the legacy-settings merge:
|
||||
|
||||
```js
|
||||
function loadConfig() {
|
||||
let value = {};
|
||||
try { value = JSON.parse(localStorage.getItem(CONFIG_KEY) || '{}') || {}; } catch (_) {}
|
||||
if (!value.url || !value.key) {
|
||||
try {
|
||||
const legacy = JSON.parse(localStorage.getItem('sunEnterpriseSettingsV1') || '{}') || {};
|
||||
const cloud = legacy.cloud || {};
|
||||
value.url ||= String(cloud.supabaseUrl || '').trim();
|
||||
value.key ||= String(cloud.anonKey || '').trim();
|
||||
} catch (_) {}
|
||||
}
|
||||
value = migrateLegacySupabaseUrl(value); // <-- add this line
|
||||
return {
|
||||
url: String(value.url || DEFAULT_SUPABASE_URL || '').trim().replace(/\/$/, ''),
|
||||
key: String(value.key || DEFAULT_SUPABASE_KEY || '').trim(),
|
||||
...
|
||||
```
|
||||
|
||||
`saveConfig()` (a few lines below) persists `config` back to `localStorage` on every config
|
||||
change already, so the corrected URL/key get written back to `sunCloudV2Config` the first time
|
||||
`loadConfig()` runs on each device — no explicit persistence call needed here, but if a save
|
||||
is not triggered naturally during your smoke test, call `saveConfig()` once right after
|
||||
`config = loadConfig();` at module init (line ~1992) to force-persist the migration immediately
|
||||
rather than waiting for the next unrelated write.
|
||||
|
||||
## 3. Session invalidation is expected
|
||||
|
||||
Because the new stack uses a freshly generated `JWT_SECRET` (README decision, since the old
|
||||
one can't be safely retrieved), any cached Supabase auth session (JWT) in `localStorage`
|
||||
becomes invalid the moment the client points at the new URL — `supabase-js` will see a 401 on
|
||||
its first call, clear the stale session automatically, and the user lands back on the Caterium
|
||||
login screen. That matches the task's accepted trade-off: users log in once more with their
|
||||
existing email + password (unchanged, since `auth.users.encrypted_password` is copied
|
||||
byte-for-byte). No code change needed for this part — it's `supabase-js`'s default behavior.
|
||||
|
||||
## Checklist before running this cutover
|
||||
|
||||
- [ ] `scripts/03-verify-counts.sql` matches old vs new (14/11/53/52/460/18)
|
||||
- [ ] `curl https://api.caterium.ru/rest/v1/`, `/auth/v1/`, `/storage/v1/`, `/functions/v1/` all respond (not connection errors)
|
||||
- [ ] Realtime WebSocket connects: `wscat -c wss://api.caterium.ru/realtime/v1/websocket?apikey=<ANON_KEY>`
|
||||
- [ ] `scripts/05-storage-sync.sh` finished, spot-checked photo URLs load
|
||||
- [ ] `scripts/04-deploy-edge-function.sh` shows `caterium-create-employee` rejecting an
|
||||
unauthenticated call with 401/400 (not 5xx)
|
||||
- [ ] Manual login test against `https://api.caterium.ru` directly (temporarily point a local
|
||||
dev copy of `app-runtime.js` at it) with one real existing account, confirm orders/
|
||||
clients/catalog load
|
||||
|
||||
Only once every box above is checked: apply the two edits above, commit to `main` with a
|
||||
message like `Cutover: point Caterium at self-hosted Supabase (api.caterium.ru)`, push. CI
|
||||
(`Caterium QA` -> `Caterium Promote Production`) auto-promotes `main` to `production` on green,
|
||||
and the existing Timeweb cron (`ops/timeweb/caterium-production-sync.sh`) deploys it to
|
||||
`app.caterium.ru` within 5 minutes. Do NOT skip CI (`--no-verify` or manual production-branch
|
||||
pushes) for this change — the QA workflow's Playwright e2e run is exactly the safety net you
|
||||
want on a backend cutover.
|
||||
|
||||
After confirming `https://app.caterium.ru` works end-to-end with no `supabase.co` requests in
|
||||
DevTools Network, leave the old managed Supabase project (`cksuehzcimitsxmeloes`) running
|
||||
untouched as a cold backup, per the task's explicit instruction — do not pause or delete it
|
||||
without separate owner confirmation.
|
||||
42
ops/supabase-selfhosted/scripts/01-dump-old.sh
Normal file
42
ops/supabase-selfhosted/scripts/01-dump-old.sh
Normal file
@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
# Dumps the OLD managed Supabase project (cksuehzcimitsxmeloes). Read-only against the
|
||||
# old project — nothing here modifies or disables the old Supabase in any way.
|
||||
#
|
||||
# Requires: Supabase CLI (npm i -g supabase, or https://github.com/supabase/cli/releases)
|
||||
# Requires: OLD_DB_URL env var — the old project's DIRECT (non-pooled) Postgres connection
|
||||
# string with the real password, from:
|
||||
# Supabase Dashboard -> Project Settings -> Database -> Connection string -> URI
|
||||
# e.g. postgres://postgres:REAL_PASSWORD@db.cksuehzcimitsxmeloes.supabase.co:5432/postgres
|
||||
set -euo pipefail
|
||||
|
||||
: "${OLD_DB_URL:?Set OLD_DB_URL to the old project's direct Postgres connection string first}"
|
||||
|
||||
OUT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/dumps"
|
||||
mkdir -p "$OUT_DIR"
|
||||
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
|
||||
command -v supabase >/dev/null 2>&1 || { echo "Install the Supabase CLI first." >&2; exit 1; }
|
||||
|
||||
echo "== Dumping roles =="
|
||||
supabase db dump --db-url "$OLD_DB_URL" -f "$OUT_DIR/roles.sql" --role-only
|
||||
|
||||
echo "== Dumping schema (structure, functions, triggers, RLS policies, grants) =="
|
||||
supabase db dump --db-url "$OLD_DB_URL" -f "$OUT_DIR/schema.sql"
|
||||
|
||||
echo "== Dumping data (COPY format, all public + auth data) =="
|
||||
supabase db dump --db-url "$OLD_DB_URL" -f "$OUT_DIR/data.sql" --use-copy --data-only
|
||||
|
||||
echo "== Row counts in the OLD project (sanity baseline) =="
|
||||
psql "$OLD_DB_URL" -Atc "
|
||||
select 'auth.users', count(*) from auth.users
|
||||
union all select 'sun_v17_orders', count(*) from public.sun_v17_orders
|
||||
union all select 'sun_v17_order_items', count(*) from public.sun_v17_order_items
|
||||
union all select 'sun_v17_clients', count(*) from public.sun_v17_clients
|
||||
union all select 'sun_v17_catalog_items', count(*) from public.sun_v17_catalog_items
|
||||
union all select 'storage.objects', count(*) from storage.objects
|
||||
" | tee "$OUT_DIR/old-counts-$STAMP.txt"
|
||||
|
||||
echo ""
|
||||
echo "Dumps written to $OUT_DIR (roles.sql, schema.sql, data.sql)."
|
||||
echo "These contain real user data (password hashes, PII) — keep them off any public path"
|
||||
echo "and delete once the migration is verified. Next: scripts/02-restore-new.sh"
|
||||
33
ops/supabase-selfhosted/scripts/02-restore-new.sh
Normal file
33
ops/supabase-selfhosted/scripts/02-restore-new.sh
Normal file
@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
# Restores roles.sql/schema.sql/data.sql (from 01-dump-old.sh) into the NEW self-hosted
|
||||
# Postgres. Run this ON the Cloud Server, from /opt/caterium-supabase, after `docker
|
||||
# compose up -d` has the db container healthy.
|
||||
set -euo pipefail
|
||||
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
DUMPS_DIR="$DIR/dumps"
|
||||
[ -f "$DIR/.env" ] && set -a && . "$DIR/.env" && set +a
|
||||
|
||||
: "${POSTGRES_PASSWORD:?POSTGRES_PASSWORD missing — check .env}"
|
||||
NEW_DB_URL="postgres://postgres:${POSTGRES_PASSWORD}@127.0.0.1:5432/postgres"
|
||||
|
||||
for f in roles.sql schema.sql data.sql; do
|
||||
[ -f "$DUMPS_DIR/$f" ] || { echo "Missing $DUMPS_DIR/$f — run 01-dump-old.sh first" >&2; exit 1; }
|
||||
done
|
||||
|
||||
echo "== Restoring roles =="
|
||||
# Managed Supabase pre-creates supabase_admin/authenticator/etc; the self-hosted image
|
||||
# does too, so role-creation conflicts here are expected and safe to ignore.
|
||||
psql "$NEW_DB_URL" -v ON_ERROR_STOP=0 -f "$DUMPS_DIR/roles.sql" || true
|
||||
|
||||
echo "== Restoring schema (public + auth + storage structures, RLS, functions, triggers, grants) =="
|
||||
psql "$NEW_DB_URL" -v ON_ERROR_STOP=1 -f "$DUMPS_DIR/schema.sql"
|
||||
|
||||
echo "== Restoring data =="
|
||||
psql "$NEW_DB_URL" -v ON_ERROR_STOP=1 -f "$DUMPS_DIR/data.sql"
|
||||
|
||||
echo ""
|
||||
echo "Restore complete. Next: psql against NEW_DB_URL with scripts/03-verify-counts.sql"
|
||||
echo "and compare against the OLD counts captured by 01-dump-old.sh."
|
||||
echo ""
|
||||
echo "NEW_DB_URL=$NEW_DB_URL"
|
||||
23
ops/supabase-selfhosted/scripts/03-verify-counts.sql
Normal file
23
ops/supabase-selfhosted/scripts/03-verify-counts.sql
Normal file
@ -0,0 +1,23 @@
|
||||
-- Run against both OLD_DB_URL and the new self-hosted DB, compare results.
|
||||
-- psql "$OLD_DB_URL" -f 03-verify-counts.sql
|
||||
-- psql "$NEW_DB_URL" -f 03-verify-counts.sql
|
||||
--
|
||||
-- Expected (Etap 8):
|
||||
-- auth.users 14
|
||||
-- sun_v17_orders 11
|
||||
-- sun_v17_order_items 53
|
||||
-- sun_v17_clients 52
|
||||
-- sun_v17_catalog_items 460
|
||||
-- storage.objects 18
|
||||
select 'auth.users' as table_name, count(*) as row_count from auth.users
|
||||
union all
|
||||
select 'sun_v17_orders', count(*) from public.sun_v17_orders
|
||||
union all
|
||||
select 'sun_v17_order_items', count(*) from public.sun_v17_order_items
|
||||
union all
|
||||
select 'sun_v17_clients', count(*) from public.sun_v17_clients
|
||||
union all
|
||||
select 'sun_v17_catalog_items', count(*) from public.sun_v17_catalog_items
|
||||
union all
|
||||
select 'storage.objects', count(*) from storage.objects
|
||||
order by 1;
|
||||
33
ops/supabase-selfhosted/scripts/04-deploy-edge-function.sh
Normal file
33
ops/supabase-selfhosted/scripts/04-deploy-edge-function.sh
Normal file
@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
# The `functions` container in docker-compose.yml already mounts ../../supabase/functions
|
||||
# (i.e. this repo's supabase/functions/) straight into the edge-runtime container, so both
|
||||
# caterium-create-employee and caterium-platform-auth-admin come up automatically with
|
||||
# `docker compose up -d` — no separate "deploy" step is normally needed. This script just
|
||||
# verifies the function is live and enforces the same security checks it had before
|
||||
# (JWT verification, workspace/role validation happen inside the function itself via its
|
||||
# calls to sun_employee_prepare_v28 / RLS, unchanged).
|
||||
set -euo pipefail
|
||||
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
[ -f "$DIR/.env" ] && set -a && . "$DIR/.env" && set +a
|
||||
|
||||
: "${API_EXTERNAL_URL:?Missing in .env}"
|
||||
: "${ANON_KEY:?Missing in .env}"
|
||||
|
||||
echo "== Restarting functions container to pick up any source changes =="
|
||||
(cd "$DIR" && docker compose restart functions)
|
||||
sleep 3
|
||||
|
||||
echo "== Health check: caterium-create-employee should reject unauthenticated calls (expect 401/400, not 5xx/timeout) =="
|
||||
curl -sS -o /tmp/fn-check.json -w 'HTTP %{http_code}\n' \
|
||||
-X POST "$API_EXTERNAL_URL/functions/v1/caterium-create-employee" \
|
||||
-H "apikey: $ANON_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
cat /tmp/fn-check.json; echo
|
||||
|
||||
echo ""
|
||||
echo "CORS: function itself sets Access-Control-Allow-Origin: *; Kong additionally restricts"
|
||||
echo "the /functions/v1/ route to https://app.caterium.ru in volumes/api/kong.yml."
|
||||
echo "If you need to tighten the function's own CORS to just app.caterium.ru, edit the"
|
||||
echo "'cors' const in supabase/functions/caterium-create-employee/index.ts."
|
||||
54
ops/supabase-selfhosted/scripts/05-storage-sync.sh
Normal file
54
ops/supabase-selfhosted/scripts/05-storage-sync.sh
Normal file
@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
# Copies Storage objects (binary files) from the OLD managed Supabase project's Storage
|
||||
# API to the new self-hosted Storage API, preserving bucket names, object paths and
|
||||
# content-type. Bucket definitions + storage.objects METADATA rows are already restored
|
||||
# by 02-restore-new.sh (they live in the `storage` schema, covered by schema.sql/data.sql)
|
||||
# — this script only moves the actual binary blobs, which live outside Postgres.
|
||||
set -euo pipefail
|
||||
|
||||
: "${OLD_SUPABASE_URL:?e.g. https://cksuehzcimitsxmeloes.supabase.co}"
|
||||
: "${OLD_SERVICE_ROLE_KEY:?Old project's service_role key (Dashboard -> Project Settings -> API)}"
|
||||
: "${NEW_SUPABASE_URL:?e.g. https://api.caterium.ru}"
|
||||
: "${NEW_SERVICE_ROLE_KEY:?New stack's SERVICE_ROLE_KEY from .env}"
|
||||
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
echo "== Listing buckets on OLD project =="
|
||||
BUCKETS=$(curl -sf "$OLD_SUPABASE_URL/storage/v1/bucket" \
|
||||
-H "Authorization: Bearer $OLD_SERVICE_ROLE_KEY" \
|
||||
-H "apikey: $OLD_SERVICE_ROLE_KEY" | jq -r '.[].id')
|
||||
|
||||
for bucket in $BUCKETS; do
|
||||
echo "== Bucket: $bucket =="
|
||||
OBJECTS=$(curl -sf -X POST "$OLD_SUPABASE_URL/storage/v1/object/list/$bucket" \
|
||||
-H "Authorization: Bearer $OLD_SERVICE_ROLE_KEY" \
|
||||
-H "apikey: $OLD_SERVICE_ROLE_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"limit": 1000, "offset": 0, "prefix": ""}' | jq -r '.[] | select(.id != null) | .name')
|
||||
|
||||
count=0
|
||||
while IFS= read -r path; do
|
||||
[ -z "$path" ] && continue
|
||||
local_file="$TMP_DIR/$(basename "$path")"
|
||||
curl -sf "$OLD_SUPABASE_URL/storage/v1/object/$bucket/$path" \
|
||||
-H "Authorization: Bearer $OLD_SERVICE_ROLE_KEY" \
|
||||
-H "apikey: $OLD_SERVICE_ROLE_KEY" \
|
||||
-o "$local_file"
|
||||
|
||||
curl -sf -X POST "$NEW_SUPABASE_URL/storage/v1/object/$bucket/$path" \
|
||||
-H "Authorization: Bearer $NEW_SERVICE_ROLE_KEY" \
|
||||
-H "apikey: $NEW_SERVICE_ROLE_KEY" \
|
||||
-H "x-upsert: true" \
|
||||
--data-binary "@$local_file" > /dev/null
|
||||
|
||||
rm -f "$local_file"
|
||||
count=$((count + 1))
|
||||
echo " copied: $bucket/$path"
|
||||
done <<< "$OBJECTS"
|
||||
echo " -> $count object(s) copied for bucket $bucket"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Done. Spot-check: open a known photo URL under $NEW_SUPABASE_URL/storage/v1/object/public/<bucket>/<path>"
|
||||
echo "and confirm it renders identically to the old one."
|
||||
112
ops/supabase-selfhosted/volumes/api/kong.yml
Normal file
112
ops/supabase-selfhosted/volumes/api/kong.yml
Normal file
@ -0,0 +1,112 @@
|
||||
_format_version: "2.1"
|
||||
_transform: true
|
||||
|
||||
consumers:
|
||||
- username: anon
|
||||
keyauth_credentials:
|
||||
- key: ${SUPABASE_ANON_KEY}
|
||||
- username: service_role
|
||||
keyauth_credentials:
|
||||
- key: ${SUPABASE_SERVICE_ROLE_KEY}
|
||||
|
||||
acls:
|
||||
- consumer: anon
|
||||
group: anon
|
||||
- consumer: service_role
|
||||
group: admin
|
||||
|
||||
services:
|
||||
- name: auth-v1
|
||||
url: http://auth:9999/
|
||||
routes:
|
||||
- name: auth-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /auth/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
config:
|
||||
origins:
|
||||
- https://app.caterium.ru
|
||||
methods: [GET, POST, PUT, PATCH, DELETE, OPTIONS]
|
||||
headers: [Authorization, apikey, Content-Type, X-Client-Info]
|
||||
credentials: true
|
||||
|
||||
- name: rest-v1
|
||||
url: http://rest:3000/
|
||||
routes:
|
||||
- name: rest-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /rest/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
config:
|
||||
origins:
|
||||
- https://app.caterium.ru
|
||||
methods: [GET, POST, PUT, PATCH, DELETE, OPTIONS]
|
||||
headers: [Authorization, apikey, Content-Type, X-Client-Info, Prefer, Range]
|
||||
credentials: true
|
||||
|
||||
- name: realtime-v1
|
||||
url: http://realtime:4000/socket/
|
||||
routes:
|
||||
- name: realtime-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /realtime/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
config:
|
||||
origins:
|
||||
- https://app.caterium.ru
|
||||
methods: [GET, POST, OPTIONS]
|
||||
headers: [Authorization, apikey, Content-Type, X-Client-Info]
|
||||
credentials: true
|
||||
|
||||
- name: storage-v1
|
||||
url: http://storage:5000/
|
||||
routes:
|
||||
- name: storage-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /storage/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
config:
|
||||
origins:
|
||||
- https://app.caterium.ru
|
||||
methods: [GET, POST, PUT, PATCH, DELETE, OPTIONS]
|
||||
headers: [Authorization, apikey, Content-Type, X-Client-Info, x-upsert]
|
||||
credentials: true
|
||||
|
||||
- name: functions-v1
|
||||
url: http://functions:9000/
|
||||
routes:
|
||||
- name: functions-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /functions/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
config:
|
||||
origins:
|
||||
- https://app.caterium.ru
|
||||
methods: [GET, POST, OPTIONS]
|
||||
headers: [Authorization, apikey, Content-Type, X-Client-Info]
|
||||
credentials: true
|
||||
|
||||
- name: meta-v1
|
||||
url: http://meta:8080/
|
||||
routes:
|
||||
- name: meta-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /pg/
|
||||
plugins:
|
||||
- name: key-auth
|
||||
config:
|
||||
key_names: [apikey]
|
||||
- name: acl
|
||||
config:
|
||||
allow: [admin]
|
||||
@ -0,0 +1,14 @@
|
||||
-- Runs once on first Postgres container boot (docker-entrypoint-initdb.d).
|
||||
-- Mirrors what a managed Supabase project already has, so schema.sql/data.sql restore
|
||||
-- cleanly on top of it. Safe to re-run (IF NOT EXISTS everywhere).
|
||||
|
||||
create extension if not exists pg_cron;
|
||||
create extension if not exists pgcrypto;
|
||||
create extension if not exists pg_stat_statements;
|
||||
create extension if not exists "uuid-ossp";
|
||||
|
||||
-- pg_cron runs jobs as the calling user against the "postgres" database by default;
|
||||
-- Caterium's cron jobs (Etap 7) are re-created by data.sql / schema.sql restore, this
|
||||
-- just guarantees the extension + background worker exist before that runs.
|
||||
grant usage on schema cron to postgres;
|
||||
grant all privileges on all tables in schema cron to postgres;
|
||||
Loading…
Reference in New Issue
Block a user