refactor(deploy): remove old multi-resource code, document single docker-compose workflow
- Remove POSTGRES_COMPOSE, REDIS_COMPOSE templates (unused) - Remove create_service(), create_api_application(), generate_worker_compose() - Remove deploy_worker(), deploy_worker_only(), verify_worker_service() - Remove resolve_worker_uuid() and all worker_uuid references - Remove get_worker_envs(), get_api_envs(), get_postgres_envs(), get_redis_envs() - Remove set_service_envs(), set_application_envs() (dead code) - Remove _extract_deploy_uuid(), _wait_service_healthy() (only used by deploy_worker) - Remove seed_admin_user() (only used by old deploy_full) - Remove DB_HOST, REDIS_HOST, WORKER_UUID, WORKER_NAME config vars - Remove --worker-only CLI arg - Replace old deploy_full() with simple redeploy via /api/v1/deploy - Update run_verification() to remove worker_uuid param - Add KI workflow comment at top of deploy.py - Update DEPLOY.md: single docker-compose stack workflow - Update COOLIFY_SETUP.md: single docker-compose stack, remove 3-resource setup - Update docs/INSTALL.md: automated --initial workflow deploy.py: 1370 → 893 lines (-477 lines, -35%)
This commit is contained in:
+190
-266
@@ -1,177 +1,195 @@
|
|||||||
# Coolify Setup — CRM System v1.0
|
# Coolify Setup — LeoCRM
|
||||||
|
|
||||||
Production deployment guide for the **CRM System** to the Coolify PaaS instance
|
Production deployment guide for LeoCRM to the Coolify PaaS instance
|
||||||
at `server.media-on.de` (server UUID `lw80w8scs4044gwcw084s00s4`).
|
at `server.media-on.de`.
|
||||||
|
|
||||||
The deploy consists of **three Coolify resources** in the same project/environment:
|
## Architektur
|
||||||
|
|
||||||
1. A **PostgreSQL 16** database resource (one-click or Docker image).
|
LeoCRM läuft als **einzelner docker-compose Stack** in einer Coolify Application.
|
||||||
2. The **crm-app** Application (Dockerfile build from a Git repository).
|
Coolify liest die `docker-compose.yaml` aus dem Git-Repo und startet alle 4
|
||||||
3. The **crm-worker** Application (same Dockerfile build, different entrypoint).
|
Container (postgres, redis, crm_app, crm_worker) in einem gemeinsamen Stack.
|
||||||
|
|
||||||
The 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).
|
Coolify Application (build_pack=dockercompose)
|
||||||
The worker is not exposed publicly — it only needs Redis and PostgreSQL access.
|
├── postgres (pgvector/pgvector:pg16)
|
||||||
|
├── redis (redis:7-alpine)
|
||||||
|
├── crm_app (FastAPI API Server, Port 8000)
|
||||||
|
└── crm_worker (ARQ Background Worker)
|
||||||
|
```
|
||||||
|
|
||||||
|
Alle Container teilen sich ein Docker-Netzwerk. Service-Namen funktionieren
|
||||||
|
als DNS-Namen (z.B. `postgres`, `redis`, `crm_app`, `crm_worker`).
|
||||||
|
|
||||||
|
**Keine separaten Coolify Services** für DB/Redis/Worker. Das funktioniert nicht,
|
||||||
|
weil Coolify jedem Service ein eigenes Netzwerk gibt und die Container sich
|
||||||
|
nicht per DNS erreichen können.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 0. ⚠️ Critical domain-format gotcha
|
## 1. Voraussetzungen
|
||||||
|
|
||||||
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
|
- Coolify server reachable at `https://server.media-on.de`, API token created
|
||||||
in *Keys & Tokens → API tokens* (Bearer token, scope: `*`).
|
in *Keys & Tokens → API tokens* (Bearer token, scope: `*`).
|
||||||
- The DNS **A record** for `crm.media-on.de` points to the public IP of the
|
- DNS **A record** for die App-Domain (z.B. `crm.media-on.de`) zeigt auf die
|
||||||
Coolify server (Traefik will answer on `:443` and route by `Host` header).
|
öffentliche IP des Coolify-Servers.
|
||||||
- The CRM source code lives in a **Forgejo repository** that Coolify can
|
- LeoCRM source code in Forgejo repository:
|
||||||
clone. Suggested location:
|
`https://forgejo.media-on.de/Leopoldadmin/leocrm.git` (branch `main`).
|
||||||
`https://forge.media-on.de/leopoldadmin/crm-system` (branch `master`).
|
- Ein **Private Deploy Key** in Coolify hinterlegt (für Git-Zugriff).
|
||||||
> If the repo does not exist yet, create it and push the project:
|
- Python 3.12+ mit `httpx` für das deploy script.
|
||||||
> ```bash
|
|
||||||
> # 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
|
## 2. Initial Deployment (automatisiert)
|
||||||
|
|
||||||
In the Coolify UI:
|
### 2.1 Umgebungsvariablen setzen
|
||||||
|
|
||||||
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
|
|
||||||
`<resource-uuid>-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":"<STRONG_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:<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`. |
|
|
||||||
|
|
||||||
|
|
||||||
### Secret generation (run once, locally)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# AUTH_SECRET (min 32 chars, recommended 48+)
|
export COOLIFY_API_TOKEN="dein-token"
|
||||||
python -c "import secrets; print(secrets.token_urlsafe(48))"
|
export APP_DOMAIN="https://crm.media-on.de"
|
||||||
|
export APP_NAME="leocrm"
|
||||||
# POSTGRES_PASSWORD (min 16 chars, recommended 24+)
|
export DB_PASSWORD="<sicheres-passwort>"
|
||||||
python -c "import secrets; print(secrets.token_urlsafe(24))"
|
export REDIS_PASSWORD="<sicheres-passwort>"
|
||||||
|
export SECRET_KEY="<mindestens-32-zeichen>"
|
||||||
|
export COOLIFY_PROJECT_UUID="<project-uuid>"
|
||||||
|
export COOLIFY_SERVER_UUID="<server-uuid>"
|
||||||
|
export COOLIFY_PRIVATE_KEY_UUID="<private-key-uuid>"
|
||||||
|
export COOLIFY_ENVIRONMENT="production" # optional
|
||||||
|
export ADMIN_EMAIL="admin@media-on.de" # optional
|
||||||
|
export ADMIN_PASSWORD="Admin123!" # optional
|
||||||
```
|
```
|
||||||
|
|
||||||
**Never commit these values.** Coolify stores them encrypted at rest, but they
|
### 2.2 Deploy starten
|
||||||
are still rendered in the UI to anyone with read access to the environment.
|
|
||||||
|
|
||||||
> **Alternative (API — bulk update):**
|
```bash
|
||||||
> ```bash
|
python scripts/deploy.py --initial
|
||||||
> 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" \
|
Das Script führt einen **2-Phase Deploy** durch:
|
||||||
> -d '{
|
|
||||||
> "data": [
|
1. **Phase 1**: Application via `private-deploy-key` erstellen, build_pack auf
|
||||||
> {"key":"DATABASE_URL", "value":"postgresql+asyncpg://crm_user:<PW>@<PG_HOST>:5432/crm_db"},
|
`dockercompose` setzen, ENV-Variablen setzen, erster Deploy **ohne Domain**.
|
||||||
> {"key":"AUTH_SECRET", "value":"<TOKEN_URLSAFE_48>"},
|
Coolify liest `docker-compose.yaml` aus dem Git-Repo und baut alle Container.
|
||||||
> {"key":"CORS_ORIGINS", "value":"https://crm.media-on.de:443"},
|
|
||||||
> {"key":"ENVIRONMENT", "value":"production"},
|
2. **Phase 2**: `docker_compose_domains` setzen (für Traefik-Labels), dann
|
||||||
> {"key":"LOG_LEVEL", "value":"INFO"},
|
Redeploy. Jetzt ist die App unter der Domain erreichbar.
|
||||||
> {"key":"BCRYPT_ROUNDS", "value":"12"}
|
|
||||||
> ]
|
### 2.3 Warum 2-Phase Deploy?
|
||||||
> }'
|
|
||||||
> ```
|
Coolify muss zuerst die `docker-compose.yaml` aus dem Git-Repo lesen, um die
|
||||||
|
Service-Namen zu kennen. Erst dann kann `docker_compose_domains` korrekt
|
||||||
|
zugeordnet werden. Ein Deploy ohne vorherigen Read der Compose-Datei führt zu
|
||||||
|
fehlenden Traefik-Labels → 503 Fehler.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Configure the public domain (with port!)
|
## 3. Redeploy (bestehende Anwendung)
|
||||||
|
|
||||||
In **crm-app → Domains → + Add Domain**:
|
```bash
|
||||||
|
export COOLIFY_API_TOKEN="dein-token"
|
||||||
|
export APP_DOMAIN="https://crm.media-on.de"
|
||||||
|
export COOLIFY_APP_UUID="dx4pqdziu4uj6x9fxs1u5z0x" # optional
|
||||||
|
|
||||||
- **Domain**: `https://crm.media-on.de:443`
|
python scripts/deploy.py
|
||||||
- ⚠️ **Port `:443` is mandatory.** See section 0.
|
```
|
||||||
- **Let's Encrypt**: **enabled** (default).
|
|
||||||
- Click **Save**. Coolify will issue the certificate and reload Traefik.
|
|
||||||
|
|
||||||
> **Alternative (API):**
|
Triggert `/api/v1/deploy` für die bestehende Coolify Application und wartet auf
|
||||||
> ```bash
|
Erfolg. Danach läuft automatisch die Verifikation (HTTP, Login, Alembic, RLS).
|
||||||
> 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)
|
## 4. Verifikation
|
||||||
|
|
||||||
In **crm-app → Advanced → Healthcheck**:
|
```bash
|
||||||
|
python scripts/deploy.py --verify-only
|
||||||
|
```
|
||||||
|
|
||||||
|
Prüft:
|
||||||
|
- HTTP Health (`/api/v1/health`)
|
||||||
|
- Login (optional, wenn LOGIN_EMAIL/LOGIN_PASSWORD gesetzt)
|
||||||
|
- Alembic Migration Head (via SSH in den postgres Container)
|
||||||
|
- RLS-Tabellen-Anzahl (via SSH)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Wichtige Hinweise
|
||||||
|
|
||||||
|
### 5.1 Service-Namen mit Unterstrichen
|
||||||
|
|
||||||
|
In `docker-compose.yaml` müssen Service-Namen **Unterstriche** verwenden:
|
||||||
|
`crm_app`, `crm_worker` — nicht `crm-app`, `crm-worker`.
|
||||||
|
|
||||||
|
Coolify konvertiert Bindestriche zu Unterstrichen in `docker_compose_domains`.
|
||||||
|
Bei Bindestrichen in der Compose-Datei gibt es keinen Match → keine
|
||||||
|
Traefik-Labels → 503 Fehler.
|
||||||
|
|
||||||
|
### 5.2 docker-compose.yaml (nicht .yml)
|
||||||
|
|
||||||
|
Coolify sucht nach `docker-compose.yaml` (mit `.yaml`). Eine Datei namens
|
||||||
|
`docker-compose.yml` wird nicht gefunden.
|
||||||
|
|
||||||
|
### 5.3 Domain ohne :443
|
||||||
|
|
||||||
|
In `docker_compose_domains` darf die Domain **kein** `:443` am Ende haben:
|
||||||
|
```
|
||||||
|
✅ https://crm.media-on.de
|
||||||
|
❌ https://crm.media-on.de:443
|
||||||
|
```
|
||||||
|
Das `:443` führt zu leeren `Host()` Traefik-Labels.
|
||||||
|
|
||||||
|
### 5.4 Kein connect_to_docker_network
|
||||||
|
|
||||||
|
Innerhalb eines docker-compose Stacks kümmert sich Coolify selbst um das
|
||||||
|
Netzwerk. `connect_to_docker_network=True` ist nicht nötig und sollte nicht
|
||||||
|
gesetzt werden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Mehrere Instanzen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test-Instanz
|
||||||
|
APP_NAME=leocrm-test APP_DOMAIN=https://crm-test.media-on.de \
|
||||||
|
python scripts/deploy.py --initial
|
||||||
|
|
||||||
|
# Produktions-Instanz
|
||||||
|
APP_NAME=leocrm APP_DOMAIN=https://crm.media-on.de \
|
||||||
|
python scripts/deploy.py --initial
|
||||||
|
```
|
||||||
|
|
||||||
|
Jede Instanz hat eigene DB, Redis, Container und Domain. Alle Parameter werden
|
||||||
|
aus `APP_NAME` und `APP_DOMAIN` abgeleitet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Environment-Variablen in Coolify
|
||||||
|
|
||||||
|
Das `--initial` Script setzt automatisch folgende ENV-Variablen in Coolify:
|
||||||
|
|
||||||
|
| Key | Wert | Quelle |
|
||||||
|
|-----|------|--------|
|
||||||
|
| `POSTGRES_USER` | `crm_user` | Default |
|
||||||
|
| `POSTGRES_DB` | `crm_db` | Default |
|
||||||
|
| `DB_PASSWORD` | * | ENV |
|
||||||
|
| `REDIS_PASSWORD` | * | ENV |
|
||||||
|
| `SECRET_KEY` | * | ENV |
|
||||||
|
| `ENVIRONMENT` | `production` | Default |
|
||||||
|
| `LOG_LEVEL` | `INFO` | Default |
|
||||||
|
| `SESSION_COOKIE_SECURE` | `true` | Default |
|
||||||
|
| `STORAGE_PATH` | `/data/storage` | Default |
|
||||||
|
| `CORS_ORIGINS` | APP_DOMAIN | ENV |
|
||||||
|
| `FRONTEND_URL` | APP_DOMAIN | ENV |
|
||||||
|
| `APP_DOMAIN` | APP_DOMAIN | ENV |
|
||||||
|
| `ADMIN_EMAIL` | `admin@media-on.de` | ENV (optional) |
|
||||||
|
| `ADMIN_PASSWORD` | `Admin123!` | ENV (optional) |
|
||||||
|
|
||||||
|
Die `docker-compose.yaml` verwendet `${VARIABLE}` Syntax — Coolify substituiert
|
||||||
|
aus diesen ENV-Variablen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Healthcheck
|
||||||
|
|
||||||
|
In **Coolify → Application → Advanced → Healthcheck**:
|
||||||
|
|
||||||
- **Healthcheck path**: `/api/v1/health`
|
- **Healthcheck path**: `/api/v1/health`
|
||||||
- **Healthcheck method**: `GET`
|
- **Healthcheck method**: `GET`
|
||||||
@@ -180,138 +198,44 @@ In **crm-app → Advanced → Healthcheck**:
|
|||||||
- **Healthcheck retries**: `3`
|
- **Healthcheck retries**: `3`
|
||||||
- **Healthcheck start period**: `15s`
|
- **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.
|
## 9. Going forward — Redeploys
|
||||||
|
|
||||||
|
- **Code change** → push to `main` auf Forgejo → `python scripts/deploy.py`
|
||||||
|
(oder Coolify UI → Deployments → Deploy).
|
||||||
|
- **Environment variable change** → Coolify UI (oder API `PATCH .../envs/bulk`)
|
||||||
|
→ Deploy (Coolify startet nicht automatisch bei ENV-Änderung neu).
|
||||||
|
- **Domain change** → API (`PATCH /api/v1/applications/{uuid}` mit
|
||||||
|
`docker_compose_domains`) — reproduzierbar, UI als Fallback.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Build & deploy
|
## 10. Troubleshooting
|
||||||
|
|
||||||
In the Coolify UI: **crm-app → Deployments → Deploy**.
|
**503 Fehler (Traefik):**
|
||||||
|
- Domain ohne `:443` in `docker_compose_domains`
|
||||||
|
- Service-Namen mit Unterstrichen in docker-compose.yaml
|
||||||
|
- `docker-compose.yaml` (nicht `.yml`)
|
||||||
|
|
||||||
Watch the build log. The first deploy will:
|
**Container können sich nicht erreichen (DNS):**
|
||||||
|
- Alles in einem docker-compose Stack (nicht separate Coolify Services)
|
||||||
|
- Kein `connect_to_docker_network` setzen
|
||||||
|
|
||||||
1. Clone the repo (branch `master`).
|
**"Docker Compose file not found":**
|
||||||
2. Build the multi-stage Dockerfile (≈ 1–2 min, depending on cache).
|
- Datei heißt `docker-compose.yaml` (nicht `.yml`)
|
||||||
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:
|
|
||||||
|
|
||||||
|
**Migration fehlgeschlagen:**
|
||||||
```bash
|
```bash
|
||||||
# 1. Root health (used by Docker HEALTHCHECK & Coolify healthcheck)
|
docker exec <postgres-container> psql -U crm_user -d crm_db -c "SELECT version_num FROM alembic_version"
|
||||||
curl -fsSL -o /dev/null -w "%{http_code}\n" https://crm.media-on.de:443/health
|
docker exec <api-container> alembic upgrade head
|
||||||
# → 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` (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
|
## 11. Referenzen
|
||||||
|
|
||||||
- **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`
|
- Coolify v4 API — `/a0/usr/plugins/coolify_control/help/coolify-control/help.md`
|
||||||
- App architecture (Section 13 lockdown) — `/a0/.a0/02-architecture.md`
|
- App architecture — `architecture.md`
|
||||||
- Task graph (Phase 4d) — `/a0/.a0/03-task-graph.json`
|
- Deploy script — `scripts/deploy.py`
|
||||||
- Restore runbook — `/a0/.a0/runbook-restore.md`
|
- Install guide — `docs/INSTALL.md`
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. Resource C — crm-worker (Background Worker)
|
|
||||||
|
|
||||||
The crm-worker runs the ARQ background worker and scheduler in a separate
|
|
||||||
container, using the same Docker image as crm-app but with a different
|
|
||||||
entrypoint (`/app/worker.sh` instead of `/app/prestart.sh`).
|
|
||||||
|
|
||||||
### Setup in Coolify UI
|
|
||||||
|
|
||||||
1. In the same project/environment as crm-app, **+ Add → Application →
|
|
||||||
Public/Private Repository**.
|
|
||||||
2. Fill in:
|
|
||||||
- **Git repository**: same as crm-app (`https://forgejo.media-on.de/Leopoldadmin/leocrm.git`)
|
|
||||||
- **Branch**: `main`
|
|
||||||
- **Build pack**: `Dockerfile`
|
|
||||||
- **Dockerfile location**: `Dockerfile` (same image)
|
|
||||||
- **Port**: `8000` (not used, but Coolify requires a port)
|
|
||||||
- **Custom Entrypoint**: `/app/worker.sh`
|
|
||||||
3. Click **Deploy** once to create the resource.
|
|
||||||
4. Note the **Application UUID**.
|
|
||||||
|
|
||||||
### Environment variables (on the crm-worker resource)
|
|
||||||
|
|
||||||
Set the same variables as crm-app, except:
|
|
||||||
|
|
||||||
| Key | Value | Notes |
|
|
||||||
|-----|-------|-------|
|
|
||||||
| `DATABASE_URL` | same as crm-app | |
|
|
||||||
| `REDIS_URL` | same as crm-app | |
|
|
||||||
| `SECRET_KEY` | same as crm-app | |
|
|
||||||
| `ENVIRONMENT` | `production` | |
|
|
||||||
| `LOG_LEVEL` | `INFO` | |
|
|
||||||
| `STORAGE_PATH` | `/data/storage` | |
|
|
||||||
|
|
||||||
No domain is needed — the worker is not publicly accessible.
|
|
||||||
|
|
||||||
### Healthcheck (Coolify side)
|
|
||||||
|
|
||||||
- **Healthcheck path**: `/api/v1/health` (not used by worker, but Coolify requires one)
|
|
||||||
- Alternatively, use a custom healthcheck command:
|
|
||||||
`pgrep -f "arq app.core.worker.WorkerSettings" || exit 1`
|
|
||||||
|
|
||||||
### Scaling
|
|
||||||
|
|
||||||
To scale the worker horizontally, deploy multiple crm-worker instances.
|
|
||||||
Cron jobs use a Redis-based distributed lock (`SET NX` with TTL) so only
|
|
||||||
one replica executes each scheduled job.
|
|
||||||
|
|||||||
@@ -1,39 +1,86 @@
|
|||||||
# LeoCRM Deployment
|
# LeoCRM Deployment
|
||||||
|
|
||||||
|
## Architektur
|
||||||
|
|
||||||
|
LeoCRM läuft als **einzelner docker-compose Stack** in Coolify. Alle 4 Container
|
||||||
|
(PostgreSQL, Redis, API, Worker) werden aus der `docker-compose.yaml` im Git-Repo
|
||||||
|
gestartet und teilen sich ein Docker-Netzwerk.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ Coolify Application (docker-compose) │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ │
|
||||||
|
│ │ postgres │ │ redis │ │
|
||||||
|
│ └────┬─────┘ └────┬─────┘ │
|
||||||
|
│ │ │ │
|
||||||
|
│ ┌────┴─────┐ ┌────┴─────┐ │
|
||||||
|
│ │ crm_app │ │crm_worker│ │
|
||||||
|
│ │ (API) │ │ (ARQ) │ │
|
||||||
|
│ └──────────┘ └──────────┘ │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Keine separaten Coolify Services für DB/Redis/Worker. Alles in einem Stack.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### Option A: Coolify (empfohlen für Produktion)
|
### Redeploy (bestehende Anwendung)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Einmalig: Umgebungsvariablen setzen
|
# Umgebungsvariablen setzen
|
||||||
export COOLIFY_API_TOKEN="dein-token"
|
export COOLIFY_API_TOKEN="dein-token"
|
||||||
export COOLIFY_APP_UUID="dx4pqdziu4uj6x9fxs1u5z0x" # oder via APP_DOMAIN auto-resolved
|
export APP_DOMAIN="https://crm.media-on.de"
|
||||||
|
export COOLIFY_APP_UUID="dx4pqdziu4uj6x9fxs1u5z0x" # optional, wird via APP_NAME gesucht
|
||||||
|
|
||||||
# Deploy
|
# Redeploy via Coolify API
|
||||||
python scripts/deploy.py
|
python scripts/deploy.py
|
||||||
|
|
||||||
# Redeploy (ohne Neubuild)
|
# Verifikation nur
|
||||||
python scripts/deploy.py --skip-build
|
python scripts/deploy.py --verify-only
|
||||||
```
|
```
|
||||||
|
|
||||||
Das Script macht automatisch:
|
Das Script macht automatisch:
|
||||||
1. Coolify Build & Deploy triggern
|
1. Coolify Application auflösen (via UUID oder Name)
|
||||||
2. Persistent Volume in Coolify DB konfigurieren (automatisch, portabel)
|
2. Deploy via `/api/v1/deploy` triggern
|
||||||
3. Auf healthy Container warten
|
3. Auf Deployment-Erfolg warten
|
||||||
4. RLS auf allen Tenant-Tabellen sicherstellen
|
4. HTTP Health-Check verifizieren
|
||||||
5. DB-Migrationen verifizieren
|
5. Login-Test (optional, wenn LOGIN_EMAIL/LOGIN_PASSWORD gesetzt)
|
||||||
6. Worker-Container starten
|
6. Alembic-Migration-Head prüfen (via SSH)
|
||||||
7. App-Health verifizieren
|
7. RLS-Tabellen zählen (via SSH)
|
||||||
8. Domain-Erreichbarkeit prüfen
|
|
||||||
|
|
||||||
**Funktioniert auf jeder Coolify-Instanz. Bei mehreren Apps. Bei Erst-Deploy und Redeploy.**
|
### Initial Deployment (neue Anwendung)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Alle Umgebungsvariablen setzen
|
||||||
|
export COOLIFY_API_TOKEN="dein-token"
|
||||||
|
export APP_DOMAIN="https://crm.media-on.de"
|
||||||
|
export APP_NAME="leocrm" # Coolify Application Name
|
||||||
|
export DB_PASSWORD="..."
|
||||||
|
export REDIS_PASSWORD="..."
|
||||||
|
export SECRET_KEY="..."
|
||||||
|
export COOLIFY_PROJECT_UUID="..."
|
||||||
|
export COOLIFY_SERVER_UUID="..."
|
||||||
|
export COOLIFY_PRIVATE_KEY_UUID="..."
|
||||||
|
export COOLIFY_ENVIRONMENT="production" # optional, default: production
|
||||||
|
|
||||||
|
# Initial deployment
|
||||||
|
python scripts/deploy.py --initial
|
||||||
|
```
|
||||||
|
|
||||||
|
Das Script macht automatisch:
|
||||||
|
1. Coolify Application via `private-deploy-key` erstellen
|
||||||
|
2. Build Pack auf `dockercompose` setzen (liest docker-compose.yaml aus Git)
|
||||||
|
3. Environment-Variablen setzen (Secrets, Domain, Admin-Credentials)
|
||||||
|
4. Erster Deploy (ohne Domain — Coolify muss docker-compose.yaml lesen)
|
||||||
|
5. `docker_compose_domains` setzen + Redeploy (mit Traefik-Labels)
|
||||||
|
6. Verifikation (HTTP, Login, Alembic, RLS)
|
||||||
|
|
||||||
### Option B: Docker Compose (lokal / ohne Coolify)
|
### Option B: Docker Compose (lokal / ohne Coolify)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# .env.docker erstellen
|
# .env.docker erstellen
|
||||||
cp .env.docker.example .env.docker
|
cp .env.docker.example .env.docker
|
||||||
$EDITOR .env.docker # SECRET_KEY, POSTGRES_PASSWORD, etc. ausfüllen
|
$EDITOR .env.docker # SECRET_KEY, DB_PASSWORD, REDIS_PASSWORD etc. ausfüllen
|
||||||
|
|
||||||
# Starten (alle 4 Container: Postgres, Redis, App, Worker)
|
# Starten (alle 4 Container: Postgres, Redis, App, Worker)
|
||||||
docker compose --env-file .env.docker up --build -d
|
docker compose --env-file .env.docker up --build -d
|
||||||
@@ -46,19 +93,20 @@ docker compose down
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Container:**
|
**Container:**
|
||||||
- `crm-postgres` — PostgreSQL 16 mit pgvector
|
- `postgres` — PostgreSQL 16 mit pgvector
|
||||||
- `crm-redis` — Redis 7
|
- `redis` — Redis 7
|
||||||
- `crm-app` — FastAPI API Server
|
- `crm_app` — FastAPI API Server
|
||||||
- `crm-worker` — ARQ Background Worker
|
- `crm_worker` — ARQ Background Worker
|
||||||
|
|
||||||
Alle mit persistenten Volumes. Kein Datenverlust bei Redeploy.
|
Alle mit persistenten Volumes. Kein Datenverlust bei Redeploy.
|
||||||
|
|
||||||
## Voraussetzungen
|
## Voraussetzungen
|
||||||
|
|
||||||
- Python 3.12+
|
- Python 3.12+
|
||||||
|
- `httpx` Python package
|
||||||
- Docker & Docker Compose (für Option B)
|
- Docker & Docker Compose (für Option B)
|
||||||
- Coolify v4+ (für Option A)
|
- Coolify v4+ (für Option A)
|
||||||
- SSH-Zugang zum Server (für Option A)
|
- SSH-Zugang zum Server (für Verifikation, Option A)
|
||||||
|
|
||||||
## Umgebungsvariablen
|
## Umgebungsvariablen
|
||||||
|
|
||||||
@@ -66,13 +114,26 @@ Siehe `.env.example` für alle Variablen. Wichtigste:
|
|||||||
|
|
||||||
| Variable | Pflicht | Default | Beschreibung |
|
| Variable | Pflicht | Default | Beschreibung |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `DATABASE_URL` | Ja | — | PostgreSQL Connection String |
|
| `COOLIFY_API_TOKEN` | Ja | — | Coolify API Token |
|
||||||
| `REDIS_URL` | Ja | — | Redis Connection String |
|
| `APP_DOMAIN` | Ja | — | App-Domain (z.B. https://crm.media-on.de) |
|
||||||
| `SECRET_KEY` | Ja | — | Mindestens 32 Zeichen |
|
| `COOLIFY_APP_UUID` | Nein | — | Application UUID (auto-resolved via APP_NAME) |
|
||||||
| `ENVIRONMENT` | Nein | `development` | `production` oder `development` |
|
| `APP_NAME` | Nein | abgeleitet aus APP_DOMAIN | Coolify Application Name |
|
||||||
| `SESSION_COOKIE_SECURE` | Nein | `true` | In Production muss `true` |
|
| `SSH_KEY` | Nein | `/a0/usr/workdir/.ssh/coolify-01-root` | SSH Key für Verifikation |
|
||||||
| `STORAGE_PATH` | Nein | `/data/storage` | Datei-Upload-Pfad |
|
| `SERVER_IP` | Nein | `46.225.91.159` | Server IP für SSH |
|
||||||
| `STORAGE_BACKEND` | Nein | `local` | `local` oder `s3` |
|
| `LOGIN_EMAIL` | Nein | — | Login-Test Email (optional) |
|
||||||
|
| `LOGIN_PASSWORD` | Nein | — | Login-Test Passwort (optional) |
|
||||||
|
|
||||||
|
### Nur für `--initial`:
|
||||||
|
|
||||||
|
| Variable | Pflicht | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `DB_PASSWORD` | Ja | PostgreSQL Passwort |
|
||||||
|
| `REDIS_PASSWORD` | Ja | Redis Passwort |
|
||||||
|
| `SECRET_KEY` | Ja | Application Secret Key (min. 32 Zeichen) |
|
||||||
|
| `COOLIFY_PROJECT_UUID` | Ja | Coolify Project UUID |
|
||||||
|
| `COOLIFY_SERVER_UUID` | Ja | Coolify Server UUID |
|
||||||
|
| `COOLIFY_PRIVATE_KEY_UUID` | Ja | Coolify Private Deploy Key UUID |
|
||||||
|
| `COOLIFY_ENVIRONMENT` | Nein | Coolify Environment (default: production) |
|
||||||
|
|
||||||
## S3 Storage (optional)
|
## S3 Storage (optional)
|
||||||
|
|
||||||
@@ -85,19 +146,19 @@ S3_ACCESS_KEY=...
|
|||||||
S3_SECRET_KEY=...
|
S3_SECRET_KEY=...
|
||||||
```
|
```
|
||||||
|
|
||||||
## Test- vs. Produktionsumgebung
|
## Mehrere Instanzen
|
||||||
|
|
||||||
**Test:**
|
Mehrere LeoCRM-Instanzen auf demselben Coolify-Server:
|
||||||
```bash
|
```bash
|
||||||
python scripts/deploy.py --environment test
|
# Test-Instanz
|
||||||
```
|
APP_NAME=leocrm-test APP_DOMAIN=https://crm-test.media-on.de python scripts/deploy.py --initial
|
||||||
Eigene Coolify-App, eigene DB, eigene Domain (`crm-test.media-on.de`).
|
|
||||||
|
|
||||||
**Produktion:**
|
# Produktions-Instanz
|
||||||
```bash
|
APP_NAME=leocrm APP_DOMAIN=https://crm.media-on.de python scripts/deploy.py --initial
|
||||||
python scripts/deploy.py --environment production
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Jede Instanz hat eigene DB, Redis, Container und Domain.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
**Container nicht healthy:**
|
**Container nicht healthy:**
|
||||||
@@ -110,15 +171,10 @@ docker logs <container-name> --tail 50
|
|||||||
docker exec <container> alembic upgrade head
|
docker exec <container> alembic upgrade head
|
||||||
```
|
```
|
||||||
|
|
||||||
**RLS nicht aktiv:**
|
**503 Fehler (Traefik):**
|
||||||
```bash
|
- Domain ohne `:443` in `docker_compose_domains` setzen
|
||||||
python scripts/deploy.py --migrate-only
|
- Service-Namen mit Unterstrichen in docker-compose.yaml (crm_app, nicht crm-app)
|
||||||
```
|
- `docker-compose.yaml` (nicht `.yml`) als Dateiname
|
||||||
|
|
||||||
**Worker nicht gestartet:**
|
|
||||||
```bash
|
|
||||||
python scripts/deploy.py --skip-build # startet Worker automatisch
|
|
||||||
```
|
|
||||||
|
|
||||||
## Backup & Restore
|
## Backup & Restore
|
||||||
|
|
||||||
@@ -126,46 +182,34 @@ python scripts/deploy.py --skip-build # startet Worker automatisch
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Full DB backup (run on the host or via docker exec)
|
# Full DB backup (run on the host or via docker exec)
|
||||||
docker exec crm-postgres pg_dump -U crm_user -Fc crm_db > backup_$(date +%Y%m%d_%H%M%S).dump
|
docker exec <postgres-container> pg_dump -U crm_user -Fc crm_db > backup_$(date +%Y%m%d_%H%M%S).dump
|
||||||
|
|
||||||
# Backup mit Custom-Format (komprimiert, parallel restore-fähig)
|
|
||||||
docker exec crm-postgres pg_dump -U crm_user -Fc -Z 9 crm_db > backup_$(date +%Y%m%d).dump
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Backup (Redis — Sessions/Queues)
|
### Backup (Redis — Sessions/Queues)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Redis RDB Snapshot
|
docker exec <redis-container> redis-cli -a "$REDIS_PASSWORD" SAVE
|
||||||
docker exec crm-redis redis-cli -a "$REDIS_PASSWORD" SAVE
|
docker cp <redis-container>:/data/dump.rdb redis_backup_$(date +%Y%m%d).rdb
|
||||||
docker cp crm-redis:/data/dump.rdb redis_backup_$(date +%Y%m%d).rdb
|
|
||||||
```
|
|
||||||
|
|
||||||
### Backup (File Storage)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Local storage volume
|
|
||||||
docker run --rm -v leocrm-fix_storage:/data -v $(pwd):/backup alpine \
|
|
||||||
tar czf /backup/storage_$(date +%Y%m%d).tar.gz /data
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Restore (PostgreSQL)
|
### Restore (PostgreSQL)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Stop app containers
|
# Stop app containers
|
||||||
docker compose stop crm-app crm-worker
|
docker compose stop crm_app crm_worker
|
||||||
|
|
||||||
# Restore DB
|
# Restore DB
|
||||||
docker exec -i crm-postgres pg_restore -U crm_user -d crm_db --clean < backup_20260726.dump
|
docker exec -i <postgres-container> pg_restore -U crm_user -d crm_db --clean < backup_20260726.dump
|
||||||
|
|
||||||
# Restart app
|
# Restart app
|
||||||
docker compose start crm-app crm-worker
|
docker compose start crm_app crm_worker
|
||||||
```
|
```
|
||||||
|
|
||||||
### Automatisierte Backups (Cron)
|
### Automatisierte Backups (Cron)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# /etc/cron.d/leocrm-backup
|
# /etc/cron.d/leocrm-backup
|
||||||
0 2 * * * root docker exec crm-postgres pg_dump -U crm_user -Fc crm_db > /backups/leocrm_$(date +\%Y\%m\%d).dump
|
0 2 * * * root docker exec <postgres-container> pg_dump -U crm_user -Fc crm_db > /backups/leocrm_$(date +\%Y\%m\%d).dump
|
||||||
0 3 * * * root find /backups -name 'leocrm_*.dump' -mtime +30 -delete
|
0 3 * * * root find /backups -name 'leocrm_*.dump' -mtime +30 -delete
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+46
-14
@@ -241,28 +241,60 @@ SECRET_KEY=your-secret-key-with-at-least-32-characters!!
|
|||||||
---
|
---
|
||||||
|
|
||||||
## 6. Coolify-Setup
|
## 6. Coolify-Setup
|
||||||
|
### 6.1 Neue Anwendung erstellen (automatisiert)
|
||||||
|
|
||||||
### 6.1 Neue Anwendung erstellen
|
LeoCRM wird als einzelner docker-compose Stack in Coolify deployt. Alle 4 Container
|
||||||
|
(postgres, redis, crm_app, crm_worker) laufen in einer Coolify Application mit
|
||||||
|
`build_pack=dockercompose`.
|
||||||
|
|
||||||
1. In Coolify: **+ New Resource** → **Docker Compose**
|
```bash
|
||||||
2. Name: `leocrm`
|
# Umgebungsvariablen setzen
|
||||||
3. Compose-Datei einfügen (siehe oben)
|
export COOLIFY_API_TOKEN="dein-token"
|
||||||
4. Domain zuweisen: `crm.example.com`
|
export APP_DOMAIN="https://crm.example.com"
|
||||||
|
export APP_NAME="leocrm"
|
||||||
|
export DB_PASSWORD="YourSecurePassword2026"
|
||||||
|
export REDIS_PASSWORD="YourRedisPassword2026"
|
||||||
|
export SECRET_KEY="your-secret-key-with-at-least-32-characters!!"
|
||||||
|
export COOLIFY_PROJECT_UUID="<project-uuid>"
|
||||||
|
export COOLIFY_SERVER_UUID="<server-uuid>"
|
||||||
|
export COOLIFY_PRIVATE_KEY_UUID="<private-key-uuid>"
|
||||||
|
|
||||||
### 6.2 Environment-Variablen in Coolify
|
# Initial deployment (2-Phase: ohne Domain, dann mit Domain + Redeploy)
|
||||||
|
python scripts/deploy.py --initial
|
||||||
|
```
|
||||||
|
|
||||||
Alle Variablen aus der `.env`-Datei in Coolify als Environment-Variablen setzen.
|
Das Script macht automatisch:
|
||||||
|
1. Coolify Application via `private-deploy-key` erstellen
|
||||||
|
2. Build Pack auf `dockercompose` setzen (liest docker-compose.yaml aus Git)
|
||||||
|
3. Environment-Variablen setzen (Secrets, Domain, Admin-Credentials)
|
||||||
|
4. Erster Deploy (ohne Domain — Coolify muss docker-compose.yaml lesen)
|
||||||
|
5. `docker_compose_domains` setzen + Redeploy (mit Traefik-Labels)
|
||||||
|
6. Verifikation (HTTP, Login, Alembic, RLS)
|
||||||
|
|
||||||
### 6.3 Deploy
|
### 6.2 Redeploy (bestehende Anwendung)
|
||||||
|
|
||||||
1. **Deploy** klicken
|
```bash
|
||||||
2. Warten bis API-Container healthy wird (start_period: 180s)
|
export COOLIFY_API_TOKEN="dein-token"
|
||||||
3. Worker-Container wird automatisch healthy
|
export APP_DOMAIN="https://crm.example.com"
|
||||||
|
python scripts/deploy.py
|
||||||
|
```
|
||||||
|
|
||||||
### 6.4 WICHTIG: DB-Image
|
### 6.3 Wichtige Hinweise
|
||||||
|
|
||||||
|
- **docker-compose.yaml** (nicht `.yml`) — Coolify sucht nach `.yaml`
|
||||||
|
- **Service-Namen mit Unterstrichen**: `crm_app`, `crm_worker` (nicht mit Bindestrich)
|
||||||
|
- **Domain ohne `:443`** in `docker_compose_domains` — `:443` führt zu leeren Traefik-Labels
|
||||||
|
- **Kein `connect_to_docker_network`** — Coolify kümmert sich selbst um das Netzwerk im Stack
|
||||||
|
- **DB-Image**: MUSS `pgvector/pgvector:pg16` sein (nicht `postgres:16-alpine`)
|
||||||
|
|
||||||
|
### 6.4 Environment-Variablen in Coolify
|
||||||
|
|
||||||
|
Das `--initial` Script setzt automatisch alle benötigten ENV-Variablen in Coolify.
|
||||||
|
Die `docker-compose.yaml` verwendet `${VARIABLE}` Syntax — Coolify substituiert
|
||||||
|
aus diesen ENV-Variablen.
|
||||||
|
|
||||||
|
Siehe auch: `DEPLOY.md` und `COOLIFY_SETUP.md` für Details.
|
||||||
|
|
||||||
Das DB-Image MUSS `pgvector/pgvector:pg16` sein, nicht `postgres:16-alpine`.
|
|
||||||
LeoCRM benötigt die `vector`-Extension für die unified_search-Plugin-Migration.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+56
-533
@@ -1,37 +1,54 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Automated deployment script for LeoCRM via Coolify API.
|
"""Automated deployment script for LeoCRM via Coolify API.
|
||||||
|
|
||||||
All container management is done through the Coolify API — no manual
|
═══════════════════════════════════════════════════════════════════════
|
||||||
docker commands, no SSH for container lifecycle. SSH is used *only*
|
CURRENT WORKFLOW — Single docker-compose Stack
|
||||||
for post-deploy verification (Alembic version, RLS table count) because
|
═══════════════════════════════════════════════════════════════════════
|
||||||
the Coolify API does not expose database internals.
|
|
||||||
|
|
||||||
No UUIDs, domains, or secrets are hardcoded. Everything comes from
|
All 4 containers (postgres, redis, crm_app, crm_worker) run in a single
|
||||||
environment variables or is resolved via the Coolify API.
|
docker-compose stack managed by one Coolify Application.
|
||||||
|
|
||||||
|
deploy.py --initial → Create the docker-compose application from scratch
|
||||||
|
(private-deploy-key + PATCH to dockercompose).
|
||||||
|
Two-phase: first deploy without domain, then set
|
||||||
|
docker_compose_domains and redeploy.
|
||||||
|
|
||||||
|
deploy.py → Redeploy existing application via /api/v1/deploy.
|
||||||
|
|
||||||
|
deploy.py --verify-only → Run verification checks only.
|
||||||
|
|
||||||
|
No separate Coolify services for worker/db/redis. Everything is in one
|
||||||
|
stack so containers share a network and can reach each other by DNS.
|
||||||
|
═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python scripts/deploy.py # Full deploy (API + Worker)
|
python scripts/deploy.py # Redeploy via Coolify API
|
||||||
python scripts/deploy.py --skip-build # Skip build, just restart
|
python scripts/deploy.py --skip-build # Skip build, just restart
|
||||||
python scripts/deploy.py --worker-only # Only deploy worker service
|
|
||||||
python scripts/deploy.py --verify-only # Only run verification
|
|
||||||
python scripts/deploy.py --initial # Create all resources from scratch
|
python scripts/deploy.py --initial # Create all resources from scratch
|
||||||
|
python scripts/deploy.py --verify-only # Only run verification
|
||||||
|
|
||||||
Environment variables:
|
Environment variables:
|
||||||
COOLIFY_API_TOKEN — Coolify API token (required)
|
COOLIFY_API_TOKEN — Coolify API token (required)
|
||||||
COOLIFY_BASE_URL — Coolify base URL (default: https://server.media-on.de)
|
COOLIFY_BASE_URL — Coolify base URL (default: https://server.media-on.de)
|
||||||
COOLIFY_APP_UUID — Application UUID (optional, resolved via API if absent)
|
COOLIFY_APP_UUID — Application UUID (optional, resolved via API if absent)
|
||||||
COOLIFY_WORKER_UUID — Worker Service UUID (optional, resolved via API if absent)
|
APP_NAME — Application name for API lookup (default: derived from APP_DOMAIN)
|
||||||
APP_NAME — Application name for API lookup (default: leocrm-api)
|
|
||||||
WORKER_NAME — Worker name for API lookup (default: leocrm-worker)
|
|
||||||
APP_DOMAIN — App domain for health/FQDN (required, e.g. https://crm.media-on.de)
|
APP_DOMAIN — App domain for health/FQDN (required, e.g. https://crm.media-on.de)
|
||||||
SSH_KEY — SSH key path for verification (default: /a0/usr/workdir/.ssh/coolify-01-root)
|
SSH_KEY — SSH key path for verification (default: /a0/usr/workdir/.ssh/coolify-01-root)
|
||||||
SERVER_IP — Server IP for SSH verification (default: 46.225.91.159)
|
SERVER_IP — Server IP for SSH verification (default: 46.225.91.159)
|
||||||
|
LOGIN_EMAIL — Login test email (optional, for verification)
|
||||||
|
LOGIN_PASSWORD — Login test password (optional, for verification)
|
||||||
|
|
||||||
Secrets (required for --initial, used by regular deploy if setting ENVs):
|
Secrets (required for --initial):
|
||||||
DB_PASSWORD — PostgreSQL password for all roles
|
DB_PASSWORD — PostgreSQL password for all roles
|
||||||
REDIS_PASSWORD — Redis password
|
REDIS_PASSWORD — Redis password
|
||||||
SECRET_KEY — Application secret key
|
SECRET_KEY — Application secret key
|
||||||
|
|
||||||
|
Initial deploy only:
|
||||||
|
COOLIFY_PROJECT_UUID — Coolify project UUID
|
||||||
|
COOLIFY_SERVER_UUID — Coolify server UUID
|
||||||
|
COOLIFY_PRIVATE_KEY_UUID — Coolify private deploy key UUID
|
||||||
|
COOLIFY_ENVIRONMENT — Coolify environment name (default: production)
|
||||||
|
|
||||||
Exit codes:
|
Exit codes:
|
||||||
0 — deployment successful
|
0 — deployment successful
|
||||||
1 — deployment failed
|
1 — deployment failed
|
||||||
@@ -56,12 +73,10 @@ import httpx
|
|||||||
COOLIFY_BASE_URL = os.environ.get("COOLIFY_BASE_URL", "https://server.media-on.de")
|
COOLIFY_BASE_URL = os.environ.get("COOLIFY_BASE_URL", "https://server.media-on.de")
|
||||||
COOLIFY_TOKEN = os.environ.get("COOLIFY_API_TOKEN", "")
|
COOLIFY_TOKEN = os.environ.get("COOLIFY_API_TOKEN", "")
|
||||||
|
|
||||||
# UUIDs — from env or resolved via API lookup by name
|
# UUID — from env or resolved via API lookup by name
|
||||||
APP_UUID = os.environ.get("COOLIFY_APP_UUID", "")
|
APP_UUID = os.environ.get("COOLIFY_APP_UUID", "")
|
||||||
WORKER_UUID = os.environ.get("COOLIFY_WORKER_UUID", "")
|
|
||||||
|
|
||||||
# Names for API lookup when UUIDs are not provided
|
# Derive APP_NAME from APP_DOMAIN if not set (e.g. https://crm.media-on.de → crm)
|
||||||
# Derive from APP_DOMAIN if not set (e.g. https://crm.media-on.de → crm)
|
|
||||||
_domain_default = ""
|
_domain_default = ""
|
||||||
if os.environ.get("APP_DOMAIN"):
|
if os.environ.get("APP_DOMAIN"):
|
||||||
try:
|
try:
|
||||||
@@ -69,7 +84,6 @@ if os.environ.get("APP_DOMAIN"):
|
|||||||
except (IndexError, ValueError):
|
except (IndexError, ValueError):
|
||||||
pass
|
pass
|
||||||
APP_NAME = os.environ.get("APP_NAME", _domain_default or "app")
|
APP_NAME = os.environ.get("APP_NAME", _domain_default or "app")
|
||||||
WORKER_NAME = os.environ.get("WORKER_NAME", f"{APP_NAME}-worker")
|
|
||||||
|
|
||||||
# Domain (required)
|
# Domain (required)
|
||||||
APP_DOMAIN = os.environ.get("APP_DOMAIN", "")
|
APP_DOMAIN = os.environ.get("APP_DOMAIN", "")
|
||||||
@@ -87,10 +101,8 @@ DB_PASSWORD = os.environ.get("DB_PASSWORD", "")
|
|||||||
REDIS_PASSWORD = os.environ.get("REDIS_PASSWORD", "")
|
REDIS_PASSWORD = os.environ.get("REDIS_PASSWORD", "")
|
||||||
SECRET_KEY = os.environ.get("SECRET_KEY", "")
|
SECRET_KEY = os.environ.get("SECRET_KEY", "")
|
||||||
|
|
||||||
# Database/Redis host names (defaults match Coolify service names)
|
# Database name (used by deploy_initial for POSTGRES_DB env)
|
||||||
DB_HOST = os.environ.get("DB_HOST", "crm-postgres")
|
|
||||||
DB_NAME = os.environ.get("DB_NAME", "crm_db")
|
DB_NAME = os.environ.get("DB_NAME", "crm_db")
|
||||||
REDIS_HOST = os.environ.get("REDIS_HOST", "crm-redis")
|
|
||||||
|
|
||||||
# Git repo for initial deployment
|
# Git repo for initial deployment
|
||||||
API_GIT_REPO = os.environ.get("API_GIT_REPO", "https://forgejo.media-on.de/Leopoldadmin/leocrm.git")
|
API_GIT_REPO = os.environ.get("API_GIT_REPO", "https://forgejo.media-on.de/Leopoldadmin/leocrm.git")
|
||||||
@@ -165,7 +177,7 @@ class CoolifyClient:
|
|||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
return resp.json()
|
return resp.json()
|
||||||
|
|
||||||
# ── Services (Worker) ──
|
# ── Services ──
|
||||||
|
|
||||||
def list_services(self) -> list[dict[str, Any]]:
|
def list_services(self) -> list[dict[str, Any]]:
|
||||||
"""List all services."""
|
"""List all services."""
|
||||||
@@ -273,114 +285,6 @@ def resolve_app_uuid(client: CoolifyClient) -> str:
|
|||||||
raise ValueError(f"Application '{APP_NAME}' not found in Coolify")
|
raise ValueError(f"Application '{APP_NAME}' not found in Coolify")
|
||||||
|
|
||||||
|
|
||||||
def resolve_worker_uuid(client: CoolifyClient) -> str | None:
|
|
||||||
"""Resolve worker UUID from env var or API lookup by name.
|
|
||||||
Returns None if worker is not configured (non-fatal)."""
|
|
||||||
if WORKER_UUID:
|
|
||||||
return WORKER_UUID
|
|
||||||
|
|
||||||
print(f" COOLIFY_WORKER_UUID not set — looking up '{WORKER_NAME}' via Coolify API...")
|
|
||||||
try:
|
|
||||||
services = client.list_services()
|
|
||||||
for svc in services:
|
|
||||||
name = svc.get("name", "")
|
|
||||||
uuid = svc.get("uuid", "")
|
|
||||||
if name == WORKER_NAME:
|
|
||||||
print(f" Found: {name} → {uuid}")
|
|
||||||
return uuid
|
|
||||||
print(f" Worker '{WORKER_NAME}' not found — worker deploy will be skipped")
|
|
||||||
return None
|
|
||||||
except Exception as e:
|
|
||||||
print(f" Warning: could not list services: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
# ─── Worker Compose Generation ────────────────────────────────────────
|
|
||||||
|
|
||||||
|
|
||||||
def generate_worker_compose(app_uuid: str, worker_uuid: str) -> str:
|
|
||||||
"""Generate worker docker-compose YAML dynamically (no hardcoded UUIDs).
|
|
||||||
|
|
||||||
Uses ${VARIABLE} syntax for secrets — Coolify substitutes from ENV.
|
|
||||||
"""
|
|
||||||
return (
|
|
||||||
"services:\n"
|
|
||||||
" worker:\n"
|
|
||||||
f" image: '{app_uuid}:latest'\n"
|
|
||||||
" restart: unless-stopped\n"
|
|
||||||
" entrypoint:\n"
|
|
||||||
" - /app/worker.sh\n"
|
|
||||||
" environment:\n"
|
|
||||||
f" DATABASE_URL: 'postgresql+asyncpg://crm_worker:${{DB_PASSWORD}}@{DB_HOST}:5432/{DB_NAME}'\n"
|
|
||||||
f" WORKER_DATABASE_URL: 'postgresql+asyncpg://crm_worker:${{DB_PASSWORD}}@{DB_HOST}:5432/{DB_NAME}'\n"
|
|
||||||
f" MIGRATION_DATABASE_URL: 'postgresql+asyncpg://crm_user:${{DB_PASSWORD}}@{DB_HOST}:5432/{DB_NAME}'\n"
|
|
||||||
f" AUTH_DATABASE_URL: 'postgresql+asyncpg://crm_auth:${{DB_PASSWORD}}@{DB_HOST}:5432/{DB_NAME}'\n"
|
|
||||||
f" REDIS_URL: 'redis://default:${{REDIS_PASSWORD}}@{REDIS_HOST}:6379/0'\n"
|
|
||||||
" SECRET_KEY: ${SECRET_KEY}\n"
|
|
||||||
" ENVIRONMENT: ${ENVIRONMENT}\n"
|
|
||||||
" STORAGE_PATH: ${STORAGE_PATH}\n"
|
|
||||||
f" COOLIFY_RESOURCE_UUID: {worker_uuid}\n"
|
|
||||||
f" COOLIFY_CONTAINER_NAME: worker-{worker_uuid}\n"
|
|
||||||
" SERVICE_NAME_WORKER: worker\n"
|
|
||||||
" volumes:\n"
|
|
||||||
f" - '{worker_uuid}_leocrm-worker-storage:/data/storage'\n"
|
|
||||||
" networks:\n"
|
|
||||||
" - coolify\n"
|
|
||||||
f" - {worker_uuid}\n"
|
|
||||||
f" container_name: worker-{worker_uuid}\n"
|
|
||||||
" labels:\n"
|
|
||||||
" - coolify.managed=true\n"
|
|
||||||
" - coolify.version=4.0.0-beta.470\n"
|
|
||||||
" - coolify.type=service\n"
|
|
||||||
f" - coolify.name=worker-{worker_uuid}\n"
|
|
||||||
f" - coolify.resourceName={WORKER_NAME}\n"
|
|
||||||
" - coolify.serviceName=worker\n"
|
|
||||||
" - coolify.service.subType=application\n"
|
|
||||||
" - coolify.service.subName=worker\n"
|
|
||||||
"volumes:\n"
|
|
||||||
" leocrm-worker-storage:\n"
|
|
||||||
" name: leocrm-worker-storage\n"
|
|
||||||
f" {worker_uuid}_leocrm-worker-storage:\n"
|
|
||||||
f" name: {worker_uuid}_leocrm-worker-storage\n"
|
|
||||||
"networks:\n"
|
|
||||||
" coolify:\n"
|
|
||||||
" external: true\n"
|
|
||||||
" name: coolify\n"
|
|
||||||
f" {worker_uuid}:\n"
|
|
||||||
f" name: {worker_uuid}\n"
|
|
||||||
" external: true\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_worker_envs() -> list[dict]:
|
|
||||||
"""Worker ENV variables from environment (no hardcoded secrets)."""
|
|
||||||
return [
|
|
||||||
{"key": "DB_PASSWORD", "value": DB_PASSWORD},
|
|
||||||
{"key": "REDIS_PASSWORD", "value": REDIS_PASSWORD},
|
|
||||||
{"key": "SECRET_KEY", "value": SECRET_KEY},
|
|
||||||
{"key": "ENVIRONMENT", "value": os.environ.get("ENVIRONMENT", "production")},
|
|
||||||
{"key": "STORAGE_PATH", "value": os.environ.get("STORAGE_PATH", "/data/storage")},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def get_api_envs() -> list[dict]:
|
|
||||||
"""API ENV variables from environment (no hardcoded secrets)."""
|
|
||||||
return [
|
|
||||||
{"key": "DATABASE_URL", "value": f"postgresql+asyncpg://crm_api:{DB_PASSWORD}@{DB_HOST}:5432/{DB_NAME}"},
|
|
||||||
{"key": "AUTH_DATABASE_URL", "value": f"postgresql+asyncpg://crm_auth:{DB_PASSWORD}@{DB_HOST}:5432/{DB_NAME}"},
|
|
||||||
{"key": "WORKER_DATABASE_URL", "value": f"postgresql+asyncpg://crm_worker:{DB_PASSWORD}@{DB_HOST}:5432/{DB_NAME}"},
|
|
||||||
{"key": "MIGRATION_DATABASE_URL", "value": f"postgresql+asyncpg://crm_user:{DB_PASSWORD}@{DB_HOST}:5432/{DB_NAME}"},
|
|
||||||
{"key": "REDIS_URL", "value": f"redis://default:{REDIS_PASSWORD}@{REDIS_HOST}:6379/0"},
|
|
||||||
{"key": "SECRET_KEY", "value": SECRET_KEY},
|
|
||||||
{"key": "ENVIRONMENT", "value": os.environ.get("ENVIRONMENT", "production")},
|
|
||||||
{"key": "STORAGE_PATH", "value": os.environ.get("STORAGE_PATH", "/data/storage")},
|
|
||||||
{"key": "FRONTEND_URL", "value": APP_DOMAIN},
|
|
||||||
{"key": "CORS_ORIGINS", "value": APP_DOMAIN},
|
|
||||||
{"key": "SESSION_COOKIE_SECURE", "value": "true"},
|
|
||||||
{"key": "LOG_LEVEL", "value": os.environ.get("LOG_LEVEL", "INFO")},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# ─── SSH Helper (verification only) ────────────────────────────────────
|
# ─── SSH Helper (verification only) ────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@@ -433,93 +337,6 @@ def deploy_api(client: CoolifyClient, app_uuid: str, skip_build: bool = False) -
|
|||||||
return StepResult(False, f"Deploy trigger failed: {e}")
|
return StepResult(False, f"Deploy trigger failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
def deploy_worker(client: CoolifyClient, app_uuid: str, worker_uuid: str, skip_build: bool = False) -> StepResult:
|
|
||||||
"""Deploy the worker service via Coolify API.
|
|
||||||
|
|
||||||
Steps:
|
|
||||||
1. Update service compose (dynamically generated, ${VARIABLE} syntax)
|
|
||||||
2. Set connect_to_docker_network=True (coolify network for Redis/Postgres)
|
|
||||||
3. Set ENV variables via Coolify API (secrets from environment)
|
|
||||||
4. Tag latest API image as :latest (Coolify uses commit-hash tags)
|
|
||||||
5. Deploy via POST /deploy (creates new container)
|
|
||||||
6. Wait for healthy
|
|
||||||
"""
|
|
||||||
print(" Deploying worker service via Coolify API...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Step 1: Update service compose with dynamic UUIDs and ${VARIABLE} syntax
|
|
||||||
compose_yaml = generate_worker_compose(app_uuid, worker_uuid)
|
|
||||||
print(" Updating worker service compose...")
|
|
||||||
client.update_service(worker_uuid, compose_yaml)
|
|
||||||
time.sleep(2)
|
|
||||||
|
|
||||||
# Step 2: Set connect_to_docker_network=True
|
|
||||||
print(" Ensuring coolify network connection...")
|
|
||||||
resp = httpx.patch(
|
|
||||||
f"{client.base_url}/api/v1/services/{worker_uuid}",
|
|
||||||
headers=client.headers,
|
|
||||||
json={"connect_to_docker_network": True},
|
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
if resp.status_code != 200:
|
|
||||||
print(f" Warning: could not set connect_to_docker_network ({resp.status_code})")
|
|
||||||
|
|
||||||
# Step 3: Set ENV variables via Coolify API (Coolify auto-generates .env)
|
|
||||||
print(" Setting ENV variables via Coolify API...")
|
|
||||||
for env in get_worker_envs():
|
|
||||||
resp = httpx.post(
|
|
||||||
f"{client.base_url}/api/v1/services/{worker_uuid}/envs",
|
|
||||||
headers=client.headers,
|
|
||||||
json=env,
|
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
if resp.status_code == 409:
|
|
||||||
resp = httpx.patch(
|
|
||||||
f"{client.base_url}/api/v1/services/{worker_uuid}/envs",
|
|
||||||
headers=client.headers,
|
|
||||||
json=env,
|
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
if resp.status_code not in (200, 201):
|
|
||||||
print(f" Warning: could not set ENV {env['key']} ({resp.status_code})")
|
|
||||||
|
|
||||||
# Step 4: Tag the latest API image as :latest
|
|
||||||
print(" Tagging latest API image as :latest...")
|
|
||||||
tag_code, tag_output = ssh_run(
|
|
||||||
f'docker images --format "{{{{.Repository}}}}:{{{{.Tag}}}}" | '
|
|
||||||
f'grep "^{app_uuid}:" | grep -v latest | head -1 | '
|
|
||||||
f'xargs -I{{}} docker tag {{}} {app_uuid}:latest'
|
|
||||||
)
|
|
||||||
if tag_code != 0:
|
|
||||||
print(f" Warning: could not tag :latest ({tag_output.strip()})")
|
|
||||||
|
|
||||||
# Step 5: Deploy via POST /deploy
|
|
||||||
print(" Deploying worker service...")
|
|
||||||
result = client.deploy_application(worker_uuid)
|
|
||||||
deploy_uuid = _extract_deploy_uuid(result)
|
|
||||||
if deploy_uuid:
|
|
||||||
print(f" Worker deploy queued: {deploy_uuid[:12]}")
|
|
||||||
dep_result = _wait_deployment(client, deploy_uuid, timeout=120)
|
|
||||||
if not dep_result.success:
|
|
||||||
return dep_result
|
|
||||||
else:
|
|
||||||
print(" No deployment UUID returned, waiting for healthy...")
|
|
||||||
|
|
||||||
# Step 6: Wait for healthy
|
|
||||||
return _wait_service_healthy(client, worker_uuid, timeout=120)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return StepResult(False, f"Worker deploy failed: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
def _extract_deploy_uuid(result: dict[str, Any]) -> str | None:
|
|
||||||
"""Extract deployment UUID from Coolify deploy response."""
|
|
||||||
deployments = result.get("deployments", [])
|
|
||||||
if deployments and isinstance(deployments, list):
|
|
||||||
return deployments[0].get("deployment_uuid")
|
|
||||||
return result.get("deployment_uuid")
|
|
||||||
|
|
||||||
|
|
||||||
def _wait_deployment(client: CoolifyClient, deploy_uuid: str, timeout: int = 300) -> StepResult:
|
def _wait_deployment(client: CoolifyClient, deploy_uuid: str, timeout: int = 300) -> StepResult:
|
||||||
"""Wait for a Coolify deployment to reach success/failed status."""
|
"""Wait for a Coolify deployment to reach success/failed status."""
|
||||||
print(f" Waiting for deployment {deploy_uuid[:12]}...")
|
print(f" Waiting for deployment {deploy_uuid[:12]}...")
|
||||||
@@ -540,26 +357,6 @@ def _wait_deployment(client: CoolifyClient, deploy_uuid: str, timeout: int = 300
|
|||||||
return StepResult(False, f"Deployment timed out after {timeout}s", time.time() - start)
|
return StepResult(False, f"Deployment timed out after {timeout}s", time.time() - start)
|
||||||
|
|
||||||
|
|
||||||
def _wait_service_healthy(client: CoolifyClient, service_uuid: str, timeout: int = 120) -> StepResult:
|
|
||||||
"""Wait for a Coolify service to reach running:healthy status."""
|
|
||||||
print(f" Waiting for service {service_uuid[:12]} to become healthy...")
|
|
||||||
start = time.time()
|
|
||||||
while time.time() - start < timeout:
|
|
||||||
try:
|
|
||||||
svc = client.get_service(service_uuid)
|
|
||||||
status = svc.get("status", "unknown")
|
|
||||||
elapsed = int(time.time() - start)
|
|
||||||
print(f" [{elapsed}s] Service status: {status}")
|
|
||||||
if status == "running:healthy" or status == "healthy":
|
|
||||||
return StepResult(True, f"Service healthy: {status}", time.time() - start, svc)
|
|
||||||
if "failed" in status.lower() or "error" in status.lower():
|
|
||||||
return StepResult(False, f"Service failed: {status}", time.time() - start, svc)
|
|
||||||
except Exception as e:
|
|
||||||
print(f" Warning: API error: {e}")
|
|
||||||
time.sleep(5)
|
|
||||||
return StepResult(False, f"Service did not become healthy in {timeout}s", time.time() - start)
|
|
||||||
|
|
||||||
|
|
||||||
# ─── Verification ──────────────────────────────────────────────────────
|
# ─── Verification ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@@ -665,21 +462,7 @@ def verify_rls() -> StepResult:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def verify_worker_service(client: CoolifyClient, worker_uuid: str) -> StepResult:
|
def run_verification(client: CoolifyClient) -> list[tuple[str, StepResult]]:
|
||||||
"""Verify worker service is running via Coolify API."""
|
|
||||||
print(" Verifying worker service via Coolify API...")
|
|
||||||
try:
|
|
||||||
svc = client.get_service(worker_uuid)
|
|
||||||
status = svc.get("status", "unknown")
|
|
||||||
status_lower = status.lower()
|
|
||||||
if "running" in status_lower or "healthy" in status_lower or status_lower == "up":
|
|
||||||
return StepResult(True, f"Worker service: {status}", details={"status": status})
|
|
||||||
return StepResult(False, f"Worker service not running: {status}", details={"status": status})
|
|
||||||
except Exception as e:
|
|
||||||
return StepResult(False, f"Worker service check failed: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
def run_verification(client: CoolifyClient, worker_uuid: str | None = None) -> list[tuple[str, StepResult]]:
|
|
||||||
"""Run all verification checks and return results."""
|
"""Run all verification checks and return results."""
|
||||||
results: list[tuple[str, StepResult]] = []
|
results: list[tuple[str, StepResult]] = []
|
||||||
|
|
||||||
@@ -703,12 +486,6 @@ def run_verification(client: CoolifyClient, worker_uuid: str | None = None) -> l
|
|||||||
results.append(("RLS tables", r))
|
results.append(("RLS tables", r))
|
||||||
_print_result(r)
|
_print_result(r)
|
||||||
|
|
||||||
if worker_uuid:
|
|
||||||
print("\n[Verify] Worker service status...")
|
|
||||||
r = verify_worker_service(client, worker_uuid)
|
|
||||||
results.append(("Worker service", r))
|
|
||||||
_print_result(r)
|
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
@@ -751,29 +528,14 @@ def validate_config() -> list[str]:
|
|||||||
# ─── Main Deploy Pipeline ──────────────────────────────────────────────
|
# ─── Main Deploy Pipeline ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
def seed_admin_user(app_uuid: str) -> StepResult:
|
|
||||||
"""Seed admin user after fresh deployment via SSH into the app container."""
|
|
||||||
admin_email = os.environ.get("ADMIN_EMAIL", "admin@media-on.de")
|
|
||||||
admin_password = os.environ.get("ADMIN_PASSWORD", "Admin123!")
|
|
||||||
if not admin_password:
|
|
||||||
return StepResult(True, "Skipped — ADMIN_PASSWORD not set")
|
|
||||||
print(f"\n[Seed] Seeding admin user ({admin_email})...")
|
|
||||||
cmd = f"docker ps --filter name={app_uuid} --format '{{{{.Names}}}}' | grep crm-app | head -1"
|
|
||||||
rc, container = ssh_run(cmd, timeout=30)
|
|
||||||
if rc != 0 or not container.strip():
|
|
||||||
return StepResult(False, f"Could not find crm-app container for {app_uuid}")
|
|
||||||
container = container.strip()
|
|
||||||
seed_cmd = f"docker exec {container} python3 /app/scripts/seed_admin.py"
|
|
||||||
rc, out = ssh_run(seed_cmd, timeout=60)
|
|
||||||
if rc != 0:
|
|
||||||
return StepResult(False, f"Admin seed failed: {out.strip()}")
|
|
||||||
return StepResult(True, f"Admin user seeded: {admin_email}")
|
|
||||||
|
|
||||||
|
|
||||||
def deploy_full(skip_build: bool = False) -> int:
|
def deploy_full(skip_build: bool = False) -> int:
|
||||||
"""Full deploy: API + Worker + Verification."""
|
"""Redeploy the application via Coolify API (/api/v1/deploy).
|
||||||
|
|
||||||
|
This is the standard redeploy workflow for an existing docker-compose stack.
|
||||||
|
For initial creation, use deploy_initial() via --initial.
|
||||||
|
"""
|
||||||
print(f"\n{'='*60}")
|
print(f"\n{'='*60}")
|
||||||
print(" LeoCRM Full Deploy")
|
print(" LeoCRM Deploy")
|
||||||
print(f"{'='*60}\n")
|
print(f"{'='*60}\n")
|
||||||
|
|
||||||
errors = validate_config()
|
errors = validate_config()
|
||||||
@@ -785,8 +547,8 @@ def deploy_full(skip_build: bool = False) -> int:
|
|||||||
client = CoolifyClient(COOLIFY_BASE_URL, COOLIFY_TOKEN)
|
client = CoolifyClient(COOLIFY_BASE_URL, COOLIFY_TOKEN)
|
||||||
steps: list[tuple[str, StepResult]] = []
|
steps: list[tuple[str, StepResult]] = []
|
||||||
|
|
||||||
# Resolve UUIDs
|
# Step 1: Resolve UUID
|
||||||
print("\n[0/3] Resolving Coolify resources...")
|
print("\n[1/2] Resolving Coolify application...")
|
||||||
try:
|
try:
|
||||||
app_uuid = resolve_app_uuid(client)
|
app_uuid = resolve_app_uuid(client)
|
||||||
print(f" App UUID: {app_uuid}")
|
print(f" App UUID: {app_uuid}")
|
||||||
@@ -794,12 +556,8 @@ def deploy_full(skip_build: bool = False) -> int:
|
|||||||
print(f"ERROR: {e}")
|
print(f"ERROR: {e}")
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
worker_uuid = resolve_worker_uuid(client)
|
# Step 2: Deploy via Coolify API
|
||||||
if worker_uuid:
|
print("\n[2/2] Deploying via Coolify API...")
|
||||||
print(f" Worker UUID: {worker_uuid}")
|
|
||||||
|
|
||||||
# Step 1: Deploy API
|
|
||||||
print("\n[1/3] Deploying API via Coolify API...")
|
|
||||||
r = deploy_api(client, app_uuid, skip_build=skip_build)
|
r = deploy_api(client, app_uuid, skip_build=skip_build)
|
||||||
steps.append(("API deploy", r))
|
steps.append(("API deploy", r))
|
||||||
_print_result(r)
|
_print_result(r)
|
||||||
@@ -807,71 +565,9 @@ def deploy_full(skip_build: bool = False) -> int:
|
|||||||
print_summary(steps)
|
print_summary(steps)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
# Step 2: Deploy Worker
|
# Step 3: Verification
|
||||||
if worker_uuid:
|
print("\n[3/3] Running verification...")
|
||||||
print("\n[2/3] Deploying Worker via Coolify API...")
|
verify_results = run_verification(client)
|
||||||
r = deploy_worker(client, app_uuid, worker_uuid, skip_build=skip_build)
|
|
||||||
steps.append(("Worker deploy", r))
|
|
||||||
_print_result(r)
|
|
||||||
else:
|
|
||||||
print("\n[2/3] Worker deploy skipped (no worker UUID resolved)")
|
|
||||||
steps.append(("Worker deploy", StepResult(True, "Skipped — no worker configured")))
|
|
||||||
|
|
||||||
# Step 3: Seed admin user (if ADMIN_PASSWORD set)
|
|
||||||
print("\n[3/4] Seeding admin user...")
|
|
||||||
r = seed_admin_user(app_uuid)
|
|
||||||
steps.append(("Admin seed", r))
|
|
||||||
_print_result(r)
|
|
||||||
|
|
||||||
# Step 4: Verification
|
|
||||||
print("\n[4/4] Running verification...")
|
|
||||||
verify_results = run_verification(client, worker_uuid=worker_uuid)
|
|
||||||
steps.extend(verify_results)
|
|
||||||
|
|
||||||
all_ok = print_summary(steps)
|
|
||||||
return 0 if all_ok else 1
|
|
||||||
|
|
||||||
|
|
||||||
def deploy_worker_only(skip_build: bool = False) -> int:
|
|
||||||
"""Deploy only the worker service + verification."""
|
|
||||||
print(f"\n{'='*60}")
|
|
||||||
print(" LeoCRM Worker-Only Deploy")
|
|
||||||
print(f"{'='*60}\n")
|
|
||||||
|
|
||||||
errors = validate_config()
|
|
||||||
if errors:
|
|
||||||
for e in errors:
|
|
||||||
print(f"ERROR: {e}")
|
|
||||||
return 2
|
|
||||||
|
|
||||||
client = CoolifyClient(COOLIFY_BASE_URL, COOLIFY_TOKEN)
|
|
||||||
steps: list[tuple[str, StepResult]] = []
|
|
||||||
|
|
||||||
# Resolve UUIDs
|
|
||||||
print("\n[0/2] Resolving Coolify resources...")
|
|
||||||
try:
|
|
||||||
app_uuid = resolve_app_uuid(client)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"ERROR: {e}")
|
|
||||||
return 2
|
|
||||||
|
|
||||||
worker_uuid = resolve_worker_uuid(client)
|
|
||||||
if not worker_uuid:
|
|
||||||
print("ERROR: No worker UUID resolved (COOLIFY_WORKER_UUID not set and API lookup failed)")
|
|
||||||
return 2
|
|
||||||
|
|
||||||
# Step 1: Deploy Worker
|
|
||||||
print("\n[1/2] Deploying Worker via Coolify API...")
|
|
||||||
r = deploy_worker(client, app_uuid, worker_uuid, skip_build=skip_build)
|
|
||||||
steps.append(("Worker deploy", r))
|
|
||||||
_print_result(r)
|
|
||||||
if not r.success:
|
|
||||||
print_summary(steps)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
# Step 2: Verification
|
|
||||||
print("\n[2/2] Running verification...")
|
|
||||||
verify_results = run_verification(client, worker_uuid=worker_uuid)
|
|
||||||
steps.extend(verify_results)
|
steps.extend(verify_results)
|
||||||
|
|
||||||
all_ok = print_summary(steps)
|
all_ok = print_summary(steps)
|
||||||
@@ -891,182 +587,16 @@ def verify_only() -> int:
|
|||||||
return 2
|
return 2
|
||||||
|
|
||||||
client = CoolifyClient(COOLIFY_BASE_URL, COOLIFY_TOKEN)
|
client = CoolifyClient(COOLIFY_BASE_URL, COOLIFY_TOKEN)
|
||||||
worker_uuid = resolve_worker_uuid(client)
|
results = run_verification(client)
|
||||||
results = run_verification(client, worker_uuid=worker_uuid)
|
|
||||||
all_ok = print_summary(results)
|
all_ok = print_summary(results)
|
||||||
return 0 if all_ok else 1
|
return 0 if all_ok else 1
|
||||||
|
|
||||||
|
|
||||||
# ─── Initial Deployment (create all Coolify resources from scratch) ──────
|
# ─── Initial Deployment (create Coolify application from scratch) ──────
|
||||||
|
|
||||||
# PostgreSQL Compose (pgvector for embeddings)
|
|
||||||
POSTGRES_COMPOSE = (
|
|
||||||
"services:\n"
|
|
||||||
" postgres:\n"
|
|
||||||
" image: pgvector/pgvector:pg16\n"
|
|
||||||
" container_name: crm-postgres\n"
|
|
||||||
" restart: unless-stopped\n"
|
|
||||||
" environment:\n"
|
|
||||||
" POSTGRES_USER: ${DB_USER}\n"
|
|
||||||
" POSTGRES_PASSWORD: ${DB_PASSWORD}\n"
|
|
||||||
" POSTGRES_DB: ${DB_NAME}\n"
|
|
||||||
" PGDATA: /var/lib/postgresql/data/pgdata\n"
|
|
||||||
" volumes:\n"
|
|
||||||
" - pgdata:/var/lib/postgresql/data\n"
|
|
||||||
" healthcheck:\n"
|
|
||||||
" test: ['CMD-SHELL', 'pg_isready -U ${DB_USER} -d ${DB_NAME}']\n"
|
|
||||||
" interval: 10s\n"
|
|
||||||
" timeout: 5s\n"
|
|
||||||
" retries: 5\n"
|
|
||||||
" start_period: 10s\n"
|
|
||||||
"volumes:\n"
|
|
||||||
" pgdata:\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Redis Compose
|
|
||||||
REDIS_COMPOSE = (
|
|
||||||
"services:\n"
|
|
||||||
" redis:\n"
|
|
||||||
" image: redis:7-alpine\n"
|
|
||||||
" container_name: crm-redis\n"
|
|
||||||
" restart: unless-stopped\n"
|
|
||||||
" command: redis-server --requirepass ${REDIS_PASSWORD}\n"
|
|
||||||
" volumes:\n"
|
|
||||||
" - redisdata:/data\n"
|
|
||||||
" healthcheck:\n"
|
|
||||||
" test: ['CMD-SHELL', 'redis-cli ping || exit 1']\n"
|
|
||||||
" interval: 10s\n"
|
|
||||||
" timeout: 5s\n"
|
|
||||||
" retries: 5\n"
|
|
||||||
"volumes:\n"
|
|
||||||
" redisdata:\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_postgres_envs() -> list[dict]:
|
|
||||||
"""PostgreSQL ENV variables from environment."""
|
|
||||||
return [
|
|
||||||
{"key": "DB_USER", "value": os.environ.get("DB_USER", "crm_user")},
|
|
||||||
{"key": "DB_PASSWORD", "value": DB_PASSWORD},
|
|
||||||
{"key": "DB_NAME", "value": DB_NAME},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def get_redis_envs() -> list[dict]:
|
|
||||||
"""Redis ENV variables from environment."""
|
|
||||||
return [
|
|
||||||
{"key": "REDIS_PASSWORD", "value": REDIS_PASSWORD},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def create_service(client: CoolifyClient, project_uuid: str, environment_name: str,
|
|
||||||
server_uuid: str, compose_raw: str, name: str) -> str | None:
|
|
||||||
"""Create a Coolify service from a docker-compose definition.
|
|
||||||
Returns the service UUID or None on failure."""
|
|
||||||
encoded = base64.b64encode(compose_raw.encode()).decode()
|
|
||||||
try:
|
|
||||||
resp = httpx.post(
|
|
||||||
f"{client.base_url}/api/v1/services",
|
|
||||||
headers=client.headers,
|
|
||||||
json={
|
|
||||||
"project_uuid": project_uuid,
|
|
||||||
"environment_name": environment_name,
|
|
||||||
"server_uuid": server_uuid,
|
|
||||||
"docker_compose_raw": encoded,
|
|
||||||
"name": name,
|
|
||||||
},
|
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
if resp.status_code in (200, 201):
|
|
||||||
data = resp.json()
|
|
||||||
return data.get("uuid")
|
|
||||||
print(f" Error creating service {name}: {resp.status_code} {resp.text[:200]}")
|
|
||||||
return None
|
|
||||||
except Exception as e:
|
|
||||||
print(f" Error creating service {name}: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def set_service_envs(client: CoolifyClient, service_uuid: str, envs: list[dict]) -> None:
|
|
||||||
"""Set ENV variables for a service via Coolify API."""
|
|
||||||
for env in envs:
|
|
||||||
resp = httpx.post(
|
|
||||||
f"{client.base_url}/api/v1/services/{service_uuid}/envs",
|
|
||||||
headers=client.headers,
|
|
||||||
json=env,
|
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
if resp.status_code == 409:
|
|
||||||
resp = httpx.patch(
|
|
||||||
f"{client.base_url}/api/v1/services/{service_uuid}/envs",
|
|
||||||
headers=client.headers,
|
|
||||||
json=env,
|
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
if resp.status_code not in (200, 201):
|
|
||||||
print(f" Warning: could not set ENV {env['key']} ({resp.status_code})")
|
|
||||||
|
|
||||||
|
|
||||||
def set_application_envs(client: CoolifyClient, app_uuid: str, envs: list[dict]) -> None:
|
|
||||||
"""Set ENV variables for an application via Coolify API."""
|
|
||||||
for env in envs:
|
|
||||||
resp = httpx.post(
|
|
||||||
f"{client.base_url}/api/v1/applications/{app_uuid}/envs",
|
|
||||||
headers=client.headers,
|
|
||||||
json=env,
|
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
if resp.status_code == 409:
|
|
||||||
resp = httpx.patch(
|
|
||||||
f"{client.base_url}/api/v1/applications/{app_uuid}/envs",
|
|
||||||
headers=client.headers,
|
|
||||||
json=env,
|
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
if resp.status_code not in (200, 201):
|
|
||||||
print(f" Warning: could not set ENV {env['key']} ({resp.status_code})")
|
|
||||||
|
|
||||||
|
|
||||||
def create_api_application(client: CoolifyClient, project_uuid: str,
|
|
||||||
environment_name: str, server_uuid: str) -> str | None:
|
|
||||||
"""Create the API application from a Git repository via private deploy key.
|
|
||||||
|
|
||||||
Returns the application UUID or None on failure.
|
|
||||||
"""
|
|
||||||
private_key_uuid = os.environ.get("COOLIFY_PRIVATE_KEY_UUID", "")
|
|
||||||
if not private_key_uuid:
|
|
||||||
print(" Error: COOLIFY_PRIVATE_KEY_UUID not set")
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
resp = httpx.post(
|
|
||||||
f"{client.base_url}/api/v1/applications/private-deploy-key",
|
|
||||||
headers=client.headers,
|
|
||||||
json={
|
|
||||||
"project_uuid": project_uuid,
|
|
||||||
"environment_name": environment_name,
|
|
||||||
"server_uuid": server_uuid,
|
|
||||||
"private_key_uuid": private_key_uuid,
|
|
||||||
"git_repository": API_GIT_REPO,
|
|
||||||
"git_branch": API_GIT_BRANCH,
|
|
||||||
"build_pack": "dockerfile",
|
|
||||||
"name": APP_NAME,
|
|
||||||
"ports_exposes": "8000",
|
|
||||||
},
|
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
if resp.status_code in (200, 201):
|
|
||||||
data = resp.json()
|
|
||||||
return data.get("uuid")
|
|
||||||
print(f" Error creating API application: {resp.status_code} {resp.text[:300]}")
|
|
||||||
return None
|
|
||||||
except Exception as e:
|
|
||||||
print(f" Error creating API application: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def deploy_initial() -> int:
|
def deploy_initial() -> int:
|
||||||
"""Initial deployment: create all Coolify resources from scratch.
|
"""Initial deployment: create the Coolify application from scratch.
|
||||||
|
|
||||||
═══════════════════════════════════════════════════════════════════════
|
═══════════════════════════════════════════════════════════════════════
|
||||||
⚠️ KI / AGENT HINWEISE — BITTE VOR ÄNDERUNGEN LESEN ⚠️
|
⚠️ KI / AGENT HINWEISE — BITTE VOR ÄNDERUNGEN LESEN ⚠️
|
||||||
@@ -1149,7 +679,7 @@ def deploy_initial() -> int:
|
|||||||
environment_name = os.environ.get("COOLIFY_ENVIRONMENT", "production")
|
environment_name = os.environ.get("COOLIFY_ENVIRONMENT", "production")
|
||||||
server_uuid = os.environ.get("COOLIFY_SERVER_UUID", "")
|
server_uuid = os.environ.get("COOLIFY_SERVER_UUID", "")
|
||||||
|
|
||||||
# ─── Single docker-compose application (like the original working app) ──
|
# ─── Single docker-compose application ──────────────────────────────
|
||||||
# All 4 containers (postgres, redis, crm-app, crm-worker) in one stack.
|
# All 4 containers (postgres, redis, crm-app, crm-worker) in one stack.
|
||||||
# Coolify reads docker-compose.yaml from the Git repo, builds images, all
|
# Coolify reads docker-compose.yaml from the Git repo, builds images, all
|
||||||
# containers share one network. Service names work as DNS names. No manual
|
# containers share one network. Service names work as DNS names. No manual
|
||||||
@@ -1315,7 +845,7 @@ def deploy_initial() -> int:
|
|||||||
|
|
||||||
# Verification
|
# Verification
|
||||||
print("\n[5/5] Running verification...")
|
print("\n[5/5] Running verification...")
|
||||||
verify_results = run_verification(client, worker_uuid=None)
|
verify_results = run_verification(client)
|
||||||
steps.extend(verify_results)
|
steps.extend(verify_results)
|
||||||
|
|
||||||
all_ok = print_summary(steps)
|
all_ok = print_summary(steps)
|
||||||
@@ -1331,9 +861,8 @@ def main() -> None:
|
|||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
epilog="""
|
epilog="""
|
||||||
Examples:
|
Examples:
|
||||||
python scripts/deploy.py # Full deploy (API + Worker)
|
python scripts/deploy.py # Redeploy via Coolify API
|
||||||
python scripts/deploy.py --skip-build # Skip build, just restart
|
python scripts/deploy.py --skip-build # Skip build, just restart
|
||||||
python scripts/deploy.py --worker-only # Only deploy worker
|
|
||||||
python scripts/deploy.py --verify-only # Only run verification
|
python scripts/deploy.py --verify-only # Only run verification
|
||||||
python scripts/deploy.py --initial # Create all resources from scratch
|
python scripts/deploy.py --initial # Create all resources from scratch
|
||||||
""",
|
""",
|
||||||
@@ -1342,10 +871,6 @@ Examples:
|
|||||||
"--skip-build", action="store_true",
|
"--skip-build", action="store_true",
|
||||||
help="Skip build, just restart services via Coolify API",
|
help="Skip build, just restart services via Coolify API",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
|
||||||
"--worker-only", action="store_true",
|
|
||||||
help="Only deploy the worker service",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--initial", action="store_true",
|
"--initial", action="store_true",
|
||||||
help="Initial deployment: create all Coolify resources from scratch",
|
help="Initial deployment: create all Coolify resources from scratch",
|
||||||
@@ -1360,8 +885,6 @@ Examples:
|
|||||||
sys.exit(deploy_initial())
|
sys.exit(deploy_initial())
|
||||||
elif args.verify_only:
|
elif args.verify_only:
|
||||||
sys.exit(verify_only())
|
sys.exit(verify_only())
|
||||||
elif args.worker_only:
|
|
||||||
sys.exit(deploy_worker_only(skip_build=args.skip_build))
|
|
||||||
else:
|
else:
|
||||||
sys.exit(deploy_full(skip_build=args.skip_build))
|
sys.exit(deploy_full(skip_build=args.skip_build))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user