# Coolify Setup — CRM System v1.0 Production deployment guide for the **CRM System** to the Coolify PaaS instance at `server.media-on.de` (server UUID `lw80w8scs4044gwcw084s00s4`). The deploy consists of **two Coolify resources** in the same project/environment: 1. A **PostgreSQL 16** database resource (one-click or Docker image). 2. The **crm-app** Application (Dockerfile build from a Git repository). The two resources talk to each other over the internal Docker network. The app is exposed publicly on `https://crm.media-on.de:443` (Let's Encrypt via Coolify). --- ## 0. ⚠️ Critical domain-format gotcha Coolify's per-application **Domain field must contain an explicit port** in the URL. If you enter the domain without `:443`, Let's Encrypt certificate issuance will silently fail and Traefik will not route traffic correctly. ``` ✅ https://crm.media-on.de:443 ❌ https://crm.media-on.de ❌ crm.media-on.de ``` > The same rule applies in the Coolify API: when calling > `PATCH /api/v1/applications/{uuid}` you must set > `{"domains": "https://crm.media-on.de:443"}` (note the `:443` suffix). > This is a known bug-fix from earlier deployments — never drop the port. --- ## 1. Prerequisites - Coolify server reachable at `https://server.media-on.de`, API token created in *Keys & Tokens → API tokens* (Bearer token, scope: `*`). - The DNS **A record** for `crm.media-on.de` points to the public IP of the Coolify server (Traefik will answer on `:443` and route by `Host` header). - The CRM source code lives in a **Forgejo repository** that Coolify can clone. Suggested location: `https://forge.media-on.de/leopoldadmin/crm-system` (branch `master`). > If the repo does not exist yet, create it and push the project: > ```bash > # One-time: create the repo via Forgejo API or UI > git remote add origin https://leopoldadmin:@forge.media-on.de/leopoldadmin/crm-system.git > git push -u origin master > ``` - You have the **internal host:port** of the Postgres resource that will be provisioned in step 2 (Coolify will print it, e.g. `abc123-postgres:5432`). --- ## 2. Resource A — PostgreSQL 16 database In the Coolify UI: 1. Go to **Databases → + Add**. 2. Choose **PostgreSQL 16** (Alpine). 3. Configuration: - **Name**: `crm-postgres` - **Database name**: `crm_db` - **User**: `crm_user` - **Password**: *(generate a strong one — see Secret generation below)* - **Public accessibility**: **disabled** (only the crm-app talks to it) 4. Click **Deploy** and wait for status `running:healthy`. 5. Note the **internal host:port** Coolify exposes (typically `-postgres:5432`). You will need it in step 3. > **Alternative (API):** > ```bash > curl -X POST http://server.media-on.de/api/v1/databases \ > -H "Authorization: Bearer $COOLIFY_TOKEN" \ > -H "Content-Type: application/json" \ > -d '{"type":"postgresql","project_uuid":"...","environment_name":"production", > "server_uuid":"lw80w8scs4044gwcw084s00s4", > "name":"crm-postgres","postgres_user":"crm_user", > "postgres_password":"", > "postgres_db":"crm_db","is_public":false}' > ``` --- ## 3. Resource B — crm-app (Dockerfile build) In the Coolify UI: 1. **Projects → + Add Project** if you don't have one yet (e.g. `CRM`). 2. **Environment → + Add Environment** → name: `production`. 3. Inside that environment, **+ Add → Application → Public/Private Repository**. 4. Fill in: - **Git repository**: `https://forge.media-on.de/leopoldadmin/crm-system` - **Branch**: `master` - **Build pack**: `Dockerfile` - **Dockerfile location**: `Dockerfile` (default, repo root) - **Port**: `8000` 5. Click **Deploy** once to let Coolify create the resource (it will fail to start without environment variables — that's expected). 6. Note the **Application UUID** (visible in the URL or via `GET /api/v1/applications`). --- ## 4. Environment variables (on the crm-app resource) In **crm-app → Environment Variables**, set: | Key | Value | Notes | |-----|-------|-------| | `DATABASE_URL` | `postgresql+asyncpg://crm_user:@:5432/crm_db` | Use the internal host from step 2 (e.g. `crm-postgres-xyz:5432`), **not** `localhost` and **not** the public DNS. | | `AUTH_SECRET` | *see secret generation* | **MUST be ≥ 32 chars.** | | `CORS_ORIGINS` | `https://crm.media-on.de:443` | Comma-separated, no wildcards, must match the domain where the browser actually loads the SPA. | | `ENVIRONMENT` | `production` | | | `LOG_LEVEL` | `INFO` | `DEBUG` only temporarily. | | `BCRYPT_ROUNDS` | `12` | Aligned with `.env.example`. | | `JWT_ALGORITHM` | `HS256` | Aligned with `.env.example`. | | `JWT_EXPIRY_HOURS` | `24` | Aligned with `.env.example`. | ### Secret generation (run once, locally) ```bash # AUTH_SECRET (min 32 chars, recommended 48+) python -c "import secrets; print(secrets.token_urlsafe(48))" # POSTGRES_PASSWORD (min 16 chars, recommended 24+) python -c "import secrets; print(secrets.token_urlsafe(24))" ``` **Never commit these values.** Coolify stores them encrypted at rest, but they are still rendered in the UI to anyone with read access to the environment. > **Alternative (API — bulk update):** > ```bash > curl -X PATCH http://server.media-on.de/api/v1/applications/$APP_UUID/envs/bulk \ > -H "Authorization: Bearer $COOLIFY_TOKEN" \ > -H "Content-Type: application/json" \ > -d '{ > "data": [ > {"key":"DATABASE_URL", "value":"postgresql+asyncpg://crm_user:@:5432/crm_db"}, > {"key":"AUTH_SECRET", "value":""}, > {"key":"CORS_ORIGINS", "value":"https://crm.media-on.de:443"}, > {"key":"ENVIRONMENT", "value":"production"}, > {"key":"LOG_LEVEL", "value":"INFO"}, > {"key":"BCRYPT_ROUNDS", "value":"12"}, > {"key":"JWT_ALGORITHM", "value":"HS256"}, > {"key":"JWT_EXPIRY_HOURS", "value":"24"} > ] > }' > ``` --- ## 5. Configure the public domain (with port!) In **crm-app → Domains → + Add Domain**: - **Domain**: `https://crm.media-on.de:443` - ⚠️ **Port `:443` is mandatory.** See section 0. - **Let's Encrypt**: **enabled** (default). - Click **Save**. Coolify will issue the certificate and reload Traefik. > **Alternative (API):** > ```bash > curl -X PATCH http://server.media-on.de/api/v1/applications/$APP_UUID \ > -H "Authorization: Bearer $COOLIFY_TOKEN" \ > -H "Content-Type: application/json" \ > -d '{"domains": "https://crm.media-on.de:443"}' > ``` --- ## 6. Healthcheck (Coolify side) In **crm-app → Advanced → Healthcheck**: - **Healthcheck path**: `/health` - **Healthcheck method**: `GET` - **Healthcheck interval**: `30s` - **Healthcheck timeout**: `10s` - **Healthcheck retries**: `3` - **Healthcheck start period**: `15s` > The Dockerfile's in-container `HEALTHCHECK` is the source of truth for > Docker-level health. The Coolify/Traefik healthcheck is what drives > automatic rollbacks and load-balancer routing. Set both, identically. --- ## 7. Build & deploy In the Coolify UI: **crm-app → Deployments → Deploy**. Watch the build log. The first deploy will: 1. Clone the repo (branch `master`). 2. Build the multi-stage Dockerfile (≈ 1–2 min, depending on cache). 3. Start the container. `prestart.sh` runs `alembic upgrade head` against the Postgres database. 4. Uvicorn binds to `0.0.0.0:8000` and starts serving. A healthy deploy ends with the container status `running:healthy`. > **Alternative (API):** > ```bash > curl -X POST http://server.media-on.de/api/v1/deploy \ > -H "Authorization: Bearer $COOLIFY_TOKEN" \ > -H "Content-Type: application/json" \ > -d "{\"uuid\":\"$APP_UUID\"}" > ``` --- ## 8. Verification From anywhere with internet access: ```bash # 1. Root health (used by Docker HEALTHCHECK & Coolify healthcheck) curl -fsSL -o /dev/null -w "%{http_code}\n" https://crm.media-on.de:443/health # → 200 # 2. API v1 health (mounted under the versioned router) curl -fsSL -o /dev/null -w "%{http_code}\n" https://crm.media-on.de:443/api/v1/health # → 200 # 3. Frontend SPA (served by the static-files mount) curl -fsSL -o /dev/null -w "%{http_code} %{content_type}\n" \ https://crm.media-on.de:443/index.html # → 200 text/html # 4. Interactive API docs # Open in a browser: https://crm.media-on.de:443/docs # Register a user via POST /api/v1/auth/register # Login via POST /api/v1/auth/login → access_token # Use the token as `Authorization: Bearer ` on protected routes ``` If any of these return `502` / `503` / `504`: - Check **crm-app → Logs** in Coolify (the UI is the only place with full stdout/stderr, the API does not expose logs). - Confirm the container is `running:healthy` (not `running:unhealthy`, `exited`, or `starting`). - Confirm the Postgres resource is `running:healthy` and the `DATABASE_URL` host matches its internal DNS name. For full incident response, see [`/a0/.a0/runbook-restore.md`](../../a0/runbook-restore.md). --- ## 9. Going forward — redeploys - **Code change** → push to `master` on Forgejo → **Deployments → Deploy** in Coolify. The Dockerfile layer-cache will reuse `pip install -r requirements.txt` if `requirements.txt` is unchanged. - **Environment variable change** → edit in Coolify UI (or `PATCH .../envs/bulk` via API) → **Deploy** (Coolify does *not* auto-restart on ENV change alone). - **Domain change** → use the API (`PATCH /api/v1/applications/{uuid}`) so it is reproducible; the UI is a fallback only. --- ## 10. References - Coolify v4 API — `/a0/usr/plugins/coolify_control/help/coolify-control/help.md` - App architecture (Section 13 lockdown) — `/a0/.a0/02-architecture.md` - Task graph (Phase 4d) — `/a0/.a0/03-task-graph.json` - Restore runbook — `/a0/.a0/runbook-restore.md`