Manifest reference
The canonical manifest reference is generated from apps/devhost/devhost.example.toml.
# devhost example manifest## This file is meant to be read as documentation first and copied second.# Rename it to `devhost.toml` in your project root when you actually want to use it.# Saving this file or its includes hot-reloads service configuration. Invalid edits# leave the current stack running. Include glob additions and removals are observed.# Changes to name, killZombies, caddy, devtools, annotation, or worktrees.enabled# require restarting devhost; an edit containing those changes is rejected in full.# Single-service restart keeps its assigned automatic port. If that port is occupied,# use "Restart stack with new ports" in Services or recovery to restart all managed# services with fresh auto ports and rebuilt port references. Fixed ports are retained.## Top-level sections:# - `name` (required)# - `killZombies` (optional; boolean to automatically kill zombie processes from the same manifest, default `true`)# - `includes` (optional; string or string array of file glob patterns to merge multiple sub-manifests)# - `[groups]` (optional; named selections of services for `devhost start <name>`)# - `[worktrees]` (optional; Git checkout selection for repository service groups, enabled by default)# - `[caddy.global]` (optional)# - `[devtools]` (optional)# - `[devtools.browser]` (optional; explicitly configured native browser control)# - `[devtools.editor]` (optional)# - `[devtools.externalToolbars]` (optional)# - `[devtools.minimap]` (optional)# - `[devtools.resources]` (optional; host CPU, memory, and disk readouts, enabled by default)# - `[devtools.status]` (optional)# - `[devtools.shortcuts]` (optional)# - `[annotation]` and `[[annotation.actions]]` (optional; annotation actions are unavailable when omitted)# - `[services.<name>]` (required; at least one service)## Hard validation rules:# - service names must match `^[a-z][a-z0-9-]*$`# - every service must define either `port` or `health`# - `alwaysStart = false` by default; `alwaysStart = true` starts the service on every `devhost start`, including one that names other services# - `managed = true` by default; `managed = false` means devhost must not start or restart that service# - daemon lifecycle services use `[services.<name>.lifecycle]` with `mode = "daemon"`# - daemon lifecycle services must keep `managed = true`, must omit top-level `command`, and must define `lifecycle.start` and `lifecycle.stop`# - daemon lifecycle services must not use `port = "auto"` or `health.process`# - a routed service (`host = ...`) must also define `port`# - a routed service must not use `health.process`# - a health table selects at most one of `tcp`, `http`, or `process`; timing-only tables require a service `port`# - `managed = false` must omit `command`, `injectPort`, and `port = "auto"`# - `managed = false` may use explicit TCP or HTTP health, but must not use `health.process`# - `bindHost` must be one of: `127.0.0.1`, `0.0.0.0`, `::1`, `::`# - `command` is best written as a string array; string form is supported but split on whitespace# - fixed numeric bind ports are reserved across live devhost processes before spawn# - hostname ownership is exclusive across projects, but one manifest may mount multiple services under the same host on distinct paths# - annotation action ids must match `^[a-z][a-z0-9-]*$` and be unique# - string values support environment-variable interpolation with `{{ env.NAME }}` placeholders# - placeholder names must start with a letter or underscore and then use letters, digits, or underscores# - other text stays literal# - malformed `{{ ... }}` text stays literal, and an unterminated `{{` keeps the rest of that string literal# - referencing an undefined valid placeholder is a manifest read error# - defined placeholders may expand to the empty string
# `name` identifies the stack in logs, state, and injected metadata.# You can interpolate environment variables in string values, for example:# `name = "{{ env.USER }}-stack"` or `host = "devhost-{{ env.USER }}.localhost"`.name = "hello-stack"
# `killZombies` controls whether devhost automatically kills and reclaims zombie# processes claiming the same hosts or ports from the same manifest path.# - `true` (default): find and kill any active devhost child or main process from the same manifest, log a message, and succeed.# - `false`: abort and report the claim conflict as an error.# killZombies = true
# `includes` lists file paths or glob patterns (relative to this manifest's directory)# of sub-manifests to load and merge. This is perfect for monorepos where each sub-application# maintains its own services.# - Included manifests' services, groups, and annotation actions are recursively merged.# - Service names must be unique across all merged manifests.# - Relative paths (like `cwd` and `watch` folders) in included manifests are automatically# rewritten to be relative to the root manifest's directory.# - If `cwd` is missing in an included service, it defaults to the directory of the manifest# it was defined in.# - Cyclic references are safely and gracefully ignored.# - The merged stack holds every included service. `devhost start web api` starts one slice of it:# the named services, their `dependsOn` chains, and the `alwaysStart` services (see `services.db`).## includes = ["packages/*/devhost.toml", "apps/*/devhost.toml"]
# Define groups here or in each app's included devhost.toml.# `devhost start app` starts web, its dependsOn chain, and alwaysStart services.# Mix group and service names: `devhost start app worker`. Shared services start once.# Group names follow service naming rules: lowercase letter, then lowercase letters,# digits, or hyphens. Names must be unique across includes and distinct from services.# Each group is a non-empty array of service names from the complete merged manifest;# nested groups and unknown services are rejected. Groups select services; dependsOn# defines their dependencies. Plain `devhost start` still starts every service.# Group edits hot-reload: selected groups use their current members. Removing a# selected group removes its selection; alwaysStart and other requested names remain.[groups]app = ["web"]
# Managed Caddy lifecycle is shared and manual.# Start it explicitly with `devhost caddy start` before running one or more stacks.# Pass `--manifest ./devhost.toml` to use this manifest's admin address, bind host,# HTTP enablement, and listener ports when generating the shared proxy configuration.# Active stack registrations take precedence for the admin address, bind host, and ports.# All stacks share the same managed Caddy instance and the same global 404 hostname list.# Stacks retain the resolved management binding for updates and retirement. Default/sparse# siblings inherit non-default bindings while active; explicit non-default conflicts fail.# Final retirement keeps the captured listener settings and leaves Caddy running.# Use matching custom settings when a manifest may start with no active stacks; an empty# runtime does not discover a custom endpoint from a different manifest.
# The `devtools.*` sections control the browser-side devhost overlay injected into# routed HTML document navigations. If all devtools features are disabled, apps# load without the overlay. Startup and recovery pages remain available.
# The repository worktree picker is enabled by default and requires Git on PATH.# Set enabled = false to disable discovery and saved-selection restore.# [worktrees]# enabled = false# Choices are remembered per manifest path and repository across devhost restarts.# Open the picker or press Refresh to discover added and deleted worktrees.# If the running checkout disappears, Refresh stops its services for explicit recovery.# All services in a repository share one checkout, preserving their cwd offsets.# Missing saved checkouts and failed launches keep the group stopped for recovery.# Repositories containing managed = false services cannot switch.# Keep configured cwd values in one checkout per repository. The original manifest# remains authoritative; selecting a checkout does not load another manifest.
[caddy.global]# `http` also serves the same routed hosts and the shared fallback page on plain HTTP.# - `false` (default): serve through HTTPS only.# - `true`: also serve the same routes through HTTP on `httpPort`.# `httpPort` selects the managed Caddy HTTP listener port.# - `80` (default): standard privileged HTTP.# - any other integer from `1` to `65535`: alternate HTTP listener port.# `httpsPort` selects the managed Caddy HTTPS listener port.# - `443` (default): standard privileged HTTPS.# - any other integer from `1` to `65535`: alternate HTTPS listener port.# `bindHost` controls which interfaces the managed Caddy HTTP/HTTPS listeners bind.# - `"127.0.0.1"` (default): loopback only.# - `"0.0.0.0"`: expose the managed Caddy front door on all IPv4/IPv6 interfaces.# - `"::1"`: IPv6 loopback only.# - `"::"`: expose the managed Caddy front door on all IPv6 interfaces.# `adminAddress` controls which loopback address/port the managed Caddy admin API listens on.# - `"127.0.0.1:20197"` (default): the default devhost-managed admin endpoint.# - any other host:port string Caddy accepts: alternate admin endpoint.## Important: this is a shared managed-Caddy setting, not an isolated per-stack listener.# If any active stack enables `caddy.global.http = true`, the shared Caddy instance serves# HTTP for all active stacks. Retiring the last true voter disables HTTP while false/default# sibling registrations remain. Final-registration retirement retains the stack's captured# HTTP setting; stop shared Caddy manually with a matching manifest when done.# Active stacks must also agree on any non-default `adminAddress`, `bindHost`, `httpPort`, and `httpsPort` values.adminAddress = "127.0.0.1:20197"bindHost = "127.0.0.1"httpPort = 80http = falsehttpsPort = 443
[devtools.editor]# `enabled` controls whether editor integration is available in the injected overlay.# - `true` (default): enable Alt + right-click component-source navigation when source metadata is available.# - `false`: disable editor integration.enabled = true# `ide` selects which editor target devhost opens for component-source navigation.# - `"vscode"` (default): open Visual Studio Code via its browser URL handler.# - `"vscode-insiders"`: open VS Code Insiders via its browser URL handler.# - `"cursor"`: open Cursor via its browser URL handler.# - `"webstorm"`: open WebStorm via its browser URL handler.# - `"neovim"`: open Neovim inside devhost's embedded terminal UI; requires `nvim` and `curl` on PATH.# Devhost loads its bundled Neovim plugin for that terminal session so cursor movement in TSX/JSX buffers can# highlight the matching React component in the routed browser page for the same devhost instance.# While the stack runs, `.tmp/devhost/<stack-name>/nvim-shell/bin/devhost-nvim` launches Neovim from a shell with# the same token-free instance cursor endpoint, project root, and plugin path.ide = "vscode"
[devtools.browser]# Native React DevTools uses an already running dedicated browser/profile owned by you.# Install the original React Developer Tools extension before loading your React app.# Tested native versions: Chrome for Testing 154.0.8037.92, extension 8.0.0,# and a mounted React DOM 19.2.5 Element/ShadowRoot host; other combinations need verification.# Native tests cover live host Components/Profiler and preserved-window connection lifetimes.# Empty endpoint (default) disables native browser access and starts no browser connection.# Set an explicit literal-loopback HTTP discovery endpoint with a nonzero port,# or a complete ws:// loopback /devtools/browser/<id> URL. DNS, remote addresses,# credentials, redirects, query/fragment parameters, and page WebSockets are rejected.# devhost never launches a browser, scans profiles, or closes your native windows.# Through the routed page, choose Connect browser control directly in the dock, then React DevTools.# The same button reads Disconnect browser control while connecting or connected.# Setup/status/observations/window/loss/error information is visible in the dock; errors have Copy.# Collapse hides the command and full readout but retains the connection and compact state cue.# Select Components or Profiler in the genuine native window yourself.# Disconnect releases devhost's connections; it preserves your native window and browser.# Controller connection/window presence does not certify live inspection or recovery.# Native window closure/replacement withholds React access; recovery remains unverified.# See https://alexgorbatchev.github.io/devhost/architecture/external-devtools/#native-react-devtoolsendpoint = ""# Official Chrome Web Store ID. Set the actual ID only for a verified original unpacked extension.reactExtensionId = "fmkadmapgofadopljbjfkapdkoienihi"
[devtools.externalToolbars]# `enabled` controls whether devhost aggregates detected third-party devtools launcher buttons.# - `true` (default): hide supported native launcher buttons and re-render them inside the devhost overlay.# - `false`: leave third-party launcher buttons untouched.# Supported tools: host-mounted TanStack Query, TanStack Router, the unified TanStack shell, React Hook Form, Jotai, and Vue inspectors.# For the unified shell, mount @tanstack/react-devtools 0.10.13 with native Form/Table/Pacer plugins.# One TanStack entry controls attached shells sharing upstream state; detached windows use native controls.# This integration targets development runtimes; see the setup guide for plugin/version boundaries.# For React Hook Form, mount @hookform/devtools DevTool with each form's control in your app.# For Jotai, mount jotai-devtools DevTools with each store; tested: 0.14.0 with jotai 2.20.3.# Import jotai-devtools before creating custom stores; its panel owns atoms and history.# devhost supplies separate Form N and Jotai N launchers; the host owns forms, stores, and native panels.# For Vue, install vite-plugin-vue-devtools 9.0.0-beta.1 in a Vite 8.3.3 development host# with @vitejs/devtools 0.7.6 and devtools.apply = "serve"; authorize its native dock first.# The Vue entry requires a mounted Vue app and visible native host; its inspector stays upstream-owned.# This host setup does not require upgrading an unrelated Vite 7 project; other Vue releases need verification.# For Redux Toolkit and native-middleware Zustand, opt into /__devhost__/redux.js on the routed development page.# Toolkit requires one public instrument EnhancedStore; Zustand requires actual StoreApi plus snapshot/restore.# The Redux entry opens the upstream browser inspector; disabling closes its owned monitor/subscriptions only.# Inspector modules/styles are content-versioned in compiled builds; its stylesheet stays in the popup document.# Configured native React access shares this enabled gate; disabling releases only devhost browser control.# Tested versions, setup, and panel identification: https://alexgorbatchev.github.io/devhost/architecture/external-devtools/enabled = true
[devtools.minimap]# `enabled` controls whether the injected log minimap UI is shown.# - `true` (default): show the log minimap in the overlay.# - `false`: hide the log minimap.enabled = true
[devtools.resources]# Host CPU, memory, and disk usage in the devtools toolbar, each as percent used. A readout# turns amber from 70% and red from 90%; hover one for the absolute figures.# CPU is the share of all logical CPUs that was busy since the previous reading. Disk is the# space used across fixed local filesystems: on Linux, USB-attached and removable drives are# left out; on macOS, the startup disk is reported.# `enabled` controls whether any readout is shown.# - `true` (default): show the readouts.# - `false`: hide them; devhost samples nothing.enabled = true# `pollInterval` sets how often every readout is read, as a duration string of at least "250ms".# - omitted (default): CPU and memory are read every "2s", disk every "1m".# - string (e.g. "5s"): read all three at this rate, unless a readout sets its own below.# pollInterval = "5s"
[devtools.resources.cpu]# `enabled` shows (`true`, default) or hides (`false`) the CPU readout.enabled = true# `pollInterval` overrides the shared rate for the CPU readout.# pollInterval = "1s"
[devtools.resources.memory]# `enabled` shows (`true`, default) or hides (`false`) the memory readout.enabled = true# `pollInterval` overrides the shared rate for the memory readout.# pollInterval = "2s"
[devtools.resources.disk]# `enabled` shows (`true`, default) or hides (`false`) the disk readout.enabled = true# `pollInterval` overrides the shared rate for the disk readout.# pollInterval = "1m"
[devtools.status]# Exited foreground services open a full-screen log view with a restart button.# devhost and sibling services keep running after service exits, including exit code 0.# `enabled` controls whether service status is shown in the devtools toolbar.# - `true` (default): show service state and the services panel in the toolbar.# - `false`: hide service status.enabled = true# `position` selects the corner the devtools toolbar is anchored to.# - `"top-right"`: anchor the toolbar to the top-right corner; its panels open below it.# - `"bottom-right"` (default): anchor the toolbar to the bottom-right corner; its panels open above it.position = "bottom-right"
[devtools.shortcuts]# `restartServices` defines a global keyboard shortcut to trigger a parallel, topological restart# of all dirty (modified) services in the browser.# - string (e.g. "alt+ctrl+r"): the keyboard shortcut.# Supported modifiers: "ctrl", "alt", "shift", "meta", combined with "+" and any physical layout-independent key code (like "r", "enter", "escape").# Syntaxes must be separated by "+", for example: "ctrl+shift+x".# If invalid syntax is provided or if omitted, it defaults back to "alt+ctrl+r".restartServices = "alt+ctrl+r"
# `idleTimeout` sets the automatic idle shutdown duration (e.g. 30s, 1m, 1h).# If the daemon receives no routed Caddy traffic and has no persistent browser WebSocket# connections or active interactive terminal sessions for this duration, it automatically# stops all started services and cleanly shuts down the stack.# - string (e.g. "1h"): enable auto-shutdown after the given duration.# - empty/negative/0 (default): disable auto-shutdown.# idleTimeout = "1h"
# Annotation actions are configured only through `[annotation]`.# If `[annotation]` is present, `[[annotation.actions]]` must contain at least one action.# If `[annotation]` is omitted, annotation submission is unavailable in the injected UI.## [annotation]# defaultAction = "fix"# Optional parent for annotation JSON, prompts, and agent support files for all actions.# Omit to use the system temp directory ($TMPDIR on Unix, otherwise /tmp).# Relative paths resolve against devhost's startup cwd, not the manifest or action cwd.# Absolute paths are accepted; empty strings are invalid. Configure in the root manifest.# Missing parents are created; failures are reported without a fallback.# Each session gets a unique private subdirectory removed during session cleanup.# The parent directory and durable queue storage are unaffected by cleanup.# tempDir = ".tmp/annotations"## [[annotation.actions]]# id = "fix"# kind = "agent"## [annotation.actions.agent]# adapter = "claude-code"# args = ["--thinking", "high"]## Agent actions support the built-in adapters `"pi"`, `"claude-code"`, `"opencode"`, and `"codex"`.# Built-in adapters accept an optional `args` string array to pass extra CLI flags (e.g. model,# thinking level, or permission modes) while preserving built-in status reporting and prompt handoff.# Custom agent actions omit `adapter` and use `command`, optional `cwd`, and optional `env`# inside `[annotation.actions.agent]` (`args` is valid only with `adapter`).# `label` names an action in the annotation composer. Built-in adapters default it to their name:# "Pi", "Claude Code", "OpenCode", or "Codex". Set `label` to tell apart actions that share an adapter.# Custom agent actions and command actions require `label`.## Codex example (requires the Codex CLI on PATH with --no-daemon and lifecycle hooks):# [[annotation.actions]]# id = "fix-codex"# kind = "agent"## [annotation.actions.agent]# adapter = "codex"# args = ["-c", "model_reasoning_effort=high"]## Codex runs interactively with session-local status hooks. Use /hooks in its terminal to# review and trust the devhost hooks; queues cannot drain until those hooks are trusted.# Codex sandbox, approval, authentication, and syntax theme settings remain user-controlled.## [[annotation.actions]]# id = "jira"# label = "Create Jira Ticket"# kind = "command"## [annotation.actions.command]# command = ["bun", "./scripts/mock-jira-ticket.ts"]# cwd = "."## [annotation.actions.command.env]# JIRA_BASE_URL = "https://example.atlassian.net"# JIRA_PROJECT_KEY = "WEB"## Command actions run directly in a devhost terminal with annotation context files exposed via:# - `DEVHOST_ANNOTATION_ACTION_ID`# - `DEVHOST_ANNOTATION_ACTION_KIND`# - `DEVHOST_ANNOTATION_ACTION_LABEL`# - `DEVHOST_ANNOTATION_FILE`# - `DEVHOST_ANNOTATION_PROMPT_FILE`# - `DEVHOST_ANNOTATION_TRANSPORT=files`# - `DEVHOST_PROJECT_ROOT`# - `DEVHOST_STACK_NAME`## A Jira ticket integration should be modeled as a `kind = "command"` action like the# example above. `devhost` does not provide a built-in Jira adapter; your script or CLI# should read `DEVHOST_ANNOTATION_FILE` or `DEVHOST_ANNOTATION_PROMPT_FILE` and create# the ticket itself.## Durable annotation queues are supported for agent actions only.
# -----------------------------------------------------------------------------# services# -----------------------------------------------------------------------------# The examples below document service fields in place instead of listing detached# option tables up here. Read them top to bottom:# - `services.web` shows a primary routed service with a fixed port# - `services.api.health` shows explicit HTTP health# - `services.preview` shows an externally managed routed service# - `services.mailpit` shows a managed daemon lifecycle service# - `services.cache.health` shows explicit TCP health# - `services.db` shows `port = "auto"` and `alwaysStart`# - `services.worker.health` shows process health## Health defaults still matter even when no explicit `[services.<name>.health]`# table is present:# - fixed numeric `port` + no `health` => implicit TCP health check on `bindHost:port`# - `port = "auto"` + no probe kind => TCP health check on the resolved port, with optional health timing overrides# - health HTTP templates resolve after every service port is assigned# - `{{ services.<name>.host }}` names the first public routing host, falling back to bindHost# - `{{ services.<name>.url }}` builds the direct HTTP base URL, mapping wildcard hosts to loopback and bracketing IPv6
[services.web]# `primary` marks this as the default service for stack-level behavior.# - `true`: make this the primary service.# - `false` (default): do not mark this service as primary.primary = true# `managed` controls whether devhost owns the service process.# - `true` (default): devhost starts, stops, and can restart the child process.# - `false`: devhost only claims the route/port and health-checks the already-running service.# Daemon lifecycle services still use `managed = true`; they switch ownership mode with `[services.<name>.lifecycle]`.# managed = false# `command` defines the child process command line.# - string array (recommended): preserves argument boundaries exactly.# - string: supported, but devhost splits it on whitespace and does not preserve shell quoting.# - commands run directly, not through an implicit shell.# - `command = ["storybook", "dev", "--port", "$PORT"]` passes literal `$PORT`.# - use an explicit shell such as `command = ["sh", "-c", "exec storybook dev --port \"$PORT\""]`# only when you intentionally want shell expansion or shell features.# Omit `command` when `[services.<name>.lifecycle].mode = "daemon"`.command = ["bun", "run", "web:dev"]# command = "bun run web:dev"# `cwd` sets the working directory for the child process.# Absolute paths are used directly and may point outside the manifest directory.# Relative paths resolve against the manifest directory and must stay within it.cwd = "./app"# `port` sets the runtime listening port or requests automatic allocation.# - integer `1..65535`: use the exact port you provide.# - `"auto"`: allocate a best-effort free port before spawn and inject it as `PORT`.# Startup and configuration reload retry clear bind collisions; individual restarts keep the assigned port.# "Restart stack with new ports" reassigns automatic ports and rebuilds references across all managed services.# Automatic allocation is best-effort, without a cross-process global auto-port allocator.port = 3000# `injectPort` controls whether devhost exports `PORT` to the child process.# - `true` (default): inject `PORT` when `port` is set.# - `false`: keep routing and health on the configured port, but do not export `PORT`.# Use this for composite commands that launch multiple child processes and should not all inherit the same port.# - injecting `PORT` does not expand `$PORT` inside `command`; that still requires an explicit shell.# injectPort = false# `bindHost` sets the socket interface the child process should bind to.# - `"127.0.0.1"` (default): IPv4 loopback only.# - `"0.0.0.0"`: all IPv4 interfaces.# - `"::1"`: IPv6 loopback only.# - `"::"`: all IPv6 interfaces.bindHost = "127.0.0.1"# `host` sets one public routed hostname or a non-empty array of unique hostnames.# Every hostname routes to the same service process, port, and path.# The first hostname is used for `DEVHOST_HOST`, `{{ services.<name>.host }}`,# and the devtools service link. Startup logs list every routed URL.host = "hello.local.test"# host = ["hello.local.test", "alias.local.test"]# `proxyLocalOrigin` translates Host and matching public Origin to the local backend.# Enable for dev servers such as Bun HTML/HMR that reject public hostnames.# The local address uses this service's bindHost and resolved port.# Wildcard bindHost values use loopback: 0.0.0.0 -> 127.0.0.1; :: -> ::1.# The app must listen there: honor injected PORT for auto, or match a fixed port.# Defaults to false; requires host. Requests without Origin keep it absent.# Foreign, opaque, and duplicate origins receive 403. X-Forwarded-Host stays public.# Applies to documents, assets, subpaths, and WebSockets; follows automatic ports.# proxyLocalOrigin = true# `path` sets a subpath for mounting (e.g. "/api/*"). Defaults to `"/"`.# Use same-host composition only within one manifest, for example:# - `services.web.host = "hello.local.test"`, `services.web.path = "/"`# - `services.api.host = "hello.local.test"`, `services.api.path = "/api/*"`# path = "/api/*"# `dependsOn` declares services that must start before this service.# `devhost start web` starts this service and everything it reaches through `dependsOn`.dependsOn = ["api"]# `watch` lists paths to watch for file changes; relative paths use the service cwd.# Relative watches follow a selected worktree; absolute watch paths remain unchanged.# - string array (e.g. ["src/"]): when files change, the service is marked `changed` (dirty) in the devtools toolbar.# - All folders are watched recursively. Heavy paths (e.g., node_modules, .git, .workspaces, .tmp) are automatically ignored.# - Rapid concurrent modifications are debounced by 200ms automatically.watch = ["src/"]# No explicit `[services.web.health]` block is used here.# Because `port = 3000`, devhost will apply the default TCP health check for `127.0.0.1:3000`.
[services.web.env]# `env` adds app-specific environment variables for this managed service.NODE_ENV = "development"PUBLIC_API_ORIGIN = "https://api.hello.local.test"
[services.api]# A routed backend with explicit HTTP health and IPv6 loopback binding.# This example uses its own hostname, but it could also share `hello.local.test`# under a distinct path such as `path = "/api/*"`.## Docker-backed variant:# - publish the container port onto the host, for example Compose `ports: ["4000:4000"]`# - keep `port = 4000` here because devhost routes to the host-visible port# - replace the command with `command = ["docker", "compose", "up", "--build", "api"]`# - keep HTTP health checks on `127.0.0.1`, `localhost`, or `::1`; Docker-internal names such as `api` are not valid herecommand = ["bun", "run", "api:dev"]cwd = "./api"port = 4000bindHost = "::1"host = "api.hello.local.test"dependsOn = ["db"]
[services.api.env]LOG_LEVEL = "debug"# The db service binds IPv6 wildcard; connect through bracketed IPv6 loopback.DATABASE_URL = "postgres://postgres:postgres@[::1]:{{ services.db.port }}/mydb"
[services.api.health]# `http` defines a readiness URL or a single-leading-slash path on this service.# Absolute http/https URLs target `127.0.0.1`, `localhost`, or `::1`.# Shorthand uses the assigned port and connectable bind host, including IPv6 brackets.# Equivalent template: `http = "{{ services.api.url }}/healthz"`.# Full construction also works: `http = "http://[::1]:{{ services.api.port }}/healthz"`.# The `host` template property names the public proxy; use `url` for direct health probes.http = "/healthz"# `interval` sets the delay between HTTP health-check attempts.# - integer `>= 1`; `200` is the default.interval = 500# `timeout` sets the maximum total wait time for this health check.# - integer `>= 1`; `30000` is the default.timeout = 5000# `retries` limits retries after the first failed attempt.# - integer `>= 0`; `0` (the default) disables the cap, so only `timeout`# bounds the total wait. A positive N permits N retries after the first# failure, giving up after N + 1 consecutive failures or the timeout.retries = 20
[services.preview]# An externally managed routed service.# Use this when another tool or wrapper command owns the process lifecycle and later opens the port.managed = falsedependsOn = ["api"]port = 4100host = "preview.hello.local.test"# Fixed-port unmanaged services still get the default TCP health check on `127.0.0.1:4100`.# Add an explicit `[services.preview.health]` block if you want custom TCP or HTTP health instead.
[services.mailpit]# A managed daemon lifecycle service.# Use this when devhost should own start/stop, but the service backgrounds itself instead of staying as one foreground child.port = 8025host = "mail.hello.local.test"
[services.mailpit.lifecycle]# `mode = "daemon"` switches from the default foreground `command` model to explicit lifecycle commands.mode = "daemon"# `start` launches or enables the daemonized service.start = ["./scripts/mailpit-devctl", "start"]# `status` is optional. When it exits `0`, devhost treats the daemon as already running and skips `start`.status = ["./scripts/mailpit-devctl", "status"]# `stop` shuts the daemon down during stack shutdown, cleanup, and service restart.stop = ["./scripts/mailpit-devctl", "stop"]
[services.mailpit.health]# Daemon lifecycle services still need normal TCP or HTTP health checks.http = "http://127.0.0.1:8025/api/v1/info"
[services.cache]# A non-routed service using explicit TCP health instead of `port`.command = ["bun", "run", "cache:dev"]cwd = "./cache"bindHost = "0.0.0.0"dependsOn = ["db"]
[services.cache.health]# `tcp` defines the port used by the TCP health check.# - integer `1..65535`.tcp = 6379# `interval` sets the delay between TCP health-check attempts.# - integer `>= 1`; `200` is the default.interval = 250# `timeout` sets the maximum total wait time for this health check.# - integer `>= 1`; `30000` is the default.timeout = 3000# `retries` limits retries after the first failed attempt.# - integer `>= 0`; `0` (the default) disables the cap, so only `timeout`# bounds the total wait. A positive N permits N retries after the first# failure, giving up after N + 1 consecutive failures or the timeout.retries = 10
[services.db]# `alwaysStart` controls whether the service starts when `devhost start` names other services.# Naming services starts one slice of a large stack, such as the one app of a monorepo you are# working on, without launching the rest. Set `alwaysStart = true` on a service every slice# needs, such as this database, so it starts whichever services are named.# - `false` (default): the service starts when it is named, when a started service depends on it,# or when `devhost start` names no service at all.# - `true`: the service and everything it reaches through `dependsOn` start on every `devhost start`.# `devhost start web` starts web, its `dependsOn` chain, and every `alwaysStart` service. The# services left out hold no process, route, hostname, or port; start one later from# "Stopped services" in the devtools Services panel, which also starts what it depends on.# `devhost service list` prints every service name; `--startable` leaves out the `alwaysStart` ones.alwaysStart = true# `port = "auto"` allocates a free port; health timing and explicit probes are supported.command = ["bun", "run", "db:dev"]cwd = "./db"port = "auto"bindHost = "::"
[services.db.health]# Keep the implicit TCP probe on the assigned port; allow up to 60 seconds for startup.timeout = 60000
[services.worker]# A background process with process-based health.# This is valid only for non-routed services.command = ["bun", "run", "worker:dev"]cwd = "./worker"dependsOn = ["api"]
[services.worker.health]# `process` treats the service as healthy while the child process remains alive.# - `true`: enable process-based health checks.process = true# `interval` sets the delay between process-health checks.# - integer `>= 1`; `200` is the default.interval = 1000# `timeout` sets the maximum total wait time for this health check.# - integer `>= 1`; `30000` is the default.timeout = 1000# `retries` limits retries after the first failed attempt.# - integer `>= 0`; `0` (the default) disables the cap, so only `timeout`# bounds the total wait. A positive N permits N retries after the first# failure, giving up after N + 1 consecutive failures or the timeout.retries = 0