Skip to content

BacMR Backend — Deployment

Production backend for https://api.bacmr.com, running on the host (185.185.83.156).

Architecture

Internet ──HTTPS──▶ nginx (aaPanel vhosts, SSL)
   api.bacmr.com      ─▶ backend container :2026 (FastAPI / uvicorn)
   docs.bacmr.com     ─▶ docs container :8055 (static MkDocs)
   supabase.bacmr.com ─▶ Kong :8000  (PUBLIC Supabase gateway — for UI / mobile clients)
        backend ──(SUPABASE_URL=http://host.docker.internal:8000, INTERNAL)──┐
                              ▼                                               ▼
   ┌───────────── self-hosted Supabase stack (Docker) ─────────────┐
   │  Kong :8000 ─▶ GoTrue (auth) · PostgREST (rest) · Storage      │
   └───────────────────────────┬───────────────────────────────────┘
                                ▼  host.docker.internal:5432
                  host Postgres  bacmr_rag  (aaPanel PG16 + pgvector + pgcrypto)

Supabase URLs — internal vs public (important)

  • Backend → Supabase: SUPABASE_URL=http://host.docker.internal:8000 — the internal Kong gateway. Server-side calls stay on the host: faster (no public round-trip / TLS handshake) and resilient to nginx/DNS issues. This is the correct setting for the backend — do NOT point it at the public URL.
  • External clients (UI / mobile) → Supabase: https://supabase.bacmr.com — an aaPanel vhost (SSL) reverse-proxying to the same Kong :8000. Health check: https://supabase.bacmr.com/auth/v1/health → GoTrue 200.
  • GoTrue is configured with API_EXTERNAL_URL=https://supabase.bacmr.com (so issued tokens and auth links reference the public domain) and SITE_URL=https://bacmr.com. Tokens are validated by signature (shared JWT_SECRET), so the backend validating them over the internal route works regardless of the token's iss.

  • Data lives in the external host Postgres bacmr_rag — survives any Docker wipe.

  • The Supabase services + backend are stateless containers (Storage file bytes are bind-mounted to infra/supabase/data/storage).
  • Every container is capped at 1 GB RAM (mem_limit: 1g).
  • First-time DB/roles/schema provisioning: see docs/95_plans/2026-06-28-self-host-supabase-external-db.md.

CI/CD (.github/workflows/deploy-rag.yml)

Trigger: push to backend-rag (i.e. when PRs like #29 merge) + manual workflow_dispatch.

Flow (build-on-server, no registry): 1. SSH into the host (appleboy/ssh-action, port 47832, user mido). 2. git fetch + git reset --hard origin/backend-rag in /opt/bacmr (the dedicated deploy checkout — separate from any dev clone). 3. Render .env and infra/supabase/.env from GitHub secrets via envsubst (.env.production.tmpl, infra/supabase/.env.tmpl). 4. docker compose -f infra/supabase/docker-compose.yml up -d (stack). 5. docker compose up -d --build backend. 6. Health-check http://localhost:2026/health, then docker image prune -f.

The quality gate (ci-rag.yml) runs separately on the same branch.

GitHub secrets (repository-level)

Set via gh secret set. The existing Backend-RAG environment (cloud/elghidiya eval secrets) is intentionally left untouched — these self-hosted secrets are repo-level.

Secret Purpose
VPS_HOST / VPS_USER / VPS_PORT SSH target (185.185.83.156 / mido / 47832)
VPS_SSH_KEY private key of the deploy keypair (public key in mido's authorized_keys)
OPENAI_API_KEY PINECONE_API_KEY GOOGLE_TRANSLATE_API_KEY GOOGLE_TRANSLATE_PROJECT_ID app keys
JWT_SECRET ANON_KEY SERVICE_ROLE_KEY self-hosted Supabase keys (shared by backend + stack)
AUTHENTICATOR_PASSWORD AUTH_ADMIN_PASSWORD STORAGE_ADMIN_PASSWORD DB role passwords (must match db/supabase/00_roles.sql)

Rotate a value: edit it in Infisical (sops.bacmr.com, project bacmr-rag-engine, env dev) and redeploy — the deploy renders .env via deploy/refresh-env.sh. (role passwords / JWT also need updating in the DB + a stack restart to stay in sync).

Reverse proxy (server-side, not in git)

aaPanel nginx vhost /www/server/panel/vhost/nginx/api.bacmr.com.conf — SSL is managed by aaPanel (Let's Encrypt, auto-renew). The proxy block added after #REWRITE-END:

location / {
    proxy_pass http://127.0.0.1:2026;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_connect_timeout 30s;
    proxy_read_timeout 300s;
}
Apply changes: sudo /www/server/nginx/sbin/nginx -t && sudo /www/server/nginx/sbin/nginx -s reload. A timestamped backup of the original vhost sits next to it (*.bak-*).

Manual deploy / rollback

cd /home/mido/Projects/BacMR/backend
# manual deploy of current branch
docker compose -f infra/supabase/docker-compose.yml --env-file infra/supabase/.env up -d
docker compose up -d --build backend

# rollback: check out a known-good commit and rebuild
git checkout <good-sha> && docker compose up -d --build backend

# logs / status
docker compose logs -f backend
docker compose -f infra/supabase/docker-compose.yml ps

Verified live

https://api.bacmr.com/health → 200; HTTP→HTTPS 301; POST /auth/signup and /auth/signin round-trip through nginx → backend → GoTrue → bacmr_rag; all five containers report a 1 GB memory limit.