Files
mc_cars_gmbh_infraestructure/README.md
T

11 KiB
Raw Blame History

MC Cars Dockerized Supabase CRM

Self-hosted Supabase stack + bilingual (DE/EN) public website + lead-management admin panel. Designed for Portainer deployment — no build: steps, all services use pre-built images with bind mounts. The host deployment root is /mnt/user/appdata/mc-cars.

What's inside

Service Image Purpose
db postgres:15-alpine Postgres with wal_level=logical
auth supabase/gotrue:v2.158.1 Email+password auth (signup disabled)
rest postgrest/postgrest:v12.2.0 Auto-generated REST API
storage supabase/storage-api:v1.11.13 S3-like bucket API (backed by ./data/storage)
imgproxy darthsim/imgproxy:v3.8.0 On-the-fly image transforms
realtime supabase/realtime:v2.30.23 Live postgres_changes subscriptions
meta supabase/postgres-meta:v0.84.2 Schema introspection for Studio
post-init postgres:15-alpine Idempotent bootstrap: seed admin + migrations
kong kong:2.8.1 Single API gateway at :55521
studio supabase/studio Supabase dashboard (:55530)
web nginx:1.27-alpine Public website (:55580)
web-admin nginx:1.27-alpine Admin web entrypoint (:55581)
n8n n8nio/n8n:latest Automation UI/API (:55590)
gotenberg gotenberg/gotenberg:8 DOCX/PDF conversion (internal only)

Requirements

  • Docker Engine with Compose v2 (or Portainer with Stacks)
  • Free ports: 55521, 55530, 55532, 55543, 55580, 55581, 55590

Run

Local Dev (Windows/macOS/Linux)

Use the local override so bind mounts point to this repository (for example ./data/db, ./data/storage, ./data/n8n).

Start local stack:

docker compose -f docker-compose.yml -f docker-compose.local.yml up -d --build

Stop local stack:

docker compose -f docker-compose.yml -f docker-compose.local.yml down

Delete local mounts and recreate from scratch:

docker compose -f docker-compose.yml -f docker-compose.local.yml down -v --remove-orphans
rm -rf ./data/db ./data/storage ./data/n8n
mkdir -p ./data/db ./data/storage ./data/n8n
docker compose -f docker-compose.yml -f docker-compose.local.yml up -d --build

PowerShell equivalent for cleanup:

docker compose -f docker-compose.yml -f docker-compose.local.yml down -v --remove-orphans
Remove-Item .\data\db,.\data\storage,.\data\n8n -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path .\data\db,.\data\storage,.\data\n8n -Force | Out-Null
docker compose -f docker-compose.yml -f docker-compose.local.yml up -d --build
  1. Clone the repo onto the host: git clone <repo> /mnt/user/appdata/mc-cars
  2. mkdir -p /mnt/user/appdata/mc-cars/data/{db,storage}
  3. Edit .env: set SITE_URL and SUPABASE_PUBLIC_URL to your domain (see below)
  4. Portainer → Stacks → Add stack → paste docker-compose.yml → paste .env into Environment variables → Deploy.

No chmod needed. config.js is generated by an inline shell command in docker-compose.yml, not a bind-mounted script file.

Via CLI

cd /mnt/user/appdata/mc-cars
docker compose up -d

First boot pulls ~1.5 GB of images and runs migrations (01-init.sql, post-boot.sql, 02-leads.sql, 08-backend-pricing-and-security.sql, 09-site-settings.sql). Give it 3060 s to settle.

Stop / reset

docker compose down                                  # stop, keep data
rm -rf /mnt/user/appdata/mc-cars/data/db             # FULL DB wipe (re-runs first-boot migrations)

URLs

Purpose URL
Public website http://<host>:55580
Admin web (dedicated nginx) http://<host>:55581
Admin page http://<host>:55581/admin.html
Supabase Studio http://<host>:55530
API gateway (Kong) http://<host>:55521
n8n http://<host>:55590
Postgres <host>:55532

Admin access is deliberately not linked from the public site. Bookmark it.

Credentials (dev defaults rotate before any deployment)

Account Value
Admin bootstrap user admin@mccars.local / mc-cars-admin
Postgres superuser postgres / see POSTGRES_PASSWORD

The admin is seeded with must_change_password = true in raw_user_meta_data. On first login the UI forces a rotation and refuses to reuse the bootstrap password. The real working password never equals .env.

Data model

  • public.vehicles — fleet, public-readable where is_active.
  • public.leads — booking form submissions with server-computed pricing. anon may INSERT only (via create_lead RPC); authenticated has full CRUD. Status: new | qualified | disqualified.
  • public.lead_attachments — ID documents and income proofs per lead. Max 1 of each enforced by unique partial index.
  • public.customers — created only by qualifying a lead. Hard FK lead_id preserves the audit link to the originating lead.
  • public.sales_orders — rental orders created during qualification, contain pricing snapshot.
  • public.site_settings — key-value settings table (e.g. hero_image_url). Publicly readable, admin-writable.
  • RPCs: calculate_price(uuid, date, date) (public pricing), create_lead(...) (server-side submission), qualify_lead(uuid, text), disqualify_lead(uuid, text), reopen_lead(uuid) — transactional, SECURITY INVOKER, authenticated only (except calculate_price and create_lead which are anon-accessible).
  • Realtime: supabase_realtime publication broadcasts inserts/updates on leads, customers, vehicles.

Environment: two variables per deployment

Only two lines in .env need changing between environments:

Variable Local dev Production
SITE_URL http://localhost:55580 https://your.domain.com
SUPABASE_PUBLIC_URL http://localhost:55521 https://your.domain.com

All other GoTrue URLs (API_EXTERNAL_URL, GOTRUE_SITE_URL, GOTRUE_URI_ALLOW_LIST) are derived automatically in docker-compose.yml.

On the NAS:

sed -i 's|SITE_URL=.*|SITE_URL=https://your.domain.com|' .env
sed -i 's|SUPABASE_PUBLIC_URL=.*|SUPABASE_PUBLIC_URL=https://your.domain.com|' .env
docker compose up -d --force-recreate web

Deployment & portability

Runtime state under /mnt/user/appdata/mc-cars/data/:

data/
├── db/          # Postgres cluster (bind mount)
└── storage/     # vehicle-photos bucket content

All bind mounts in docker-compose.yml use absolute paths under /mnt/user/appdata/mc-cars. Clone the repo there, deploy as a Portainer stack, done. No build: steps — every service pulls a pre-built image.

To put behind Nginx Proxy Manager with a single public domain:

Details tab: Scheme http, Forward to <NAS IP>:55580, Cache Assets OFF, Websockets Support ON.

SSL tab: your cert, Force SSL ON, HTTP/2 Support ON.

Advanced tab (⚙️): paste these location blocks:

location /auth/v1/ {
    proxy_pass http://<NAS IP>:55521;
    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;
}
location /rest/v1/ {
    proxy_pass http://<NAS IP>:55521;
    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;
}
location /realtime/v1/ {
    proxy_pass http://<NAS IP>:55521;
    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_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}
location /storage/v1/ {
    proxy_pass http://<NAS IP>:55521;
    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;
}

Do not expose /pg/ or Studio publicly.

Project layout

MC Cars/
├── docker-compose.yml         # full stack, bind-mount portability
├── .env                       # dev secrets / tunables
├── data/                      # runtime state (git-ignored)
├── supabase/
│   ├── kong.yml               # gateway routes (/auth, /rest, /realtime, /storage, /pg)
│   └── migrations/
│       ├── 00-run-init.sh     # creates supabase service roles
│       ├── 01-init.sql        # vehicles + bucket + seed cars
│       ├── post-boot.sql      # admin user (must_change_password) + bucket row
│       ├── 02-leads.sql       # leads, customers, RPCs, realtime publication
│       ├── 08-backend-pricing-and-security.sql  # calculate_price RPC, refactored create_lead, document security
│       └── 09-site-settings.sql                 # site_settings table + hero_image_url seed
├── frontend/
│   ├── nginx.conf
│   ├── index.html             # public DE/EN site, booking form -> leads
│   ├── admin.html             # auth-gated CRM + settings panel
│   ├── app.js                 # dynamic hero image, server-side pricing sidebar
│   ├── admin.js               # realtime + qualify/disqualify + password change + settings
│   ├── config.js              # generated at container start (git-ignored)
│   ├── i18n.js
│   ├── styles.css             # CSS-variable hero image with fallback
│   ├── impressum.html
│   └── datenschutz.html
├── .gitattributes             # enforces LF on .sh files
├── AGENT.md                   # findings, conventions, traps
└── ARQUITECTURE.md            # architecture deep-dive

See AGENT.md and ARQUITECTURE.md for everything deeper.