From f80c96be55296d0f6184a9fdff8fbe0409a23a46 Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Sun, 22 Feb 2026 17:06:36 +0100
Subject: [PATCH] fix: rebuild/recreate — use ops CLI instead of Coolify API
---
app/routers/rebuild.py | 176 +++++++++++++++++++++-------------------------------------
1 files changed, 63 insertions(+), 113 deletions(-)
diff --git a/app/routers/rebuild.py b/app/routers/rebuild.py
index d4873a4..447979f 100644
--- a/app/routers/rebuild.py
+++ b/app/routers/rebuild.py
@@ -19,6 +19,7 @@
from app.auth import verify_token
from app.ops_runner import (
+ OPS_CLI,
_BACKUP_TIMEOUT,
run_command,
run_command_host,
@@ -298,68 +299,41 @@
async def _op_rebuild(project: str, env: str) -> AsyncGenerator[str, None]:
"""
- Rebuild: Coolify stop → docker build → Coolify start.
+ Rebuild: docker compose down → build image → docker compose up.
+ Uses `ops rebuild` on the host which handles env files, profiles, and cd correctly.
No data loss. For code/Dockerfile changes.
"""
- try:
- uuid = _coolify_uuid(project, env)
- build = _build_cfg(project, env)
- except ValueError as exc:
- yield _line(f"[error] Config error: {exc}")
- yield _done(False, project, env, "rebuild")
- return
+ yield _line(f"[rebuild] Rebuilding {project}/{env} via ops CLI...")
- # Step 1: Stop via Coolify
- yield _line(f"[rebuild] Stopping {project}/{env} via Coolify (uuid={uuid})...")
- try:
- await _coolify_action("stop", uuid)
- yield _line(f"[rebuild] Coolify stop queued. Waiting for containers to stop...")
- # Step 2: Poll until stopped
- stopped = await _poll_until_stopped(project, env)
- if not stopped:
- yield _line(f"[warn] Containers may still be running after {_POLL_MAX_WAIT}s — proceeding anyway")
- else:
- yield _line(f"[rebuild] All containers stopped.")
- except RuntimeError as exc:
- if "already stopped" in str(exc).lower():
- yield _line(f"[rebuild] Service already stopped — continuing with build.")
- else:
- yield _line(f"[error] Coolify stop failed: {exc}")
- yield _done(False, project, env, "rebuild")
- return
-
- # Step 3: Build image (if project uses local images)
- if build:
- ctx = build["build_context"]
- image = f"{build['image_name']}:{env}"
- yield _line(f"[rebuild] Building Docker image: {image}")
- yield _line(f"[rebuild] Build context: {ctx}")
-
- async for line in stream_command_host(
- ["docker", "build", "-t", image, ctx],
- timeout=_BACKUP_TIMEOUT,
- ):
+ had_output = False
+ success = True
+ async for line in stream_command_host(
+ [OPS_CLI, "rebuild", project, env],
+ timeout=_BACKUP_TIMEOUT,
+ ):
+ had_output = True
+ if line.startswith("[stderr] "):
yield _line(line)
- else:
- yield _line(f"[rebuild] No local image build needed (registry images only).")
+ elif line.startswith("ERROR") or line.startswith("[error]"):
+ yield _line(f"[error] {line}")
+ success = False
+ else:
+ yield _line(f"[rebuild] {line}")
- # Step 4: Start via Coolify
- yield _line(f"[rebuild] Starting {project}/{env} via Coolify...")
- try:
- await _coolify_action("start", uuid)
- yield _line(f"[rebuild] Coolify start queued. Waiting for containers...")
- except RuntimeError as exc:
- yield _line(f"[error] Coolify start failed: {exc}")
- yield _done(False, project, env, "rebuild")
- return
+ if not had_output:
+ yield _line(f"[error] ops rebuild produced no output — check registry config for {project}")
+ success = False
- # Step 5: Poll until running
- running = await _poll_until_running(project, env)
- if running:
- yield _line(f"[rebuild] Containers are up.")
- yield _done(True, project, env, "rebuild")
+ if success:
+ # Verify containers came up
+ containers = await _find_containers_for_service(project, env)
+ if containers:
+ yield _line(f"[rebuild] {len(containers)} container(s) running: {', '.join(containers)}")
+ yield _done(True, project, env, "rebuild")
+ else:
+ yield _line(f"[warn] No containers found after rebuild — check docker compose logs")
+ yield _done(False, project, env, "rebuild")
else:
- yield _line(f"[warn] Containers did not appear healthy within {_POLL_MAX_WAIT}s — check Coolify logs.")
yield _done(False, project, env, "rebuild")
@@ -369,12 +343,10 @@
async def _op_recreate(project: str, env: str) -> AsyncGenerator[str, None]:
"""
- Recreate: Coolify stop → wipe data → docker build → Coolify start.
+ Recreate: docker compose down → wipe data → docker build → docker compose up.
DESTRUCTIVE — wipes all data volumes. Shows "Go to Backups" banner on success.
"""
try:
- uuid = _coolify_uuid(project, env)
- build = _build_cfg(project, env)
data_dir = _data_dir(project, env)
cfg = _project_cfg(project)
except ValueError as exc:
@@ -382,42 +354,35 @@
yield _done(False, project, env, "recreate")
return
- # Step 1: Stop via Coolify
- yield _line(f"[recreate] Stopping {project}/{env} via Coolify (uuid={uuid})...")
- try:
- await _coolify_action("stop", uuid)
- yield _line(f"[recreate] Coolify stop queued. Waiting for containers to stop...")
- # Step 2: Poll until stopped
- stopped = await _poll_until_stopped(project, env)
- if not stopped:
- yield _line(f"[warn] Containers may still be running after {_POLL_MAX_WAIT}s — proceeding anyway")
- else:
- yield _line(f"[recreate] All containers stopped.")
- except RuntimeError as exc:
- if "already stopped" in str(exc).lower():
- yield _line(f"[recreate] Service already stopped — skipping stop step.")
- else:
- yield _line(f"[error] Coolify stop failed: {exc}")
- yield _done(False, project, env, "recreate")
- return
+ # Step 1: Find and stop containers via docker compose
+ code_dir = cfg.get("path", "") + f"/{env}/code"
+ yield _line(f"[recreate] Stopping {project}/{env} containers...")
- # Step 3: Wipe data volumes
- yield _line(f"[recreate] WARNING: Wiping data directory: {data_dir}")
- # Verify THIS env's containers are actually stopped before wiping
+ stop_result = await run_command_host(
+ ["sh", "-c", f"cd {code_dir} && docker compose -p {env}-{cfg.get('name_prefix', project)} --profile {env} down 2>&1 || true"],
+ timeout=120,
+ )
+ if stop_result["output"].strip():
+ for line in stop_result["output"].strip().splitlines():
+ yield _line(line)
+
+ # Step 2: Verify containers are stopped
name_prefix = cfg.get("name_prefix", project)
- # Use grep to AND-match both prefix and env (docker --filter uses OR for multiple name filters)
verify = await run_command_host(
- ["sh", "-c", f"docker ps --format '{{{{.Names}}}}' | grep '^{env}-{name_prefix}\\|^{name_prefix}-{env}' || true"],
+ ["sh", "-c", f"docker ps --format '{{{{.Names}}}}' | grep '^{env}-{name_prefix}-' || true"],
timeout=30,
)
- running = verify["output"].strip()
- if running:
+ running_containers = verify["output"].strip()
+ if running_containers:
yield _line(f"[error] Containers still running for {project}/{env}:")
- for line in running.splitlines():
+ for line in running_containers.splitlines():
yield _line(f" {line}")
yield _done(False, project, env, "recreate")
return
+ yield _line(f"[recreate] All containers stopped.")
+ # Step 3: Wipe data volumes
+ yield _line(f"[recreate] WARNING: Wiping data directory: {data_dir}")
wipe_result = await run_command_host(
["sh", "-c", f"rm -r {data_dir}/* 2>&1; echo EXIT_CODE=$?"],
timeout=120,
@@ -432,39 +397,24 @@
yield _done(False, project, env, "recreate")
return
- # Step 4: Build image (if project uses local images)
- if build:
- ctx = build["build_context"]
- image = f"{build['image_name']}:{env}"
- yield _line(f"[recreate] Building Docker image: {image}")
- yield _line(f"[recreate] Build context: {ctx}")
-
- async for line in stream_command_host(
- ["docker", "build", "-t", image, ctx],
- timeout=_BACKUP_TIMEOUT,
- ):
+ # Step 4: Rebuild via ops CLI (handles image build + compose up)
+ yield _line(f"[recreate] Rebuilding containers...")
+ async for line in stream_command_host(
+ [OPS_CLI, "rebuild", project, env],
+ timeout=_BACKUP_TIMEOUT,
+ ):
+ if line.startswith("[stderr] "):
yield _line(line)
- else:
- yield _line(f"[recreate] No local image build needed (registry images only).")
+ else:
+ yield _line(f"[recreate] {line}")
- # Step 5: Start via Coolify
- yield _line(f"[recreate] Starting {project}/{env} via Coolify...")
- try:
- await _coolify_action("start", uuid)
- yield _line(f"[recreate] Coolify start queued. Waiting for containers...")
- except RuntimeError as exc:
- yield _line(f"[error] Coolify start failed: {exc}")
- yield _done(False, project, env, "recreate")
- return
-
- # Step 6: Poll until running
- running = await _poll_until_running(project, env)
- if running:
- yield _line(f"[recreate] Containers are up. Restore a backup to complete recovery.")
+ # Step 5: Verify containers came up
+ containers = await _find_containers_for_service(project, env)
+ if containers:
+ yield _line(f"[recreate] {len(containers)} container(s) running. Restore a backup to complete recovery.")
yield _done(True, project, env, "recreate")
else:
- yield _line(f"[warn] Containers did not appear within {_POLL_MAX_WAIT}s — check Coolify logs.")
- # Still return success=True so the "Go to Backups" banner appears
+ yield _line(f"[warn] No containers found after recreate — check docker compose logs")
yield _done(True, project, env, "recreate")
--
Gitblit v1.3.1