docs: update README, AGENT, ARQUITECTURE for 555xx ports, Portainer deploy, absolute paths

This commit is contained in:
Lago
2026-04-17 18:54:30 +02:00
parent 8b0a25f9c3
commit c1c9063996
3 changed files with 97 additions and 62 deletions
+20 -16
View File
@@ -4,12 +4,13 @@ A compact field guide for anyone (human or AI) making changes to this stack. Ski
--- ---
## 1. The portability contract (don't break it) ## 1. The deployment model
1. **No named volumes.** All runtime state bind-mounts to `./data/`. `docker compose down -v` must never be required to migrate: copy the folder, `docker compose up -d`, done. 1. **Portainer-native.** No `build:` steps. Every service pulls a pre-built image. The `web` service uses `nginx:1.27-alpine` with bind-mounted static files.
2. **No absolute paths** in `docker-compose.yml` or any config. 2. **Absolute host paths.** All bind mounts point to `/mnt/user/appdata/mc-cars/...`. Runtime state lives under `/mnt/user/appdata/mc-cars/data/`.
3. **Secrets live in `.env`**; the `.env` ships with dev defaults and is safe to commit until real deploy. Any non-local deployment must rotate every key (see §6). 3. **No named volumes.** Wiping data means `rm -rf /mnt/user/appdata/mc-cars/data/db` — not `docker volume rm`.
4. Anything that resets the DB in dev is `Remove-Item -Recurse -Force .\data` — not `docker volume rm`. 4. **Secrets live in `.env`**; the `.env` ships with dev defaults. Any non-local deployment must rotate every key (see §6).
5. **External ports are in the `555xx` range** to avoid collisions on a busy host (`55521` Kong, `55530` Studio, `55532` Postgres, `55543` Kong TLS, `55580` web).
--- ---
@@ -17,11 +18,11 @@ A compact field guide for anyone (human or AI) making changes to this stack. Ski
Migrations come in two flavours: Migrations come in two flavours:
### First-boot only (runs iff `./data/db` is empty) ### First-boot only (runs iff `/mnt/user/appdata/mc-cars/data/db` is empty)
- `supabase/migrations/00-run-init.sh` — wrapper that runs `01-init.sql` via `psql` so we can pre-create Supabase service roles (`anon`, `authenticated`, `service_role`, `authenticator`, `supabase_auth_admin`, `supabase_storage_admin`) that the official images otherwise assume already exist on their `supabase/postgres` image. We use plain `postgres:15-alpine`, so we create them ourselves. - `supabase/migrations/00-run-init.sh` — wrapper that runs `01-init.sql` via `psql` so we can pre-create Supabase service roles (`anon`, `authenticated`, `service_role`, `authenticator`, `supabase_auth_admin`, `supabase_storage_admin`) that the official images otherwise assume already exist on their `supabase/postgres` image. We use plain `postgres:15-alpine`, so we create them ourselves.
- `supabase/migrations/01-init.sql` — vehicles table, RLS, bucket row, seed demo vehicles. - `supabase/migrations/01-init.sql` — vehicles table, RLS, bucket row, seed demo vehicles.
Postgres' official entrypoint only processes `/docker-entrypoint-initdb.d/` on an **empty** data dir. To re-run: wipe `./data/db`. Postgres' official entrypoint only processes `/docker-entrypoint-initdb.d/` on an **empty** data dir. To re-run: wipe `/mnt/user/appdata/mc-cars/data/db`.
### Post-boot (every time, idempotent) ### Post-boot (every time, idempotent)
- `supabase/migrations/post-boot.sql` — seeds the admin `auth.users` row with `must_change_password=true`, creates the `vehicle-photos` bucket row if missing. Parameterized: `psql -v admin_email=... -v admin_password=...`. - `supabase/migrations/post-boot.sql` — seeds the admin `auth.users` row with `must_change_password=true`, creates the `vehicle-photos` bucket row if missing. Parameterized: `psql -v admin_email=... -v admin_password=...`.
@@ -61,7 +62,8 @@ Both are run by the `post-init` service, which gates on `auth.users` and `storag
## 5. Kong traps ## 5. Kong traps
- On Windows/WSL2, Docker Desktop's `wslrelay` intercepts `127.0.0.1:8000`. We expose Kong on host **`54321`** (container `8000`). Don't move it back to 8000. - On Windows/WSL2, Docker Desktop's `wslrelay` intercepts `127.0.0.1:8000`. On the production host Kong is exposed on **`55521`** (container `8000`).
- On a busy Unraid/Docker host all MC Cars ports live in the `555xx` range to avoid collisions.
- Kong needs `KONG_PLUGINS: bundled,request-transformer,cors,key-auth,acl,basic-auth`. Omitting `acl` breaks Studio. - Kong needs `KONG_PLUGINS: bundled,request-transformer,cors,key-auth,acl,basic-auth`. Omitting `acl` breaks Studio.
- `kong.yml` has one `consumer` per role with `acl` groups; plugins `key-auth` + `acl` gate which `apikey` (anon vs service_role) can reach which route. - `kong.yml` has one `consumer` per role with `acl` groups; plugins `key-auth` + `acl` gate which `apikey` (anon vs service_role) can reach which route.
@@ -84,7 +86,9 @@ Password min length is enforced server-side by `GOTRUE_PASSWORD_MIN_LENGTH=10`.
## 7. Frontend conventions ## 7. Frontend conventions
- **Only** the anon key leaves the server. `frontend/Dockerfile` writes `config.js` at container start from `$SUPABASE_URL` / `$SUPABASE_ANON_KEY`. The service_role key is not mounted into `web`. - **Only** the anon key leaves the server. `frontend/99-config.sh` (bind-mounted into the nginx entrypoint dir) writes `config.js` at container start from `$SUPABASE_URL` / `$SUPABASE_ANON_KEY`. The service_role key is not mounted into `web`.
- The `web` service uses `nginx:1.27-alpine` directly (no `build:`). Static files + nginx.conf + the entrypoint script are bind-mounted read-only. Updating the frontend is `git pull` + restart the container.
- `.gitattributes` enforces `eol=lf` on `*.sh` files so the entrypoint doesn't break with CRLF on Windows checkouts.
- `frontend/app.js` (public) creates a Supabase client with `persistSession: false` — the public site never needs a session. - `frontend/app.js` (public) creates a Supabase client with `persistSession: false` — the public site never needs a session.
- `frontend/admin.js` uses `persistSession: true, storageKey: "mccars.auth"` and a single realtime channel `mccars-admin` that subscribes to leads/customers/vehicles. - `frontend/admin.js` uses `persistSession: true, storageKey: "mccars.auth"` and a single realtime channel `mccars-admin` that subscribes to leads/customers/vehicles.
- `app.js` writes `vehicle_id` (uuid) **and** denormalized `vehicle_label` ("BMW M3") into the lead at submit time, so the admin UI renders even if the vehicle is later deleted. - `app.js` writes `vehicle_id` (uuid) **and** denormalized `vehicle_label` ("BMW M3") into the lead at submit time, so the admin UI renders even if the vehicle is later deleted.
@@ -95,30 +99,30 @@ Password min length is enforced server-side by `GOTRUE_PASSWORD_MIN_LENGTH=10`.
| Symptom | Cause / Fix | | Symptom | Cause / Fix |
| ------------------------------------------------------- | ---------------------------------------------------------------- | | ------------------------------------------------------- | ---------------------------------------------------------------- |
| `auth` container restart loop, "role supabase_auth_admin does not exist" | `00-run-init.sh` didn't run. Wipe `./data/db`, retry. | | `auth` container restart loop, "role supabase_auth_admin does not exist" | `00-run-init.sh` didn't run. Wipe `/mnt/user/appdata/mc-cars/data/db`, retry. |
| `realtime` container exits, "wal_level must be logical" | `db` `command:` missing `wal_level=logical`. | | `realtime` container exits, "wal_level must be logical" | `db` `command:` missing `wal_level=logical`. |
| 502 on `/realtime/v1/` through Kong | Missing `acl` plugin in `KONG_PLUGINS`, or realtime not listed in `kong.yml` consumers. | | 502 on `/realtime/v1/` through Kong | Missing `acl` plugin in `KONG_PLUGINS`, or realtime not listed in `kong.yml` consumers. |
| Admin login works but Leads tab is empty | RLS: session is `anon` instead of `authenticated`. Check that `signInWithPassword` succeeded and token is attached to subsequent requests. | | Admin login works but Leads tab is empty | RLS: session is `anon` instead of `authenticated`. Check that `signInWithPassword` succeeded and token is attached to subsequent requests. |
| Booking form submits but no row appears | anon lacks `INSERT` grant on `public.leads`. Check `02-leads.sql` ran (see `post-init` logs). | | Booking form submits but no row appears | anon lacks `INSERT` grant on `public.leads`. Check `02-leads.sql` ran (see `post-init` logs). |
| `.sh` files reject with `\r: command not found` | CRLF line endings. `dos2unix` or re-save as LF. | | `.sh` files reject with `\r: command not found` | CRLF line endings. `dos2unix` or re-save as LF. |
| Port 8000 "connection refused" on Windows | Docker Desktop wslrelay. Use `54321`. | | Port 8000 "connection refused" on Windows | Docker Desktop wslrelay. Use `55521` (production port). |
--- ---
## 9. How to verify a working stack ## 9. How to verify a working stack
```powershell ```bash
docker compose ps # all services up (post-init exited 0) docker compose ps # all services up (post-init exited 0)
curl http://localhost:54321/rest/v1/vehicles?select=brand -H "apikey: $env:ANON" # 6 demo cars curl http://localhost:55521/rest/v1/vehicles?select=brand -H "apikey: $ANON" # 6 demo cars
``` ```
In the browser: In the browser:
1. Open http://localhost:8080, submit the booking form. 1. Open http://\<host\>:55580, submit the booking form.
2. Open http://localhost:8080/admin.html, log in with `.env` bootstrap creds. 2. Open http://\<host\>:55580/admin.html, log in with `.env` bootstrap creds.
3. Rotate the password when prompted. 3. Rotate the password when prompted.
4. The lead you submitted appears in "Aktive Leads" — in real time. 4. The lead you submitted appears in "Aktive Leads" — in real time.
5. Click **Qualifizieren**. Row disappears from active, a new row appears in **Kunden** with the `lead_id` displayed. 5. Click **Qualifizieren**. Row disappears from active, a new row appears in **Kunden** with the `lead_id` displayed.
6. Open Supabase Studio at http://localhost:3000 and confirm the `customers.lead_id` FK matches. 6. Open Supabase Studio at http://\<host\>:55530 and confirm the `customers.lead_id` FK matches.
--- ---
+37 -22
View File
@@ -7,9 +7,9 @@ A thorough walkthrough of how the MC Cars system is wired. Complements [AGENT.md
## 1. High-level diagram ## 1. High-level diagram
``` ```
Browser (localhost) Browser (<host>)
│ │ │ │
:8080 (nginx) :54321 (Kong) :3000 (Studio) :55580 (nginx) :55521 (Kong) :55530 (Studio)
│ │ │ │ │ │
┌──────┴──────┐ │ │ ┌──────┴──────┐ │ │
│ web │ │ │ │ web │ │ │
@@ -32,7 +32,7 @@ A thorough walkthrough of how the MC Cars system is wired. Complements [AGENT.md
│ logical) │ └───────────┘ │ logical) │ └───────────┘
└──────────┘ ▲ └──────────┘ ▲
│ │ │ │
./data/db ./data/storage /mnt/.../data/db /mnt/.../data/storage
``` ```
Dashed lifetime services (exit 0 once done): `post-init`. Dashed lifetime services (exit 0 once done): `post-init`.
@@ -46,8 +46,8 @@ Network: single user-defined bridge `mccars`. No host networking, no ingress con
### 2.1 Public visitor reads cars ### 2.1 Public visitor reads cars
``` ```
browser → :8080/index.html (nginx, static) browser → :55580/index.html (nginx, static)
browser → :54321/rest/v1/vehicles?select=*&is_active=eq.true browser → :55521/rest/v1/vehicles?select=*&is_active=eq.true
→ Kong → rest (PostgREST) → postgres → Kong → rest (PostgREST) → postgres
→ RLS: "vehicles_select_active" (for anon, is_active=true only) → RLS: "vehicles_select_active" (for anon, is_active=true only)
← JSON ← JSON
@@ -59,7 +59,7 @@ No auth header except the anon `apikey`. Kong strips the key and forwards with t
``` ```
browser → supabase.from('leads').insert({...}) browser → supabase.from('leads').insert({...})
→ :54321/rest/v1/leads (POST, apikey=anon) → :55521/rest/v1/leads (POST, apikey=anon)
→ PostgREST → postgres → PostgREST → postgres
→ RLS "leads_anon_insert": insert allowed, select denied → RLS "leads_anon_insert": insert allowed, select denied
← 201 (no body returned to anon) ← 201 (no body returned to anon)
@@ -71,7 +71,7 @@ The frontend never reads leads back. If the INSERT fails (validation, RLS), the
``` ```
browser(admin.html) → supabase.auth.signInWithPassword browser(admin.html) → supabase.auth.signInWithPassword
→ :54321/auth/v1/token?grant_type=password → :55521/auth/v1/token?grant_type=password
→ Kong → gotrue → postgres(auth.users) → Kong → gotrue → postgres(auth.users)
← access_token (JWT, aud=authenticated), refresh_token ← access_token (JWT, aud=authenticated), refresh_token
← user.raw_user_meta_data.must_change_password (true on bootstrap) ← user.raw_user_meta_data.must_change_password (true on bootstrap)
@@ -79,7 +79,7 @@ browser(admin.html) → supabase.auth.signInWithPassword
if must_change_password: if must_change_password:
user blocked in "Passwort setzen" screen user blocked in "Passwort setzen" screen
supabase.auth.updateUser({password, data:{must_change_password:false}}) supabase.auth.updateUser({password, data:{must_change_password:false}})
→ :54321/auth/v1/user (PUT with Bearer access_token) → :55521/auth/v1/user (PUT with Bearer access_token)
→ gotrue updates auth.users → gotrue updates auth.users
``` ```
@@ -106,8 +106,8 @@ Realtime publication fires:
``` ```
admin.js → supabase.storage.from('vehicle-photos').upload(path, file) admin.js → supabase.storage.from('vehicle-photos').upload(path, file)
→ :54321/storage/v1/object/vehicle-photos/<path> → :55521/storage/v1/object/vehicle-photos/<path>
→ Kong → storage-api → ./data/storage → Kong → storage-api → /mnt/.../data/storage
← public URL served via imgproxy for on-the-fly transforms ← public URL served via imgproxy for on-the-fly transforms
admin.js → UPDATE vehicles SET photo_url = <public url> admin.js → UPDATE vehicles SET photo_url = <public url>
``` ```
@@ -204,7 +204,7 @@ Declarative config in `supabase/kong.yml`. One consumer per role:
Plugins pipeline: `cors``key-auth``acl` → proxy. Public routes (storage public objects) are whitelisted without `key-auth`. Plugins pipeline: `cors``key-auth``acl` → proxy. Public routes (storage public objects) are whitelisted without `key-auth`.
Host port mapping: `54321:8000` (8000 blocked by Docker Desktop's wslrelay on Windows). Host port mapping: `55521:8000` (8000 blocked by Docker Desktop's wslrelay on Windows; `555xx` range avoids collisions on busy Docker hosts).
--- ---
@@ -212,7 +212,8 @@ Host port mapping: `54321:8000` (8000 blocked by Docker Desktop's wslrelay on Wi
``` ```
frontend/ frontend/
├── Dockerfile (nginx:alpine, writes config.js at boot) ├── Dockerfile (legacy, not used in Portainer deploy)
├── 99-config.sh (entrypoint: writes config.js at boot)
├── nginx.conf (serves /, gzip, cache headers) ├── nginx.conf (serves /, gzip, cache headers)
├── index.html app.js (public site, anon key, persistSession:false) ├── index.html app.js (public site, anon key, persistSession:false)
├── admin.html admin.js (admin CRM, persistSession:true) ├── admin.html admin.js (admin CRM, persistSession:true)
@@ -222,30 +223,44 @@ frontend/
└── datenschutz.html └── datenschutz.html
``` ```
- `config.js` is generated at container start (`envsubst`) from `SUPABASE_URL` and `SUPABASE_ANON_KEY` only. The service_role key is never mounted into the web container. - `config.js` is generated at container start by `99-config.sh` (bind-mounted into `/docker-entrypoint.d/`) from `$SUPABASE_URL` and `$SUPABASE_ANON_KEY` only. The service_role key is never mounted into the web container.
- The `web` service uses `nginx:1.27-alpine` directly with bind-mounted files (no `build:` step). This is Portainer-compatible: updating the frontend is `git pull` + container restart.
- `admin.js` is ES modules, imports `@supabase/supabase-js` from `esm.sh` (CDN, pinned version). One Supabase client per page. - `admin.js` is ES modules, imports `@supabase/supabase-js` from `esm.sh` (CDN, pinned version). One Supabase client per page.
- State lives in a single `state` object. Admin re-renders the active tab on realtime events. - State lives in a single `state` object. Admin re-renders the active tab on realtime events.
- Force-password-change modal is the only state that can preempt the rest of the admin UI after login. - Force-password-change modal is the only state that can preempt the rest of the admin UI after login.
--- ---
## 7. Portability & deployment ## 7. Deployment
**The contract:** everything under `c:\Coding\MC Cars GmbH\` (or whatever you rename it to) is the full stack. Copy it anywhere Docker runs, bring up the stack. **Host root:** `/mnt/user/appdata/mc-cars`
What's state (under `./data/`): All bind mounts in `docker-compose.yml` use absolute paths under that root. The compose has no `build:` steps — every image is pulled from a registry, making it Portainer-compatible.
- `./data/db/` — Postgres cluster
- `./data/storage/` — object bucket files What's state (under `/mnt/user/appdata/mc-cars/data/`):
- `data/db/` — Postgres cluster
- `data/storage/` — object bucket files
What's code/config (committed): What's code/config (committed):
- `docker-compose.yml`, `.env` (dev defaults), `supabase/`, `frontend/`, `AGENT.md`, `ARQUITECTURE.md`, `README.md` - `docker-compose.yml`, `.env`, `supabase/`, `frontend/`, `AGENT.md`, `ARQUITECTURE.md`, `README.md`, `.gitattributes`
**Portainer deployment:**
1. Clone repo to `/mnt/user/appdata/mc-cars`
2. `chmod +x` the `.sh` files
3. `mkdir -p data/{db,storage}`
4. Portainer → Stacks → Add stack → paste compose + env vars → Deploy
**Nginx Proxy Manager (single public domain):**
- Proxy `/``mccars-web:80` (or `<host>:55580`)
- Custom locations `/auth/v1/`, `/rest/v1/`, `/realtime/v1/`, `/storage/v1/``mccars-kong:8000` (or `<host>:55521`)
- Do **not** expose `/pg/` or Studio publicly
- Update `.env` URLs to `https://cars.yourdomain.com`
For real deployments: For real deployments:
1. Generate a new `JWT_SECRET` and matching `ANON_KEY` / `SERVICE_ROLE_KEY` (see Supabase self-hosting docs). 1. Generate a new `JWT_SECRET` and matching `ANON_KEY` / `SERVICE_ROLE_KEY` (see Supabase self-hosting docs).
2. Set a strong `POSTGRES_PASSWORD`, `ADMIN_PASSWORD`. 2. Set a strong `POSTGRES_PASSWORD`, `ADMIN_PASSWORD`.
3. Put Kong behind a TLS reverse-proxy (Caddy/Traefik/nginx) and set `SITE_URL`, `API_EXTERNAL_URL`, `SUPABASE_PUBLIC_URL` to the public https URLs. 3. Wire real SMTP for password-reset mail (`SMTP_*`).
4. Wire real SMTP for password-reset mail (`SMTP_*`). 4. Back up `/mnt/user/appdata/mc-cars/data/` on a schedule.
5. Back up `./data/` on a schedule.
--- ---
+38 -22
View File
@@ -1,6 +1,6 @@
# MC Cars Dockerized Supabase CRM # MC Cars Dockerized Supabase CRM
Self-hosted Supabase stack + bilingual (DE/EN) public website + lead-management admin panel. Everything lives under this folder. Copying the folder to another machine and running `docker compose up -d` reproduces the stack bit-for-bit — all runtime state is under `./data/` bind mounts, no named volumes. 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 ## What's inside
@@ -14,19 +14,28 @@ Self-hosted Supabase stack + bilingual (DE/EN) public website + lead-management
| `realtime` | `supabase/realtime:v2.30.23` | Live `postgres_changes` subscriptions | | `realtime` | `supabase/realtime:v2.30.23` | Live `postgres_changes` subscriptions |
| `meta` | `supabase/postgres-meta:v0.84.2` | Schema introspection for Studio | | `meta` | `supabase/postgres-meta:v0.84.2` | Schema introspection for Studio |
| `post-init` | `postgres:15-alpine` | Idempotent bootstrap: seed admin + migrations | | `post-init` | `postgres:15-alpine` | Idempotent bootstrap: seed admin + migrations |
| `kong` | `kong:2.8.1` | Single API gateway at `:54321` | | `kong` | `kong:2.8.1` | Single API gateway at `:55521` |
| `studio` | `supabase/studio` | Supabase dashboard (`:3000`) | | `studio` | `supabase/studio` | Supabase dashboard (`:55530`) |
| `web` | local `nginx:alpine` build | Public site + admin panel (`:8080`) | | `web` | `nginx:1.27-alpine` | Public site + admin panel (`:55580`) |
## Requirements ## Requirements
- Docker Desktop / Docker Engine with Compose v2 - Docker Engine with Compose v2 (or Portainer with Stacks)
- Free ports: `3000`, `5432`, `8080`, `54321`, `54443` - Free ports: `55521`, `55530`, `55532`, `55543`, `55580`
## Run ## Run
```powershell ### Via Portainer (recommended)
cd 'c:\Coding\MC Cars GmbH'
1. Clone the repo onto the host: `git clone <repo> /mnt/user/appdata/mc-cars`
2. `chmod +x /mnt/user/appdata/mc-cars/frontend/99-config.sh /mnt/user/appdata/mc-cars/supabase/migrations/00-run-init.sh`
3. `mkdir -p /mnt/user/appdata/mc-cars/data/{db,storage}`
4. Portainer → Stacks → Add stack → paste `docker-compose.yml` → paste `.env` into Environment variables → Deploy.
### Via CLI
```bash
cd /mnt/user/appdata/mc-cars
docker compose up -d docker compose up -d
``` ```
@@ -34,21 +43,20 @@ First boot pulls ~1.5 GB of images and runs migrations (`01-init.sql`, `post-boo
### Stop / reset ### Stop / reset
```powershell ```bash
docker compose down # stop, keep data docker compose down # stop, keep data
docker compose down -v # stop + delete named volumes (there are none anymore) rm -rf /mnt/user/appdata/mc-cars/data/db # FULL DB wipe (re-runs first-boot migrations)
Remove-Item -Recurse -Force .\data # FULL wipe (needed to re-run first-boot migrations)
``` ```
## URLs ## URLs
| Purpose | URL | | Purpose | URL |
| ------------------------------- | -------------------------------- | | ------------------------------- | --------------------------------- |
| Public website | http://localhost:8080 | | Public website | http://\<host\>:55580 |
| Admin panel | http://localhost:8080/admin.html | | Admin panel | http://\<host\>:55580/admin.html |
| Supabase Studio | http://localhost:3000 | | Supabase Studio | http://\<host\>:55530 |
| API gateway (Kong) | http://localhost:54321 | | API gateway (Kong) | http://\<host\>:55521 |
| Postgres | `localhost:5432` | | Postgres | `<host>:55532` |
> Admin access is deliberately **not** linked from the public site. Bookmark it. > Admin access is deliberately **not** linked from the public site. Bookmark it.
@@ -69,9 +77,9 @@ The admin is seeded with `must_change_password = true` in `raw_user_meta_data`.
- RPCs: `qualify_lead(uuid, text)`, `disqualify_lead(uuid, text)`, `reopen_lead(uuid)` — transactional, `SECURITY INVOKER`, `authenticated` only. - RPCs: `qualify_lead(uuid, text)`, `disqualify_lead(uuid, text)`, `reopen_lead(uuid)` — transactional, `SECURITY INVOKER`, `authenticated` only.
- Realtime: `supabase_realtime` publication broadcasts inserts/updates on leads, customers, vehicles. - Realtime: `supabase_realtime` publication broadcasts inserts/updates on leads, customers, vehicles.
## Portability ## Deployment & portability
Runtime state under `./data/`: Runtime state under `/mnt/user/appdata/mc-cars/data/`:
``` ```
data/ data/
@@ -79,7 +87,13 @@ data/
└── storage/ # vehicle-photos bucket content └── storage/ # vehicle-photos bucket content
``` ```
Everything else (config, migrations, frontend) is in the repo. Zip the folder, scp it, `docker compose up -d` — you have the same stack. 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 (`cars.yourdomain.com`):
- Proxy `/``mccars-web:80` (or `<host>:55580`)
- Custom locations `/auth/v1/`, `/rest/v1/`, `/realtime/v1/`, `/storage/v1/``mccars-kong:8000` (or `<host>:55521`)
- Do **not** expose `/pg/` or Studio publicly.
- Update `.env` URLs to `https://cars.yourdomain.com`.
## Project layout ## Project layout
@@ -96,17 +110,19 @@ MC Cars/
│ ├── post-boot.sql # admin user (must_change_password) + bucket row │ ├── post-boot.sql # admin user (must_change_password) + bucket row
│ └── 02-leads.sql # leads, customers, RPCs, realtime publication │ └── 02-leads.sql # leads, customers, RPCs, realtime publication
├── frontend/ ├── frontend/
│ ├── Dockerfile # nginx + runtime anon-key injection │ ├── Dockerfile # (legacy, not used in Portainer deploy)
│ ├── 99-config.sh # entrypoint: injects config.js with anon key
│ ├── nginx.conf │ ├── nginx.conf
│ ├── index.html # public DE/EN site, booking form -> leads │ ├── index.html # public DE/EN site, booking form -> leads
│ ├── admin.html # auth-gated CRM │ ├── admin.html # auth-gated CRM
│ ├── app.js │ ├── app.js
│ ├── admin.js # realtime + qualify/disqualify + password change │ ├── admin.js # realtime + qualify/disqualify + password change
│ ├── config.js # anon-only runtime config │ ├── config.js # anon-only runtime config (generated at boot)
│ ├── i18n.js │ ├── i18n.js
│ ├── styles.css │ ├── styles.css
│ ├── impressum.html │ ├── impressum.html
│ └── datenschutz.html │ └── datenschutz.html
├── .gitattributes # enforces LF on .sh files
├── AGENT.md # findings, conventions, traps ├── AGENT.md # findings, conventions, traps
└── ARQUITECTURE.md # architecture deep-dive └── ARQUITECTURE.md # architecture deep-dive
``` ```