Host control channel
"This operation targets the HOST machine, but no host channel is configured" — what the host channel is, why the rest of the install looks healthy, and the repair for each install shape.
When Openship runs in a container it builds and runs your apps through the Docker socket — but a
container can't operate the machine underneath it. For the handful of jobs that need the host itself,
Openship keeps a private SSH channel to it: an ed25519 key authorized for one user, dialed over the
internal host.docker.internal bridge and never over a public address.
This page is about the state where that channel was never built. The install reported success, the dashboard looks fine, ordinary deploys work — and then one specific operation fails with this:
This operation targets the HOST machine, but no host channel is configured
(OPENSHIP_HOST_SSH_HOST is unset) and Openship is running in a container — so it
would have run inside the container instead, against the wrong filesystem.
Re-run `openship up` to provision the host channel. `openship doctor` reports
whether it worked.You can also meet it before anything fails: as a !!! HOST CONTROL NOT CONFIGURED banner in the API's
boot log, as a Host control row reading never provisioned in openship doctor, or as a banner on
the server's screen in the dashboard.
Where this lives
Nothing here is a dashboard setting — the channel is CLI-only on purpose, because a toggle in the dashboard would be reachable by the very thing it defends against. The install-time context is in Installation → Host control; the commands are in the CLI reference.
This is degraded, not broken
Read this before you touch anything, because the instinct to rebuild the box is the wrong one:
- Ordinary deploys to this box still work — they go through the Docker socket. Building, running, restarting and routing your apps needs the socket, not this channel.
- Blocked until the channel works:
- the host terminal, host system info and host port scans
- taking over
:80/:443from a proxy Openship didn't start - installing or updating the mail engine
- deploying a catalog app that needs a generated config file on the host (Supabase's
kong.yml, say)
One more symptom belongs next to those but isn't a lost feature: this box also reads Offline in the dashboard's server list while the channel is down. That's the same break wearing a scarier label — the box is up, the control plane just can't reach it the one way that check uses.
Why everything else looks healthy
Because it is healthy. The channel's address is host-local, so it traverses the host's
filter/INPUT chain, where a default-deny firewall applies. Published container ports (:4000,
:3001, :80, :443) are DNAT'd and never reach that chain. Every check that goes through a published
port passes; only this one hangs.
Repair — it depends how you installed
The two Compose install shapes look nearly identical on the box and have different repairs. Applied to the wrong one, the first does nothing and the second can cost you every stored environment variable. Check which you have:
ls ~/.openship/compose/docker-compose.yml # exists → the CLI installed this stackInstalled with openship up
One command — the same one the error names:
openship upIt generates and authorizes the key, writes the environment variables, recreates the API container so it
reads them, and then dials the host from inside that container and prints what came back. If the
probe reports a dropped connection it offers to add the firewall rule for you (or takes
--open-host-firewall for a scripted run). Then verify.
The run is idempotent: your secrets, database volume and certificates are carried forward, and a channel that already works is re-provisioned to the same key.
openship update does this too — but only when there's a newer release
openship update regenerates the stack from the new template and re-provisions the channel the same
way, then re-probes it. That makes it the better command when you're also behind a version — an update
is exactly when a working channel silently stops working, because the recreated API container can land
in a different docker subnet than your firewall rule allows. But when you're already on the latest
release it exits at "Already on the latest version" without touching the stack, so on its own it is
not a repair. Use openship up.
Installed from the raw docker/docker-compose.yml
Do not run openship up on this box
Both install shapes pin the Compose project name openship, so openship up here doesn't create a
second stack — it adopts yours, volumes and all, while generating a fresh
~/.openship/compose/.env. Current releases stop before writing anything ("Refusing to continue: this
would regenerate secrets your existing install already has"), which is the outcome you want. Releases
without that guard minted a new BETTER_AUTH_SECRET — the key your stored environment variables are
encrypted with — making every one of them permanently unreadable. openship update is not the
answer either: with no ~/.openship/compose stack to reconcile it updates the CLI and leaves your
containers alone.
Provision the channel by hand instead. It's five steps, and the environment variables are the last
one — setting them alone gets you Cannot read host SSH key or a hung handshake.
Generate a key on the host (not inside a container):
sudo mkdir -p /var/lib/openship/host-ssh
sudo ssh-keygen -t ed25519 -N '' -C openship-host-executor \
-f /var/lib/openship/host-ssh/id_ed25519Authorize it for root, restricted to the private ranges the docker bridge dials from. It has to be
root: the paths host operations touch are root-owned, so a lesser user yields a channel that connects
and then can't do the job.
sudo mkdir -p /root/.ssh && sudo chmod 700 /root/.ssh
printf 'from="172.16.0.0/12,192.168.0.0/16,10.0.0.0/8,127.0.0.1",restrict,pty %s\n' \
"$(sudo cat /var/lib/openship/host-ssh/id_ed25519.pub)" \
| sudo tee -a /root/.ssh/authorized_keys
sudo chmod 600 /root/.ssh/authorized_keysrestrict denies port forwarding, agent forwarding and X11; pty is added back because the host
terminal needs one. Without from=, that key is a general-purpose root login from anywhere sshd
accepts — on a VPS, the whole internet.
Satisfy the two prerequisites — sshd reachable from containers, and the host firewall open for them.
They're below; on a box running ufw the firewall is
the step that's actually missing.
Set the variables in the .env you pass with --env-file:
OPENSHIP_HOST_SSH_HOST=host.docker.internal
OPENSHIP_HOST_SSH_USER=root
OPENSHIP_HOST_SSH_PORT=22
OPENSHIP_HOST_SSH_KEY=/run/secrets/openship_host_key
OPENSHIP_HOST_KEY_PATH=/var/lib/openship/host-ssh/id_ed25519The last two are a pair: OPENSHIP_HOST_KEY_PATH is the key's path on the host, which
docker/docker-compose.yml mounts at OPENSHIP_HOST_SSH_KEY inside the API container — so no compose
file needs editing. It must be an absolute path; a relative one resolves against docker/, not the
directory you run docker compose from. Left unset, /dev/null is mounted in its place, which is what
lets the stack start on a box with no key at all.
Recreate the API container. env_file: is read when a container is created, so a restart changes
nothing and the fix appears to have done nothing:
docker compose --env-file .env -f docker/docker-compose.yml \
up -d --force-recreate --no-deps apiVerify it worked
The repair happens on the host and the fault shows up in a container, so "the command succeeded" and "the channel works" are genuinely different questions. Ask the second one:
openship doctorThe Host control row dials the host from inside the API container: reachable at host.docker.internal:22 is the pass. Anything else names the cause — a dropped connection (firewall), a
refusal (sshd), or a name that didn't resolve.
On a raw docker compose install, doctor reports less
The dial above needs the CLI's own stack at ~/.openship/compose, which a hand-run
docker/docker-compose.yml install doesn't have. There, the Host control row falls back to what the
API says about its own environment over the loopback API: never provisioned — run openship up (read
the warning above before you do), reachable from the api container, or the fault by name. It cannot
name an address, because nothing dialed one.
For the address and the exact firewall rule, use the API's boot log — it diagnoses the channel on every start:
docker compose --env-file .env -f docker/docker-compose.yml logs api | grep 'HOST CONTROL'No output is the pass; the banner is only printed when something is wrong. Or exercise the channel directly — open the server's Terminal tab in the dashboard, which is this channel and nothing else.
The two prerequisites that break this silently
Either of these leaves a channel that is fully configured and completely dead, which is why the error says "not configured" long before anyone suspects the host.
sshd must be running, and listening where containers can reach it
The channel is real SSH to the host's own daemon. A minimal cloud image or a container-optimized OS may not run one, in which case there's nothing to connect to:
sudo systemctl enable --now sshAlso check that ListenAddress in /etc/ssh/sshd_config isn't pinned to 127.0.0.1. That looks like
sensible hardening and is invisible to every other test, because everything else reaches this box
through a published port. A connection refused (rather than a hang) is this one.
The host firewall must allow container→host on the SSH port
This is the common cause. Traffic from the API container to host-gateway is host-local, so it goes
through filter/INPUT — where ufw's default deny sits. Published container ports are DNAT'd and skip
ufw entirely, so nothing else on the box notices. The connection doesn't fail, it hangs, which looks
exactly like a bad key.
Allow the docker bridge subnets the API container dials from:
# ufw
sudo ufw allow from 172.16.0.0/12 to any port 22 proto tcp
# firewalld
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.0.0/12" port port="22" protocol="tcp" accept'
sudo firewall-cmd --reload172.16.0.0/12 is Docker's default address pool — wider than one bridge, and correct for any of them.
A raw iptables rule works too but doesn't survive a reboot on its own; persist it with
iptables-persistent or your distro's equivalent. On a CLI install you don't write any of this
yourself: openship up and openship update read the API container's actual networks, print the
narrower rule, and offer to apply it (or take --open-host-firewall for a scripted run).
The rule is in place and it still doesn't work
Something ahead of ufw/firewalld is dropping it — check Docker's own DOCKER-USER chain, and any
cloud provider firewall or security group. If the failure changed shape when you added the rule (a
hang became a refusal), the rule did its job and you're now on the sshd problem above.
Turning the channel off on purpose
A box that only manages remote servers doesn't need a host channel. Switching it off is a hardening decision, not a repair — everything in the blocked list above stays blocked:
openship up --no-host-controlThat deletes the private key and revokes its authorized_keys line, so the box's attack surface
shrinks rather than just this install's behaviour. On a raw docker compose install the equivalent is
OPENSHIP_HOST_CONTROL=false in .env (plus removing the key and its authorized line yourself). Host
operations then refuse with a message naming the choice instead of the fault, and this box stops being
offered as a deploy target.
Related
Installation
Where the channel comes from at install time, and which posture suits your box.
Servers & SSH
The other SSH — connecting to a remote server you added yourself.
Deployment & build failures
A deploy that went red for a reason that isn't the host channel.
Domains, DNS & SSL
The :80/:443 takeover this channel performs, and certificates that won't issue.
Servers & SSH
Fix a server that won't connect over SSH, a component that won't install or reports unhealthy, missing Docker, and port-forward tunnels that won't start.
Desktop app & local data
Fix desktop and self-hosted startup problems — the database lock, "can't reach the server", a stuck update — and find where your local data lives and how to recover it.