# Stack trace for `docker-compose.prod.yml` deployment
Use this doc to see **how the frontend, backend, Postgres, and MinIO connect**, in what **order Compose starts services**, and how to **verify** each tier after deploy. For Postgres volume errors, HTTP vs HTTPS on Vite, and `.env` dotfiles, see [deploy-production-docker.md](./deploy-production-docker.md).
---
## 1. High-level dependency graph
Compose service names (`postgres`, `minio`, etc.) resolve on the **`profyt-net`** bridge (`10.5.0.0/16`). Static IPs are defined in `docker-compose.prod.yml` for readability; traffic still uses DNS names **`be0`**, **`minio`**, **`postgres`**.
```mermaid
flowchart TB
subgraph browser [Browser on the Internet]
U[User]
end
subgraph host [Docker host ports]
FEPORT["HOST:FE_PORT ~ Vite HTTP"]
MINAPI["HOST:MINIO_API_PORT"]
MINUI["HOST:MINIO_CONSOLE_PORT"]
end
subgraph net [Compose network profyt-net]
fe0["fe0 :8080
nginx (prod)"]
be0["be0 :4402
FastAPI"]
pg[("postgres :5432")]
s3["minio :9000 API
:9001 console"]
end
U -->|"http://PUBLIC_HOST:FE_PORT (not https)"| FEPORT --> fe0
U -->|"presigned GET bytes"| MINAPI --> s3
fe0 -->|"nginx proxy /api, /submitted-initiatives…"| be0
be0 -->|"INITIATIVE_DATABASE_URL"| pg
be0 -->|"S3_ENDPOINT_URL presign + server ops"| s3
subgraph oneoff [runs after MinIO healthy]
cors["minio-cors:
ensure buckets"]
end
cors --> s3
subgraph localhost_only [Reachable only on the host VM]
dbmap["127.0.0.1:15432 → postgres"]
bemap["127.0.0.1:4402 → be0"]
end
```
---
## 2. Responsibility matrix
| Piece | Compose service | Listen (container) | Published to Internet? | Env / config that ties it together |
|--------|----------------|----------------------|-------------------------|-----------------------------------|
| **Frontend** | `fe0` | `0.0.0.0:8080` | **`http://${PUBLIC_HOST}:${FE_PORT}`** maps host → container 8080 | **Production:** `Dockerfile.prod` → nginx static + proxy `/api` → `be0`. **Dev:** Vite (`Dockerfile` + `npm run dev`) |
| **Backend API** | `be0` | `0.0.0.0:4402` | **No** — `127.0.0.1:4402` on host only; browsers use **`fe0` proxy** `/api`, etc. | `INITIATIVE_DATABASE_URL=postgresql+asyncpg://…@postgres:5432/…`, `S3_ENDPOINT_URL=http://minio:9000`, **`S3_PUBLIC_ENDPOINT_URL`** defaults to **`http://${PUBLIC_HOST}:${MINIO_API_PORT}`** — set **`https://…`** when MinIO sits behind HTTPS for presigned URLs ([minio-behind-https.md](./minio-behind-https.md)) |
| **Database** | `postgres` | `:5432` | **No** — `127.0.0.1:15432` on host for admin tools only | `POSTGRES_*` first init only (`initiative_pg_data` volume) |
| **Object storage** | `minio` | API `:9000`, console `:9001` | **`http://${PUBLIC_HOST}:${MINIO_API_PORT}`** (often proxied via HTTPS in production — see linked doc above) **`MINIO_CONSOLE_PORT`** | **`MINIO_SERVER_URL`** / **`MINIO_BROWSER_REDIRECT_URL`** compile from **`PUBLIC_HOST`** by default or use `.env` HTTPS overrides together with **`S3_PUBLIC_ENDPOINT_URL`** |
**`minio-cors`** is a one-shot job: waits for healthy MinIO and creates **`initiative-attachments`**, **`initiative-exports`**, and **`initiative-quarantine`**. **Community MinIO** does not implement S3 per-bucket CORS (`mc cors set`); browsers rely on **`MINIO_API_CORS_ALLOW_ORIGIN`** on the **`minio`** service (defaults to `*` in Compose) for presigned GETs.
---
## 3. Request paths (mental trace)
1. **SPA + API calls**
User opens **`http://PUBLIC_HOST:FE_PORT`**. The browser loads Vite-served assets from **`fe0`**. Calls to **`/api/...`** (and similar proxied paths) go to **`fe0`**, which forwards to **`http://be0:4402`** inside the network.
2. **Presigned S3 / MinIO from the browser**
**`be0`** builds URLs using **`S3_PUBLIC_ENDPOINT_URL`** (must be reachable from the user’s browser, usually **`http://PUBLIC_HOST:MINIO_API_PORT`**). The browser downloads objects **directly from MinIO** on the host-published port—not through **`be0`**.
3. **Backend → Postgres**
Only **`be0`** uses **`INITIATIVE_DATABASE_URL`**; host `127.0.0.1:15432` is optional for **`psql`** / dumps from the VPS shell.
4. **Backend → MinIO (server-side)**
**`be0`** uses **`S3_ENDPOINT_URL=http://minio:9000`** for signing and internal API traffic; **`minio`** is the Compose DNS name, not **`PUBLIC_HOST`**.
---
## 4. Startup order Compose enforces
| Order | Service | Blocking condition |
|------|---------|--------------------|
| 1 | `postgres`, `minio` | (none in compose—they start in parallel.) |
| 2 | `minio-cors` | `minio` **healthy** |
| 3 | `be0` | `postgres` **healthy** AND `minio` **healthy** |
| 4 | `fe0` | `be0` **started** |
If Postgres never becomes healthy (**bad `POSTGRES_*` vs existing volume**, etc.), **`be0` never attaches** cleanly and **`fe0`** may misbehave or appear “up” while API calls fail.
---
## 5. Deploy checklist (recommended)
From the **repository root on the VPS** (same folder as `docker-compose.prod.yml`):
1. **`.env`** present (`ls -a`), values filled from `.env.example`.
2. **`PUBLIC_HOST`** = the hostname or IP users type in the browser (must match how you open the UI and how MinIO URLs are generated).
3. **`./scripts/verify-prod-env.sh`** exits `0`.
4. Start the stack (pick one):
- **Script (pull, build, detached):** `./scripts/deploy-prod.sh`
- **Manual compose:** see **subsection 5.1** below.
5. Open app with **`http://`**, not **`https://`**, unless you put a reverse proxy in front.
### 5.1 Manual `docker compose -f docker-compose.prod.yml up`
This is valid as long as you stay in the **repo root** and a **`.env`** file exists there.
- **Variable substitution:** Compose automatically reads a file named **`.env`** in the **project directory** (normally your current working directory) and uses it to expand `${PUBLIC_HOST}`, `${FE_PORT}`, etc. in `docker-compose.prod.yml`. You do **not** have to pass `--env-file .env` for that to work, but being explicit avoids surprises:
```bash
docker compose --env-file .env -f docker-compose.prod.yml up -d --build
```
- **Foreground vs daemon:** plain `up` streams logs in the terminal and exits with Ctrl+C (containers stop unless you use `--abort-on-container-exit` behavior—default stops on interrupt). For a long-running server, prefer **`up -d`** (detached).
- **Rebuild after Dockerfile or dependency changes:** add **`--build`** (the deploy script always builds). Without it, Compose may reuse old images.
- **No pre-checks:** the script runs `verify-prod-env.sh` and `compose config` for you; if you use only `up`, run **`./scripts/verify-prod-env.sh`** yourself first so bad `POSTGRES_USER` / empty ports fail fast.
Example minimal manual flow:
```bash
cd /path/to/remix-of-my-perspective-lifestyle-32
./scripts/verify-prod-env.sh
docker compose --env-file .env -f docker-compose.prod.yml up -d --build
docker compose --env-file .env -f docker-compose.prod.yml ps
```
---
## 6. Quick verification commands
Run on the host with the same `--env-file` you use for deploy:
```bash
docker compose --env-file .env -f docker-compose.prod.yml ps
docker compose --env-file .env -f docker-compose.prod.yml logs --tail=80 postgres
docker compose --env-file .env -f docker-compose.prod.yml logs --tail=80 be0
docker compose --env-file .env -f docker-compose.prod.yml logs --tail=40 fe0
docker compose --env-file .env -f docker-compose.prod.yml logs --tail=40 minio
```
Smoke checks:
- **Postgres**: `docker compose ... exec postgres pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"` (substitute real values from `.env` when using shell snippets).
- **Backend** (from host): `curl -sS http://127.0.0.1:4402/docs` — expect FastAPI Swagger HTML (or `/openapi.json`).
- **MinIO** (from host or laptop if firewall allows): `curl -sS -o /dev/null -w "%{http_code}" http://${PUBLIC_HOST}:${MINIO_API_PORT}/minio/health/live`.
---
## 7. Firewall hint
Typically you must allow **inbound TCP**: **`FE_PORT`**, **`MINIO_API_PORT`**, **`MINIO_CONSOLE_PORT`** (and **`22`** for SSH). Postgres and **`be0`** intentionally stay on **localhost-only** binds in this compose file.