Skip to content

configuration

github-actions[bot] edited this page Sep 23, 2026 · 20 revisions

Configuration

devctl is driven by YAML. Unknown fields are rejected. version: 1 is required.

Editors and agents: point at the JSON Schema so field names complete.

# yaml-language-server: $schema=https://raw.githubusercontent.com/amr-m-abdelgawad/devctl/main/schema/devctl.config.schema.json
version: 1

The schema file lives at schema/devctl.config.schema.json. The demo config uses a relative path so it works offline.

Discovery

Walks from the current directory toward the filesystem root:

flowchart TB
  cwd["Current directory"] --> nested{"Has .devctl/config.yaml?"}
  nested -->|yes| root["Repo root = that directory"]
  nested -->|no| single{"Has devctl.yaml?"}
  single -->|yes| root
  single -->|no| top{"Filesystem root?"}
  top -->|no| parent["Walk to parent"]
  parent --> nested
  top -->|yes| miss["No configuration found"]
Loading

--config points at a file or a .devctl directory. Repository root is the directory that contains .devctl (or the parent of devctl.yaml).

When the main file lives in .devctl/, modular files merge in:

flowchart TB
  main[".devctl/config.yaml"] --> services[".devctl/services/*.yaml"]
  main --> profiles[".devctl/profiles/*.yaml"]
  main --> http[".devctl/http/*.yaml"]
  main --> routes[".devctl/proxy/routes.yaml"]
Loading

Service, profile, and HTTP recipe filenames become keys (identity.yaml → service identity, login.yamlhttp.login). Files within each modular directory are loaded in sorted filename order, making overrides deterministic even when both .yaml and .yml fragments resolve to the same key.

Overlays and precedence

Local overlays merge after the repo config. Each stage overrides the one before it, so the rightmost source wins:

flowchart LR
  defaults["Built-in defaults"] --> repo["Repository .devctl"]
  repo --> homeLocal["~/.devctl/config.local.yaml"]
  homeLocal --> repoLocal[".devctl/config.local.yaml"]
  repoLocal --> session[".devctl/overlays/<name>.yaml"]
  session --> env["DEVCTL_* / ENV_SOURCE_ORDER"]
  env --> flags["CLI --config"]
Loading

The repository's own config.local.yaml overrides the one in your home directory, not the other way round: overlays are applied home-first so the repo-specific file gets the last word.

devctl start --overlay <name> then applies .devctl/overlays/<name>.yaml (same keys as config.local.yaml, presence-aware) after both local files so the session layer wins. The name is sticky for the session — omit --overlay on later starts to keep it — and is recorded in state.json (config_overlay) so daemon replacement and devctl reload keep applying it. A missing file fails start and devctl config validate with overlay "X" not found: .devctl/overlays/X.yaml. Operators may commit named overlays; they are not a second config language.

TUI appearance is not this file. Theme, keys, mouse, and MCP listen live in tui.json layers — see Building from source and TUI. Settings can patch web.enabled, web.listen.port, proxy.inspect_max_bytes, and llm.capture_max_bytes into .devctl/config.local.yaml (created if missing; other keys are left alone).

Top-level keys

Key Role
version Must be 1
project.name Shown in the TUI header
google.project_id / region Cloud project (optional)
templates Named service bases (extends)
services Process definitions
tasks Named transient commands run with devctl run
http Named outbound HTTP recipes — see Custom HTTP APIs
profiles Named service sets, overlay binds, and per-service env
proxy Listen address, token endpoint, routes (inspect.enabled captures bodies) — see Proxy
logs In-memory cap and persistence
telemetry.otlp Opt-in loopback OTLP/HTTP+JSON receiver (off by default) — see Telemetry
web Opt-in loopback telemetry web UI (off by default, port 18900) — see Web console
llm Opt-in LLM traffic inspector (off by default) — see LLM inspector
auth.refresh_threshold_seconds Token refresh window (default 300)
shutdown stop_services_on_exit, grace_seconds
ui Optional theme / keymap hints in YAML (TUI prefs still win from tui.json)
secrets Extra redaction markers and regexes
doctor.tools Extra CLI binaries to probe
plugins { path } modules loaded when the supervisor starts
environment.sources / secrets / sops Env source order, named secrets, and an optional SOPS file. A service can also set environment.terraform to read literal env vars from its Terraform — see Environment

Templates

templates:
  python-http:
    health: { type: http, interval_seconds: 2, timeout_seconds: 1 }
    logs: { stdout: true, stderr: true }
    restart: { policy: on_failure, max_retries: 2, backoff_seconds: 1 }

services:
  api:
    extends: python-http
    command: [python3, main.py]

Tasks

Tasks accept command, shell, working_dir, dependencies, and environment. Dependencies name services and are made ready before the one-off command runs. Tasks have no ports, health checks, restart policy, or status entry; see Services for an example.

Validation and reload

devctl config validate
devctl config validate --json
devctl config show
devctl config diff
devctl reload

config diff explains the resolved result instead of merely printing it. Each entry includes the winning source file and layer (main, modular_service, modular_profile, modular_proxy, home_local, repo_local, session_overlay, or synthesized) and the ordered sources it shadowed. Use --json for structured output. config validate|show|diff apply the sticky session overlay (or --overlay <name>).

devctl config diff — each effective value with the file and layer that won

Checks: YAML syntax, required fields, unknown fields, service references, dependency conditions and cycles, HTTP recipes (url, body vs form, reserved outputs, expose requires proxy, recipe cycles, ${http.*} / ${token} refs), health thresholds, duplicate ports, identities, proxy routes (including per-service proxy fragments and synthesized expose / http.*.expose routes merged at load), proxy.listen.port when proxy.enabled is true, environment references, profile references, optional plugins[].path, telemetry.otlp.listen (loopback host, valid port, no collision with the proxy/token-endpoint/gRPC-route ports), and web.listen (loopback host, valid port, no collision with the proxy/token-endpoint/OTLP/gRPC-route ports).

The TUI Config screen v / /buffer overlay validates this text before writing. Invalid YAML is not saved. e still opens $EDITOR.

The TUI Config screen — merged project, google, runtime, logs, proxy routes, services, and tasks in one view

The supervisor watches .devctl/ (fs.watch, ~200ms debounce) and runs the same path as /reload. devctl reload and TUI /reload re-read configuration, publish ConfigurationChanged, and list services that must restart because command, environment, ports, identity, or watch changed. A running proxy hot-swaps routes when listen addresses are unchanged — see Proxy.

Changing the plugins path list hot-applies token providers, log parsers, and proxy middleware. Editing an already-imported plugin file (same path, newer mtime) still requires devctl down && devctl start — Bun’s module cache cannot unload it. A running service’s environment is unchanged until that service restarts.

Related

Clone this wiki locally