From 9ce9cfcef591af53789736a9386af380a1401df2 Mon Sep 17 00:00:00 2001 From: Sebastian Garcia Date: Thu, 3 Sep 2026 16:46:50 +0200 Subject: [PATCH 1/5] fix: avoid GHCR dependency in dashboard build --- dashboard/Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dashboard/Dockerfile b/dashboard/Dockerfile index 8515207..f7700e6 100644 --- a/dashboard/Dockerfile +++ b/dashboard/Dockerfile @@ -17,7 +17,9 @@ RUN headref=$(awk '{ print $2 }' .git/HEAD) \ # Stage 2: Download Python dependencies FROM python:3.12-slim AS download-python -COPY --from=ghcr.io/astral-sh/uv:0.12.9@sha256:8b940d3a9d65bed080436972241af2e21c84b5e8c9193f7014ed71479ee795ff /uv /bin/uv +# Install the pinned uv release from PyPI. Building from the GHCR image fails +# in environments where ghcr.io's token service is unavailable. +RUN pip install --no-cache-dir uv==0.12.9 COPY dashboard/server/requirements.txt . ENV UV_COMPILE_BYTECODE=1 \ From 917502acefc0836eb00c9a8cc33c94db8dd80f47 Mon Sep 17 00:00:00 2001 From: Sebastian Garcia Date: Fri, 4 Sep 2026 10:12:11 +0200 Subject: [PATCH 2/5] fix: reuse local images when starting plugins --- dashboard/server/app.py | 1 - dashboard/server/docker.py | 12 ++++++++++++ docs/plugins.md | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/dashboard/server/app.py b/dashboard/server/app.py index 2124aeb..1a8e913 100644 --- a/dashboard/server/app.py +++ b/dashboard/server/app.py @@ -338,7 +338,6 @@ async def plugin_start(plugin_id: str): try: eprint(f"Let's start a plugin with id: '{plugin_id}'") - docker.stop_compose(plugin['dir']) docker.start_compose(plugin['dir']) except Exception as exc: eprint(f"error starting a plugin ({plugin_id}): {exc}") diff --git a/dashboard/server/docker.py b/dashboard/server/docker.py index c58fefe..90e52fd 100644 --- a/dashboard/server/docker.py +++ b/dashboard/server/docker.py @@ -5,6 +5,18 @@ def start_compose(dir: str): file = f"{dir}/docker-compose.yml" + # Starting an already-built plugin must work offline. In particular, avoid + # asking registries for base-image metadata on every stop/start cycle. + result = subprocess.run( + ['docker-compose', '-f', file, 'up', '-d', '--no-build'], + stdout=sys.stdout, + stderr=sys.stderr + ) + if result.returncode == 0: + return + + # A newly installed plugin might not have a local image yet. Build only as + # a fallback, after the cache-only start proves insufficient. result = subprocess.run( ['docker-compose', '-f', file, 'up', '-d', '--build'], stdout=sys.stdout, diff --git a/docs/plugins.md b/docs/plugins.md index 21eabd0..f2447e9 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -71,6 +71,7 @@ Plugin authors are fully responsible for their own `docker-compose.yml`, includi - Plugin services should perform graceful shutdown by handling cleanly `SIGINT` signal (received when plugin stops) such as stopping dynamically started containers etc. - All containers defined in the plugin compose file, and any containers the plugin starts later, must use the `scl-plugin--` prefix for observability and debugging reasons. +- Starting a plugin reuses its locally built images so routine stop/start cycles work without registry access. If an image is not available locally, the dashboard retries the start with a build. ## Reference Plugin From 38562588ce38f92db0c6d59a317a184fe5c0d59c Mon Sep 17 00:00:00 2001 From: Sebastian Garcia Date: Fri, 4 Sep 2026 11:40:02 +0200 Subject: [PATCH 3/5] fix: pin plugin compose project names --- dashboard/server/app.py | 10 ++++++---- dashboard/server/docker.py | 27 ++++++++++++++++++--------- docs/plugins.md | 1 + 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/dashboard/server/app.py b/dashboard/server/app.py index 1a8e913..0ccd994 100644 --- a/dashboard/server/app.py +++ b/dashboard/server/app.py @@ -241,7 +241,9 @@ def plugin_to_public(plugin: dict) -> dict: if public_plugin['valid']: try: - public_plugin['running'] = docker.is_up(public_plugin['dir']) + public_plugin['running'] = docker.is_up( + public_plugin['dir'], public_plugin['id'] + ) except Exception as exc: public_plugin['runtime_error'] = str(exc) @@ -338,7 +340,7 @@ async def plugin_start(plugin_id: str): try: eprint(f"Let's start a plugin with id: '{plugin_id}'") - docker.start_compose(plugin['dir']) + docker.start_compose(plugin['dir'], plugin_id) except Exception as exc: eprint(f"error starting a plugin ({plugin_id}): {exc}") return f"error starting a plugin ({plugin_id}): {exc}", 500 @@ -357,7 +359,7 @@ async def plugin_stop(plugin_id: str): try: eprint(f"Let's stop a plugin with id: '{plugin_id}'") - docker.stop_compose(plugin['dir']) + docker.stop_compose(plugin['dir'], plugin_id) except Exception as exc: eprint(f"error stopping a plugin ({plugin_id}): {exc}") return f"error stopping a plugin ({plugin_id}): {exc}", 500 @@ -376,7 +378,7 @@ async def plugins_stop_all(): plugin_id = plugin['id'] try: eprint(f"Let's stop a plugin with id: '{plugin_id}'") - docker.stop_compose(plugin['dir']) + docker.stop_compose(plugin['dir'], plugin_id) except Exception as exc: eprint(f"error stopping a plugin ({plugin_id}): {exc}") return f"error stopping a plugin ({plugin_id}): {exc}", 500 diff --git a/dashboard/server/docker.py b/dashboard/server/docker.py index 90e52fd..c96b7e7 100644 --- a/dashboard/server/docker.py +++ b/dashboard/server/docker.py @@ -2,13 +2,21 @@ import sys -def start_compose(dir: str): +def _compose_command(file: str, project_name: str = None) -> list[str]: + command = ['docker-compose', '-f', file] + if project_name: + command.extend(['--project-name', project_name]) + return command + + +def start_compose(dir: str, project_name: str = None): file = f"{dir}/docker-compose.yml" + command = _compose_command(file, project_name) # Starting an already-built plugin must work offline. In particular, avoid # asking registries for base-image metadata on every stop/start cycle. result = subprocess.run( - ['docker-compose', '-f', file, 'up', '-d', '--no-build'], + [*command, 'up', '-d', '--no-build'], stdout=sys.stdout, stderr=sys.stderr ) @@ -18,7 +26,7 @@ def start_compose(dir: str): # A newly installed plugin might not have a local image yet. Build only as # a fallback, after the cache-only start proves insufficient. result = subprocess.run( - ['docker-compose', '-f', file, 'up', '-d', '--build'], + [*command, 'up', '-d', '--build'], stdout=sys.stdout, stderr=sys.stderr ) @@ -26,11 +34,11 @@ def start_compose(dir: str): raise Exception("Error doing docker-compose up") -def stop_compose(dir: str): +def stop_compose(dir: str, project_name: str = None): file = f"{dir}/docker-compose.yml" result = subprocess.run( - ['docker-compose', '-f', file, 'down'], + [*_compose_command(file, project_name), 'down'], stdout=sys.stdout, stderr=sys.stderr ) @@ -38,12 +46,13 @@ def stop_compose(dir: str): raise Exception("Error doing docker-compose down") -def is_up(dir: str) -> bool: +def is_up(dir: str, project_name: str = None) -> bool: file = f"{dir}/docker-compose.yml" + command = _compose_command(file, project_name) result = subprocess.run( - f"docker-compose -f {file} ps --services --filter 'status=running'", - shell=True, capture_output=True, text=True + [*command, 'ps', '--services', '--filter', 'status=running'], + capture_output=True, text=True ) if result.returncode != 0: raise Exception("Error reading all services") @@ -54,7 +63,7 @@ def is_up(dir: str) -> bool: return False result = subprocess.run( - ['docker-compose', '-f', file, 'ps', "--services"], + [*command, 'ps', '--services'], capture_output=True, text=True ) if result.returncode != 0: diff --git a/docs/plugins.md b/docs/plugins.md index f2447e9..bd2f9b6 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -72,6 +72,7 @@ Plugin authors are fully responsible for their own `docker-compose.yml`, includi - Plugin services should perform graceful shutdown by handling cleanly `SIGINT` signal (received when plugin stops) such as stopping dynamically started containers etc. - All containers defined in the plugin compose file, and any containers the plugin starts later, must use the `scl-plugin--` prefix for observability and debugging reasons. - Starting a plugin reuses its locally built images so routine stop/start cycles work without registry access. If an image is not available locally, the dashboard retries the start with a build. +- The dashboard uses the plugin metadata `id` as the Docker Compose project name, so image and container ownership stays consistent regardless of the plugin directory name or launch context. ## Reference Plugin From d0a8717f42bc3c6e637d31adf20955018eaaa28d Mon Sep 17 00:00:00 2001 From: Sebastian Garcia Date: Fri, 4 Sep 2026 11:52:15 +0200 Subject: [PATCH 4/5] fix: preserve existing plugin compose projects --- dashboard/server/app.py | 10 ++++------ dashboard/server/docker.py | 19 ++++++++----------- docs/plugins.md | 1 - 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/dashboard/server/app.py b/dashboard/server/app.py index 0ccd994..1a8e913 100644 --- a/dashboard/server/app.py +++ b/dashboard/server/app.py @@ -241,9 +241,7 @@ def plugin_to_public(plugin: dict) -> dict: if public_plugin['valid']: try: - public_plugin['running'] = docker.is_up( - public_plugin['dir'], public_plugin['id'] - ) + public_plugin['running'] = docker.is_up(public_plugin['dir']) except Exception as exc: public_plugin['runtime_error'] = str(exc) @@ -340,7 +338,7 @@ async def plugin_start(plugin_id: str): try: eprint(f"Let's start a plugin with id: '{plugin_id}'") - docker.start_compose(plugin['dir'], plugin_id) + docker.start_compose(plugin['dir']) except Exception as exc: eprint(f"error starting a plugin ({plugin_id}): {exc}") return f"error starting a plugin ({plugin_id}): {exc}", 500 @@ -359,7 +357,7 @@ async def plugin_stop(plugin_id: str): try: eprint(f"Let's stop a plugin with id: '{plugin_id}'") - docker.stop_compose(plugin['dir'], plugin_id) + docker.stop_compose(plugin['dir']) except Exception as exc: eprint(f"error stopping a plugin ({plugin_id}): {exc}") return f"error stopping a plugin ({plugin_id}): {exc}", 500 @@ -378,7 +376,7 @@ async def plugins_stop_all(): plugin_id = plugin['id'] try: eprint(f"Let's stop a plugin with id: '{plugin_id}'") - docker.stop_compose(plugin['dir'], plugin_id) + docker.stop_compose(plugin['dir']) except Exception as exc: eprint(f"error stopping a plugin ({plugin_id}): {exc}") return f"error stopping a plugin ({plugin_id}): {exc}", 500 diff --git a/dashboard/server/docker.py b/dashboard/server/docker.py index c96b7e7..042406d 100644 --- a/dashboard/server/docker.py +++ b/dashboard/server/docker.py @@ -2,16 +2,13 @@ import sys -def _compose_command(file: str, project_name: str = None) -> list[str]: - command = ['docker-compose', '-f', file] - if project_name: - command.extend(['--project-name', project_name]) - return command +def _compose_command(file: str) -> list[str]: + return ['docker-compose', '-f', file] -def start_compose(dir: str, project_name: str = None): +def start_compose(dir: str): file = f"{dir}/docker-compose.yml" - command = _compose_command(file, project_name) + command = _compose_command(file) # Starting an already-built plugin must work offline. In particular, avoid # asking registries for base-image metadata on every stop/start cycle. @@ -34,11 +31,11 @@ def start_compose(dir: str, project_name: str = None): raise Exception("Error doing docker-compose up") -def stop_compose(dir: str, project_name: str = None): +def stop_compose(dir: str): file = f"{dir}/docker-compose.yml" result = subprocess.run( - [*_compose_command(file, project_name), 'down'], + [*_compose_command(file), 'down'], stdout=sys.stdout, stderr=sys.stderr ) @@ -46,9 +43,9 @@ def stop_compose(dir: str, project_name: str = None): raise Exception("Error doing docker-compose down") -def is_up(dir: str, project_name: str = None) -> bool: +def is_up(dir: str) -> bool: file = f"{dir}/docker-compose.yml" - command = _compose_command(file, project_name) + command = _compose_command(file) result = subprocess.run( [*command, 'ps', '--services', '--filter', 'status=running'], diff --git a/docs/plugins.md b/docs/plugins.md index bd2f9b6..f2447e9 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -72,7 +72,6 @@ Plugin authors are fully responsible for their own `docker-compose.yml`, includi - Plugin services should perform graceful shutdown by handling cleanly `SIGINT` signal (received when plugin stops) such as stopping dynamically started containers etc. - All containers defined in the plugin compose file, and any containers the plugin starts later, must use the `scl-plugin--` prefix for observability and debugging reasons. - Starting a plugin reuses its locally built images so routine stop/start cycles work without registry access. If an image is not available locally, the dashboard retries the start with a build. -- The dashboard uses the plugin metadata `id` as the Docker Compose project name, so image and container ownership stays consistent regardless of the plugin directory name or launch context. ## Reference Plugin From b86a4b31b2649b99320e3132653f607b63df9880 Mon Sep 17 00:00:00 2001 From: Sebastian Garcia Date: Mon, 7 Sep 2026 11:43:40 +0200 Subject: [PATCH 5/5] revert: rebuild plugins when starting --- dashboard/server/app.py | 1 + dashboard/server/docker.py | 28 +++++----------------------- docs/plugins.md | 1 - 3 files changed, 6 insertions(+), 24 deletions(-) diff --git a/dashboard/server/app.py b/dashboard/server/app.py index 1a8e913..2124aeb 100644 --- a/dashboard/server/app.py +++ b/dashboard/server/app.py @@ -338,6 +338,7 @@ async def plugin_start(plugin_id: str): try: eprint(f"Let's start a plugin with id: '{plugin_id}'") + docker.stop_compose(plugin['dir']) docker.start_compose(plugin['dir']) except Exception as exc: eprint(f"error starting a plugin ({plugin_id}): {exc}") diff --git a/dashboard/server/docker.py b/dashboard/server/docker.py index 042406d..c58fefe 100644 --- a/dashboard/server/docker.py +++ b/dashboard/server/docker.py @@ -2,28 +2,11 @@ import sys -def _compose_command(file: str) -> list[str]: - return ['docker-compose', '-f', file] - - def start_compose(dir: str): file = f"{dir}/docker-compose.yml" - command = _compose_command(file) - - # Starting an already-built plugin must work offline. In particular, avoid - # asking registries for base-image metadata on every stop/start cycle. - result = subprocess.run( - [*command, 'up', '-d', '--no-build'], - stdout=sys.stdout, - stderr=sys.stderr - ) - if result.returncode == 0: - return - # A newly installed plugin might not have a local image yet. Build only as - # a fallback, after the cache-only start proves insufficient. result = subprocess.run( - [*command, 'up', '-d', '--build'], + ['docker-compose', '-f', file, 'up', '-d', '--build'], stdout=sys.stdout, stderr=sys.stderr ) @@ -35,7 +18,7 @@ def stop_compose(dir: str): file = f"{dir}/docker-compose.yml" result = subprocess.run( - [*_compose_command(file), 'down'], + ['docker-compose', '-f', file, 'down'], stdout=sys.stdout, stderr=sys.stderr ) @@ -45,11 +28,10 @@ def stop_compose(dir: str): def is_up(dir: str) -> bool: file = f"{dir}/docker-compose.yml" - command = _compose_command(file) result = subprocess.run( - [*command, 'ps', '--services', '--filter', 'status=running'], - capture_output=True, text=True + f"docker-compose -f {file} ps --services --filter 'status=running'", + shell=True, capture_output=True, text=True ) if result.returncode != 0: raise Exception("Error reading all services") @@ -60,7 +42,7 @@ def is_up(dir: str) -> bool: return False result = subprocess.run( - [*command, 'ps', '--services'], + ['docker-compose', '-f', file, 'ps', "--services"], capture_output=True, text=True ) if result.returncode != 0: diff --git a/docs/plugins.md b/docs/plugins.md index f2447e9..21eabd0 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -71,7 +71,6 @@ Plugin authors are fully responsible for their own `docker-compose.yml`, includi - Plugin services should perform graceful shutdown by handling cleanly `SIGINT` signal (received when plugin stops) such as stopping dynamically started containers etc. - All containers defined in the plugin compose file, and any containers the plugin starts later, must use the `scl-plugin--` prefix for observability and debugging reasons. -- Starting a plugin reuses its locally built images so routine stop/start cycles work without registry access. If an image is not available locally, the dashboard retries the start with a build. ## Reference Plugin