Skip to content

Troubleshooting

Each section starts from what you see and ends with the fix.

configuration reload rejected means a saved edit to devhost.toml or an included manifest could not apply. The stack keeps running with the last accepted manifest, and pages with the toolbar show the reason under it.

A syntax error names the file, line, and column; any other problem starts with the setting it concerns, such as services.api.port. Fix it and save again. If services failed to start partway through a reload, devhost restored the previous ones; the logs name anything it could not restore. See Hot reload.

Caddy is not available. means the shared proxy is not running. Start it:

Terminal window
devhost caddy start

On Linux, an error about binding port 443 means Caddy lacks the privilege to use low ports:

Terminal window
devhost caddy privileged-ports # may ask for your password
devhost caddy start

Run privileged-ports again whenever the Caddy binary is replaced. For Caddy’s own messages, read <state>/caddy/caddy.runtime.log, or caddy.stdout.log and caddy.stderr.log beside it for the last start; AGENT=1 also prints the underlying connection error. See Files and directories.

Browsers reject devhost HTTPS certificates

Section titled “Browsers reject devhost HTTPS certificates”

managed caddy local CA is not trusted, so browsers will reject devhost HTTPS certificates. means your system does not trust Caddy’s certificate authority yet: you declined the password prompt, the installation failed, or devhost could not ask (no terminal, or AGENT=1). Install it from a terminal:

Terminal window
devhost caddy trust

A phone or another computer needs the authority too; on a Mac, use devhost caddy trust-remote. See Trust the certificates.

<host> is already claimed by PID <pid> from <manifest>. means another running stack routes that hostname. It may come from another directory, so devhost stop in yours finds nothing. Find and stop it:

Terminal window
devhost stack list
# 3596463 /home/me/projects/app/.tmp/review/devhost.toml
devhost stop --manifest "$(devhost stack list | fzf | cut -f2)"

The PID in the list matches the one in the error. See Stop a stack started elsewhere.

An already running error means another live stack already uses your root manifest or one of its included manifests. A manifest file belongs to one stack at a time, even when the two stacks start different services. The error prints the command that stops the other stack; run it, then start yours. The same conflict rejects a hot reload that adds an include another stack uses.

Stopping zombie PID ... followed by Stopped zombie PID ... means devhost found a process from an earlier run of the same manifest, one that crashed or was killed, still holding a hostname or port, and stopped it. This is on by default (killZombies = true).

  • devhost tries each leftover process once per startup or reload, and prints the pair once even when that process held several hostnames and ports.
  • If the process cannot be killed, or has not exited after five seconds, devhost reports an error and leaves that hostname or port unclaimed.
  • Processes from a different manifest, and stacks that are still running, are never killed; they produce the usual conflict errors above.

To get a conflict error instead of the cleanup, turn it off at the top of the manifest:

killZombies = false

A single-service restart keeps its automatic port. If another process took that port, the restart fails and points to Restart Stack, which assigns new automatic ports. A fixed port cannot be reassigned: stop whatever holds it or change the setting. See Restarts and recovery.

devhost stops that repository’s services and shows a recovery error. Open Worktrees and pick an available checkout. See New and deleted worktrees.

The dev server prints http://localhost:5173 and works there, but https://app.localhost times out or shows another app’s response.

localhost can resolve to IPv6 ::1, while devhost connects to 127.0.0.1 unless the service sets bindHost. The two addresses can reach different listeners, or one can have none. Compare them:

Terminal window
curl -I http://localhost:5173/
curl -I http://127.0.0.1:5173/
curl -g -I 'http://[::1]:5173/'

When the health check fails on one loopback address but the other answers, devhost says so once per start and suggests the services.<name>.bindHost value to use. Check that the listener that answered really is your app, then either:

  • Make the app listen where devhost connects. Set Vite’s server.host and server.port to the manifest’s address and port, and server.strictPort = true so Vite fails instead of moving to another port. See Vite’s server options.

  • Point devhost at the app’s listener, for an app that listens on IPv6 loopback only:

    [services.app]
    command = ["bun", "run", "dev"]
    port = 5173
    bindHost = "::1"
    host = "app.localhost"

bindHost tells devhost where to connect and check health; it does not reconfigure your app. A mismatch ends in a health-check timeout (30 seconds by default) and a recovery page; fix the listener, then press Restart.

Messages such as recipe dev was terminated ... by signal 15 or script "dev" exited with code 143 mean devhost stopped that process. The devhost line just before it says why: a failed health check or a shutdown signal the stack received (Received ...; shutting down the stack.). Without such a line, something else sent the signal, and devhost reports exited with code 143, or 137 for SIGKILL, which often means the system ran out of memory.

A wrapper command that starts, say, a frontend and an API together passes the service’s PORT to every process it starts. One of them takes the routed port, another moves to a fallback, and API requests return 404 while the page looks fine.

Split them into separate services, which is the better fix. To keep the wrapper, stop devhost from setting PORT and configure each process’s port yourself:

[services.app]
command = ["bun", "run", "dev"]
port = 5173
injectPort = false
host = "app.localhost"

When a tunnel or another proxy connects to devhost over plain HTTP, Caddy needs its HTTP listener even if visitors use HTTPS. Set http = true in config.toml; see Proxy ports and listeners.