9.7 KiB
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:
- A PostgreSQL 16 database resource (one-click or Docker image).
- 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:443suffix). 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.depoints to the public IP of the Coolify server (Traefik will answer on:443and route byHostheader). - The CRM source code lives in a Forgejo repository that Coolify can
clone. Suggested location:
https://forge.media-on.de/leopoldadmin/crm-system(branchmaster).If the repo does not exist yet, create it and push the project:
# One-time: create the repo via Forgejo API or UI git remote add origin https://leopoldadmin:<TOKEN>@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:
- Go to Databases → + Add.
- Choose PostgreSQL 16 (Alpine).
- 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)
- Name:
- Click Deploy and wait for status
running:healthy. - Note the internal host:port Coolify exposes (typically
<resource-uuid>-postgres:5432). You will need it in step 3.
Alternative (API):
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":"<STRONG_PASSWORD>", "postgres_db":"crm_db","is_public":false}'
3. Resource B — crm-app (Dockerfile build)
In the Coolify UI:
- Projects → + Add Project if you don't have one yet (e.g.
CRM). - Environment → + Add Environment → name:
production. - Inside that environment, + Add → Application → Public/Private Repository.
- 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
- Git repository:
- Click Deploy once to let Coolify create the resource (it will fail to start without environment variables — that's expected).
- 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:<PW>@<postgres-internal-host>: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)
# 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):
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:<PW>@<PG_HOST>:5432/crm_db"}, {"key":"AUTH_SECRET", "value":"<TOKEN_URLSAFE_48>"}, {"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
:443is mandatory. See section 0.
- ⚠️ Port
- Let's Encrypt: enabled (default).
- Click Save. Coolify will issue the certificate and reload Traefik.
Alternative (API):
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
HEALTHCHECKis 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:
- Clone the repo (branch
master). - Build the multi-stage Dockerfile (≈ 1–2 min, depending on cache).
- Start the container.
prestart.shrunsalembic upgrade headagainst the Postgres database. - Uvicorn binds to
0.0.0.0:8000and starts serving.
A healthy deploy ends with the container status running:healthy.
Alternative (API):
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:
# 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 <access_token>` 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(notrunning:unhealthy,exited, orstarting). - Confirm the Postgres resource is
running:healthyand theDATABASE_URLhost matches its internal DNS name.
For full incident response, see /a0/.a0/runbook-restore.md.
9. Going forward — redeploys
- Code change → push to
masteron Forgejo → Deployments → Deploy in Coolify. The Dockerfile layer-cache will reusepip install -r requirements.txtifrequirements.txtis unchanged. - Environment variable change → edit in Coolify UI (or
PATCH .../envs/bulkvia 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