- Impressum: MC Cars GmbH, Gaisfeld 1/2, 8564 Krottendorf-Gaisfeld - FN 675751 b, Landesgericht für Zivilrechtssachen Graz - Geschäftsführer: Christian Leski, Marco Schober - UID placeholder (wird nachgereicht) - Email domain: mccars.at → mc-cars.at across all pages - Social links: mccars → mc-cars
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
Via Portainer (recommended)
- Clone the repo onto the host:
git clone <repo> /mnt/user/appdata/mc-cars mkdir -p /mnt/user/appdata/mc-cars/data/{db,storage}- Edit
.env: setSITE_URLandSUPABASE_PUBLIC_URLto your domain (see below) - Portainer → Stacks → Add stack → paste
docker-compose.yml→ paste.envinto Environment variables → Deploy.
No
chmodneeded.config.jsis generated by an inline shell command indocker-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 30–60 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 whereis_active.public.leads— booking form submissions with server-computed pricing.anonmayINSERTonly (viacreate_leadRPC);authenticatedhas 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 FKlead_idpreserves 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,authenticatedonly (except calculate_price and create_lead which are anon-accessible). - Realtime:
supabase_realtimepublication broadcasts inserts/updates on leads, customers, vehicles.
Environment: three variables per deployment
Three variables 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 |
N8N_WEBHOOK_URL |
http://localhost:55521/webhook/manual-email-send |
https://your.domain.com/webhook/manual-email-send |
All other GoTrue URLs (API_EXTERNAL_URL, GOTRUE_SITE_URL, GOTRUE_URI_ALLOW_LIST) are derived automatically in docker-compose.yml.
Quick setup with deploy-setup.sh
Use the included deployment script to update all environment variables at once:
./deploy-setup.sh https://www.mc-cars.at
This updates .env and outputs the configuration. Then restart:
docker compose down
docker compose up -d --build
Manual setup (legacy sed method)
sed -i 's|SITE_URL=.*|SITE_URL=https://www.mc-cars.at|' .env
sed -i 's|SUPABASE_PUBLIC_URL=.*|SUPABASE_PUBLIC_URL=https://www.mc-cars.at|' .env
sed -i 's|N8N_WEBHOOK_URL=.*|N8N_WEBHOOK_URL=https://www.mc-cars.at/webhook/manual-email-send|' .env
docker compose up -d --build
How n8n webhooks work
- n8n runs internally (not exposed to the internet)
- Kong API gateway proxies
/webhook/*traffic to internal n8n - Browser requests to
https://your.domain.com/webhook/manual-email-sendroute through Kong → n8n - Frontend config is generated at container startup from
N8N_WEBHOOK_URLenvironment variable
See N8N_WEBHOOK_ROUTING.md for full architecture details.
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.