Troubleshooting

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/:443 from 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 stack

Installed with openship up

One command — the same one the error names:

openship up

It 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_ed25519

Authorize 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_keys

restrict 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_ed25519

The 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 api

Verify 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 doctor

The 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 ssh

Also 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 --reload

172.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-control

That 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.

On this page