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:
+56
-533
@@ -1,37 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
"""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*
|
||||
for post-deploy verification (Alembic version, RLS table count) because
|
||||
the Coolify API does not expose database internals.
|
||||
═══════════════════════════════════════════════════════════════════════
|
||||
CURRENT WORKFLOW — Single docker-compose Stack
|
||||
═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
No UUIDs, domains, or secrets are hardcoded. Everything comes from
|
||||
environment variables or is resolved via the Coolify API.
|
||||
All 4 containers (postgres, redis, crm_app, crm_worker) run in a single
|
||||
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:
|
||||
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 --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 --verify-only # Only run verification
|
||||
|
||||
Environment variables:
|
||||
COOLIFY_API_TOKEN — Coolify API token (required)
|
||||
COOLIFY_BASE_URL — Coolify base URL (default: https://server.media-on.de)
|
||||
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: leocrm-api)
|
||||
WORKER_NAME — Worker name for API lookup (default: leocrm-worker)
|
||||
APP_NAME — Application name for API lookup (default: derived from APP_DOMAIN)
|
||||
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)
|
||||
SERVER_IP — Server IP for SSH verification (default: 46.225.91.159)
|
||||
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)
|
||||
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
|
||||
REDIS_PASSWORD — Redis password
|
||||
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:
|
||||
0 — deployment successful
|
||||
1 — deployment failed
|
||||
@@ -56,12 +73,10 @@ import httpx
|
||||
COOLIFY_BASE_URL = os.environ.get("COOLIFY_BASE_URL", "https://server.media-on.de")
|
||||
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", "")
|
||||
WORKER_UUID = os.environ.get("COOLIFY_WORKER_UUID", "")
|
||||
|
||||
# Names for API lookup when UUIDs are not provided
|
||||
# Derive from APP_DOMAIN if not set (e.g. https://crm.media-on.de → crm)
|
||||
# Derive APP_NAME from APP_DOMAIN if not set (e.g. https://crm.media-on.de → crm)
|
||||
_domain_default = ""
|
||||
if os.environ.get("APP_DOMAIN"):
|
||||
try:
|
||||
@@ -69,7 +84,6 @@ if os.environ.get("APP_DOMAIN"):
|
||||
except (IndexError, ValueError):
|
||||
pass
|
||||
APP_NAME = os.environ.get("APP_NAME", _domain_default or "app")
|
||||
WORKER_NAME = os.environ.get("WORKER_NAME", f"{APP_NAME}-worker")
|
||||
|
||||
# Domain (required)
|
||||
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", "")
|
||||
SECRET_KEY = os.environ.get("SECRET_KEY", "")
|
||||
|
||||
# Database/Redis host names (defaults match Coolify service names)
|
||||
DB_HOST = os.environ.get("DB_HOST", "crm-postgres")
|
||||
# Database name (used by deploy_initial for POSTGRES_DB env)
|
||||
DB_NAME = os.environ.get("DB_NAME", "crm_db")
|
||||
REDIS_HOST = os.environ.get("REDIS_HOST", "crm-redis")
|
||||
|
||||
# Git repo for initial deployment
|
||||
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()
|
||||
return resp.json()
|
||||
|
||||
# ── Services (Worker) ──
|
||||
# ── Services ──
|
||||
|
||||
def list_services(self) -> list[dict[str, Any]]:
|
||||
"""List all services."""
|
||||
@@ -273,114 +285,6 @@ def resolve_app_uuid(client: CoolifyClient) -> str:
|
||||
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) ────────────────────────────────────
|
||||
|
||||
|
||||
@@ -433,93 +337,6 @@ def deploy_api(client: CoolifyClient, app_uuid: str, skip_build: bool = False) -
|
||||
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:
|
||||
"""Wait for a Coolify deployment to reach success/failed status."""
|
||||
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)
|
||||
|
||||
|
||||
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 ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -665,21 +462,7 @@ def verify_rls() -> StepResult:
|
||||
)
|
||||
|
||||
|
||||
def verify_worker_service(client: CoolifyClient, worker_uuid: 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]]:
|
||||
def run_verification(client: CoolifyClient) -> list[tuple[str, StepResult]]:
|
||||
"""Run all verification checks and return results."""
|
||||
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))
|
||||
_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
|
||||
|
||||
|
||||
@@ -751,29 +528,14 @@ def validate_config() -> list[str]:
|
||||
# ─── 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:
|
||||
"""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(" LeoCRM Full Deploy")
|
||||
print(" LeoCRM Deploy")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
errors = validate_config()
|
||||
@@ -785,8 +547,8 @@ def deploy_full(skip_build: bool = False) -> int:
|
||||
client = CoolifyClient(COOLIFY_BASE_URL, COOLIFY_TOKEN)
|
||||
steps: list[tuple[str, StepResult]] = []
|
||||
|
||||
# Resolve UUIDs
|
||||
print("\n[0/3] Resolving Coolify resources...")
|
||||
# Step 1: Resolve UUID
|
||||
print("\n[1/2] Resolving Coolify application...")
|
||||
try:
|
||||
app_uuid = resolve_app_uuid(client)
|
||||
print(f" App UUID: {app_uuid}")
|
||||
@@ -794,12 +556,8 @@ def deploy_full(skip_build: bool = False) -> int:
|
||||
print(f"ERROR: {e}")
|
||||
return 2
|
||||
|
||||
worker_uuid = resolve_worker_uuid(client)
|
||||
if worker_uuid:
|
||||
print(f" Worker UUID: {worker_uuid}")
|
||||
|
||||
# Step 1: Deploy API
|
||||
print("\n[1/3] Deploying API via Coolify API...")
|
||||
# Step 2: Deploy via Coolify API
|
||||
print("\n[2/2] Deploying via Coolify API...")
|
||||
r = deploy_api(client, app_uuid, skip_build=skip_build)
|
||||
steps.append(("API deploy", r))
|
||||
_print_result(r)
|
||||
@@ -807,71 +565,9 @@ def deploy_full(skip_build: bool = False) -> int:
|
||||
print_summary(steps)
|
||||
return 1
|
||||
|
||||
# Step 2: Deploy Worker
|
||||
if worker_uuid:
|
||||
print("\n[2/3] 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)
|
||||
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)
|
||||
# Step 3: Verification
|
||||
print("\n[3/3] Running verification...")
|
||||
verify_results = run_verification(client)
|
||||
steps.extend(verify_results)
|
||||
|
||||
all_ok = print_summary(steps)
|
||||
@@ -891,182 +587,16 @@ def verify_only() -> int:
|
||||
return 2
|
||||
|
||||
client = CoolifyClient(COOLIFY_BASE_URL, COOLIFY_TOKEN)
|
||||
worker_uuid = resolve_worker_uuid(client)
|
||||
results = run_verification(client, worker_uuid=worker_uuid)
|
||||
results = run_verification(client)
|
||||
all_ok = print_summary(results)
|
||||
return 0 if all_ok else 1
|
||||
|
||||
|
||||
# ─── Initial Deployment (create all Coolify resources 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
|
||||
# ─── Initial Deployment (create Coolify application from scratch) ──────
|
||||
|
||||
|
||||
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 ⚠️
|
||||
@@ -1149,7 +679,7 @@ def deploy_initial() -> int:
|
||||
environment_name = os.environ.get("COOLIFY_ENVIRONMENT", "production")
|
||||
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.
|
||||
# Coolify reads docker-compose.yaml from the Git repo, builds images, all
|
||||
# containers share one network. Service names work as DNS names. No manual
|
||||
@@ -1315,7 +845,7 @@ def deploy_initial() -> int:
|
||||
|
||||
# Verification
|
||||
print("\n[5/5] Running verification...")
|
||||
verify_results = run_verification(client, worker_uuid=None)
|
||||
verify_results = run_verification(client)
|
||||
steps.extend(verify_results)
|
||||
|
||||
all_ok = print_summary(steps)
|
||||
@@ -1331,9 +861,8 @@ def main() -> None:
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
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 --worker-only # Only deploy worker
|
||||
python scripts/deploy.py --verify-only # Only run verification
|
||||
python scripts/deploy.py --initial # Create all resources from scratch
|
||||
""",
|
||||
@@ -1342,10 +871,6 @@ Examples:
|
||||
"--skip-build", action="store_true",
|
||||
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(
|
||||
"--initial", action="store_true",
|
||||
help="Initial deployment: create all Coolify resources from scratch",
|
||||
@@ -1360,8 +885,6 @@ Examples:
|
||||
sys.exit(deploy_initial())
|
||||
elif args.verify_only:
|
||||
sys.exit(verify_only())
|
||||
elif args.worker_only:
|
||||
sys.exit(deploy_worker_only(skip_build=args.skip_build))
|
||||
else:
|
||||
sys.exit(deploy_full(skip_build=args.skip_build))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user