fix: report and repair a missing container→host SSH channel - #518
Open
Rish-it wants to merge 4 commits into
Open
fix: report and repair a missing container→host SSH channel#518Rish-it wants to merge 4 commits into
Rish-it wants to merge 4 commits into
Conversation
`provisionHostSshChannel` collapsed every failure to `null`, which is also what the deliberate absences return (`--no-host-control`, a non-Linux box). `renderEnv` then wrote an `.env` with no OPENSHIP_HOST_SSH_* keys, `openship up` reported a healthy install, and the first deploy to that box failed with "no host channel is configured (OPENSHIP_HOST_SSH_HOST is unset)". A missing `ssh-keygen` was enough, and re-running `openship up` — the remedy that error names — silently did the same thing again. It now returns the reason alongside the channel, and `composeUp` prints it once the stack is up: what broke, what it costs, and how to confirm the fix. The install still succeeds — a box that only manages remote servers needs no channel — it just stops being silent about it. Adds `composeHostChannel()`, the same question asked of an install that already exists (`.env` + the key on disk), for `openship doctor`.
Compose only: that stack's API is containerized, so every operation on the box it runs on goes over SSH to the host. An install whose channel never got provisioned reported all-green here — service, database, API, dashboard, edge — and only failed at the first deploy. Fails the check when the channel is missing or its key is gone, passes when it is there or host control was deliberately turned off.
`.env.example` tells a raw `docker compose` install to set OPENSHIP_HOST_SSH_KEY=/run/secrets/openship_host_key, but nothing ever mounted a key there — so that stack could never do a host operation no matter what its `.env` said. Mounts it from OPENSHIP_HOST_KEY_PATH, /dev/null when unset, matching the compose file `openship up` generates.
"Re-run `openship up`" is a no-op for the case that produces this error most often — a run that already tried and couldn't. Name `openship doctor` first, which now reports the channel and why it is missing.
There was a problem hiding this comment.
Pull request overview
This PR hardens containerized (Docker Compose) installs by making missing container→host SSH provisioning visible at install time, diagnosable via openship doctor, and supportable in the “raw docker compose” stack by mounting the configured host key into the API container.
Changes:
- Update compose install provisioning to distinguish “deliberate absence” from “provisioning failure”, surface failures after
up, and add acomposeHostChannel()doctor check. - Extend
openship doctorto report host-channel health for compose installs. - Align the raw
docker/docker-compose.ymlstack and.env.examplewith the host-key mount/config needed for host operations; improve the runtime error guidance to point users atopenship doctor.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/adapters/src/system/executor.ts | Improves the “missing host channel” error to direct users to openship doctor for diagnosis. |
| docker/docker-compose.yml | Adds a bind mount for the host SSH key into the API container for raw compose installs. |
| apps/cli/test/unit/compose-host-channel.test.ts | Adds unit coverage for provisioning warnings vs deliberate absence, and doctor reporting for channel states. |
| apps/cli/src/lib/repair.ts | Adds a compose-only “Host” component check to openship doctor. |
| apps/cli/src/lib/compose.ts | Returns { channel, error } from provisioning, adds composeHostChannel(), threads warning text to the end of composeUp(). |
| .env.example | Documents OPENSHIP_HOST_KEY_PATH alongside host-channel SSH variables for raw compose setups. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+572
to
+579
| if (g.error || g.status !== 0) { | ||
| return { | ||
| channel: null, | ||
| error: g.error | ||
| ? "ssh-keygen isn't installed (package: openssh-client)" | ||
| : `ssh-keygen failed (exit ${g.status})`, | ||
| }; | ||
| } |
| return null; | ||
| return { channel: { user, keyPath }, error: null }; | ||
| } catch (err) { | ||
| return { channel: null, error: (err as Error).message }; |
| - /etc/letsencrypt:/etc/letsencrypt:z | ||
| - /var/lib/openship/edge/acme:/var/www/acme:z | ||
| - /opt/openship/static:/opt/openship/static:z | ||
| # Host-op SSH key at the path OPENSHIP_HOST_SSH_KEY names in .env.example — |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A containerized install could come up with no container→host SSH channel, report success, and only fail at the first deploy —
This operation targets the HOST machine, but no host channel is configured (OPENSHIP_HOST_SSH_HOST is unset).openship upnow says why it couldn't provision the channel,openship doctorreports whether an install has one, and the rawdocker composestack finally has somewhere to mount the key its.env.exampletells you to configure.Motivation
openship upswallowed every reason it couldn't provision.provisionHostSshChannelis onetry { … } catch { return null }, andnullis also what the DELIBERATE absences return (--no-host-control, a non-Linux box).renderEnvwrites theOPENSHIP_HOST_SSH_*keys onlyif (host), so any failure produced an.envwith no channel, an install that printed✔ Openship is running via Docker Compose, and a deploy that died later with the error above. A missingssh-keygenwas enough to get there.Worse, the remedy that error names is a no-op for exactly this case: re-running
openship upon such a box provisions nothing again, exit 0, no output. Nothing else surfaced it either —openship doctorreported all-green, andprobeReachable(ssh-manager.ts:648) callsrecordSuccesswhen the var is unset, so the box shows Online right up to the deploy.The raw compose stack could never have a channel.
docker/docker-compose.yml(README: "Self-host with raw Docker Compose (no CLI)") setsextra_hosts: host.docker.internal:host-gatewaybut mounts no key, while.env.exampletells that install to setOPENSHIP_HOST_SSH_KEY=/run/secrets/openship_host_key— a path nothing ever put a file at. That stack fails every host operation regardless of its.env.Deploy is where it surfaces because a "This Server" deploy resolves through
resolveServerExecutor→acquireHostChannel, and the host-port allocation inbuild-pipeline.ts:1580goes throughsshManager.withHostExecutor— both before any Docker work.Related issue
Fixes #509
Changes
apps/cli/src/lib/compose.ts—provisionHostSshChannelreturns{ channel, error }:erroris null for the deliberate absences and set for everything else (ENOENT is reported as "ssh-keygen isn't installed (package: openssh-client)", not a generic failure).composeUpprints it once the stack is up — after the pull output, not buried in it. The install still succeeds; a box that only manages remote servers is fine without a channel. AddscomposeHostChannel(): the same question asked of an install that already exists (.env+ the key on disk), shaped as adoctorcheck.apps/cli/src/lib/repair.ts—openship doctorgains aHostrow, compose installs only (a bare install is already on the host). Fails when the channel is missing or its key is gone; passes when it's there or host control was deliberately turned off. Lands in the panel, the one-shot report and--jsonthrough the existingComponentCheckpipeline.docker/docker-compose.yml— mount the host key fromOPENSHIP_HOST_KEY_PATH,/dev/nullwhen unset, matching the compose fileopenship upgenerates..env.example— nameOPENSHIP_HOST_KEY_PATHalongside the vars that depend on it.packages/adapters/src/system/executor.ts— the error points atopenship doctorbeforeopenship up, since the run that produced it may already have tried.apps/cli/test/unit/compose-host-channel.test.ts— new; provisioning reported vs. deliberate absence stays quiet, and each statedoctorreports.Verification
Reproduced end to end first, on a real Linux host rather than a mock: a privileged container running its own
dockerdandsshd, driving the real CLI throughopenship up --composewith real image pulls (postgres, redis, api, dashboard, edge).A clean box provisions correctly — this is not broken in general:
Remove
ssh-keygen, re-run —mainreports a healthy install with no channel, and the API then returns the reported error verbatim:Same box, this branch:
Restore
ssh-keygen, re-runopenship up: no warning,✓ Host SSH to host.docker.internal as root, and the call that returned the error now returns{"status":{"classification":"ours",…}}.The raw compose stack, brought up for real from
docker/docker-compose.yml(postgres + redis + api) with the key the CLI provisioned:Negative control on the same stack with
OPENSHIP_HOST_KEY_PATHunset, so the mount falls back to/dev/null(the pre-fix state — key named in.env, nothing mounted): the api still boots healthy, and the host operation fails withSSH requires one of privateKey, sshAgent, or password.So the mount is load-bearing and its default is harmless.The regression test fails with only
compose.tsreverted — 6 of 8, with the two negative controls staying green, so the warning isn't unconditional:Suites, run twice each and green both times:
Every commit on the branch was checked individually — 233 CLI tests passing at each, none red.
Two pre-existing failures on
mainthat this branch neither touches nor fixes:@repo/dashboard#lint(stale.nextgenerated types against the installed Next — fails identically on a cleanmain), andprettier --checkoncompose.ts,repair.tsanddocker-compose.yml, all three of which are already unformatted onmain. Runningbun formatwould rewrite unrelated lines in them; the one new file is prettier-clean.Deliberately out of scope, both worth their own change:
up --dry-runstill previews a channel it may be unable to create (composePlancalls the pure planner, never the provisioner), and a local Docker deploy still requires the channel for host-port allocation even though the build and run go over the mounted socket.Checklist
bun run test,bun run --cwd <workspace> lint, andbun formatall pass locally —bun run testpasses;@repo/dashboard#lintandbun formatare pre-existing failures onmain, see Verification