You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(selfhost): host-companion redeploy trigger via MCP admin tool
Closes#7723. A new admin-category MCP tool, loopover_admin_trigger_redeploy,
lets an operator trigger a real self-redeploy (pull the published image,
restart, wait for health) through an MCP client -- without ever mounting
/var/run/docker.sock into an app-facing container.
Design decision (proxy-mediated vs. host-companion, per #7720's own framing):
host companion. Investigated extending the existing docker-proxy sidecar
first -- tecnativa/docker-socket-proxy's ACL model has no way to scope a
restart to one named container (only "any container reachable through the
socket"), and has no concept of "pull a new image" at all, since the real
desired behavior is docker-compose-level orchestration, not a single Docker
Engine API call a raw proxy could cleanly allowlist.
The companion (scripts/redeploy-companion.ts) runs entirely outside Docker as
a systemd service, listens on a Unix domain socket (never a TCP port),
authenticates with its own separate REDEPLOY_COMPANION_TOKEN (distinct from
LOOPOVER_MCP_ADMIN_TOKEN -- two independent credentials, not one), and shells
out to the existing, already-tested scripts/deploy-selfhost-image.sh rather
than reimplementing its pull+recreate+health-wait sequence.
Entirely opt-in: no companion installed means the tool reports
configured: false, every other admin tool is unaffected.
Copy file name to clipboardExpand all lines: apps/loopover-ui/content/docs/self-hosting-configuration.mdx
+43-1Lines changed: 43 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -345,7 +345,49 @@ Restart the `loopover` service after changing either (`docker compose up -d --no
345
345
Drop `dryRun` (or set it to `false`) to write for real once you're happy with the dry-run result. `scope` is `"global"` (the mount-root default file) or `"repo"` (pass `repoFullName`); `loopover_admin_get_config` additionally accepts `"effective"` to read the exact deep-merged view a real review sees, same as the "Private per-repo config" deep-merge described above. `loopover_admin_list_config_backups` takes the same `scope`/`repoFullName` pair and returns each backup's path and timestamp, newest first.
346
346
347
347
<Calloutvariant="warn"title="Boundaries">
348
-
These tools only read and write `LOOPOVER_REPO_CONFIG_DIR`. They do not trigger a redeploy, and they do not touch the public dashboard or `/v1/app/*` settings surface — `LOOPOVER_MCP_ADMIN_TOKEN` cannot sign into the control panel or call the routes `ADMIN_GITHUB_LOGINS` gates.
348
+
These three tools only read and write `LOOPOVER_REPO_CONFIG_DIR`. Triggering a redeploy is a separate tool with its own separate token — see "MCP redeploy trigger" below. None of the admin tools touch the public dashboard or `/v1/app/*` settings surface — `LOOPOVER_MCP_ADMIN_TOKEN` cannot sign into the control panel or call the routes `ADMIN_GITHUB_LOGINS` gates.
349
+
</Callout>
350
+
351
+
## MCP redeploy trigger
352
+
353
+
A fourth `admin`-category tool, `loopover_admin_trigger_redeploy` (#7723): pulls the published image, restarts this instance, and waits for it to report healthy — the same sequence `./scripts/deploy-selfhost-image.sh` runs by hand, triggered remotely through an MCP client instead. Same `LOOPOVER_MCP_ADMIN_ENABLED` + `LOOPOVER_MCP_ADMIN_TOKEN` gating as the config tools above, plus one more layer specific to this tool.
354
+
355
+
<Calloutvariant="warn"title="Why this needs a whole extra moving part, not just another admin tool">
356
+
Every other admin tool runs entirely inside this container. Triggering a redeploy can't: this container has to end up running a *new* image, which means something *outside* this container has to do the actual `docker compose pull && up -d`. The obvious shortcut — mount `/var/run/docker.sock` into this container so it can ask Docker to do it — is explicitly the thing this repo tells contributors never to do (see `docker-proxy`'s own comment in `docker-compose.yml`: a bind-mounted socket, even read-only, is "effectively host root"; `review-enrichment/src/analyzers/iac-misconfig.ts` flags exactly this pattern in the PRs this bot reviews for everyone else). Extending the existing `docker-proxy` sidecar (`tecnativa/docker-socket-proxy`) doesn't avoid the problem either — its access-control model has no way to scope a restart to *one* named container, only "any container reachable through the socket," and it has no concept of "pull a new image" at all (that's compose-level orchestration, not a single Docker Engine API call).
357
+
358
+
The design here instead runs a small **host companion** — a plain Node process, outside Docker entirely, installed as a systemd service (`systemd/loopover-redeploy-companion.service.example`) — that listens on a Unix domain socket and, on an authenticated request, shells out to the real `deploy-selfhost-image.sh`. This container reaches it via a narrow, purpose-built socket bind-mounted in (never the Docker socket itself), authenticated with its own separate shared secret (`REDEPLOY_COMPANION_TOKEN`) so a leaked `LOOPOVER_MCP_ADMIN_TOKEN` alone still can't trigger a redeploy — two independent credentials have to both be compromised, not one.
359
+
</Callout>
360
+
361
+
**Entirely opt-in.** Skip this whole section and the other three admin tools work exactly as documented above — `loopover_admin_trigger_redeploy` just reports `configured: false` until you set it up.
The `loopover` service's `docker-compose.yml` entry already bind-mounts the companion's socket and reads `REDEPLOY_COMPANION_TOKEN_FILE` — nothing to add there. Restart the `loopover` service once the companion is running so it picks up the token.
`image` is optional — omit it to redeploy whatever `LOOPOVER_IMAGE` is already configured (the same default `deploy-selfhost-image.sh` itself uses). The tool call doesn't return until the companion's own health-wait finishes (or times out), and streams back every log line the real script printed along the way, so a failed redeploy comes back with the actual reason, not just a bare non-zero exit code.
388
+
389
+
<Calloutvariant="note"title="This restarts the very process serving the tool call">
390
+
A successful redeploy means this container gets replaced mid-request. The companion waits for the *new* container to report healthy before it responds, so the tool call itself completes normally either way — but expect the connection to briefly drop if you're also watching logs live.
349
391
</Callout>
350
392
351
393
## Config-as-code blocks with no dashboard equivalent
Copy file name to clipboardExpand all lines: apps/loopover-ui/content/docs/self-hosting-security.mdx
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -26,7 +26,7 @@ eyebrow: Self-hosting
26
26
]}
27
27
/>
28
28
29
-
`docker-compose.yml` ships native Docker Compose `secrets:` mounts for the highest-value secrets (the GitHub App private key, webhook secret, API/MCP/MCP-admin/internal-job tokens, the setup token, the two token-encryption master keys, the Orb enrollment secret, the PagerDuty routing key, and the Claude Code subscription token) — file-mounted at `/run/secrets/<name>`, never exposed via `docker inspect` or `docker compose config` the way a plain `environment:`/`env_file` value is. This is purely additive: an inline `.env` value always takes priority if you set both, so you can migrate one secret at a time, or not at all. See `secrets/README.md` for the full file list.
29
+
`docker-compose.yml` ships native Docker Compose `secrets:` mounts for the highest-value secrets (the GitHub App private key, webhook secret, API/MCP/MCP-admin/redeploy-companion/internal-job tokens, the setup token, the two token-encryption master keys, the Orb enrollment secret, the PagerDuty routing key, and the Claude Code subscription token) — file-mounted at `/run/secrets/<name>`, never exposed via `docker inspect` or `docker compose config` the way a plain `environment:`/`env_file` value is. This is purely additive: an inline `.env` value always takes priority if you set both, so you can migrate one secret at a time, or not at all. See `secrets/README.md` for the full file list.
30
30
31
31
<CodeBlock
32
32
filename="shell"
@@ -50,7 +50,7 @@ category a secret falls into before rotating it.
50
50
{
51
51
title: "Freely rotatable",
52
52
description:
53
-
"GITHUB_WEBHOOK_SECRET, LOOPOVER_API_TOKEN, LOOPOVER_MCP_TOKEN, LOOPOVER_MCP_ADMIN_TOKEN, INTERNAL_JOB_TOKEN, SELFHOST_SETUP_TOKEN, REES_SHARED_SECRET. Static bearer comparisons -- nothing is encrypted with them. Generate a new value, restart both sides. Only cost: updating callers holding the old value.",
53
+
"GITHUB_WEBHOOK_SECRET, LOOPOVER_API_TOKEN, LOOPOVER_MCP_TOKEN, LOOPOVER_MCP_ADMIN_TOKEN, INTERNAL_JOB_TOKEN, SELFHOST_SETUP_TOKEN, REES_SHARED_SECRET. Static bearer comparisons -- nothing is encrypted with them. Generate a new value, restart both sides. Only cost: updating callers holding the old value. REDEPLOY_COMPANION_TOKEN is the one exception in this list that needs a second manual step: it's shared between this container and the separate host companion process, so rotating it means updating BOTH secrets/redeploy_companion_token.txt and the companion's own /etc/loopover-redeploy-companion.env, not just one file.",
0 commit comments