# CLI Reference ## Synopsis ``` ember [flags] ``` ## Flags | Flag | Type | Default | Description | |--------------------|------|---------|-------------| | `--addr` | string (repeatable) | `http://localhost:2019` | Caddy admin API address (`http://`, `https://`, or `unix//path`). Repeatable in `--daemon`, `--json`, `status`, and `wait` modes to monitor multiple instances. Supports `name=url` aliases and per-instance suffixes (`,ca=PATH`, `,cert=PATH`, `,key=PATH`, `,insecure`, `,interval=DURATION`); see [Multi-instance](#multi-instance-monitoring). | | `-i`, `--interval` | duration | `1s` | Polling interval | | `--timeout` | duration | `0` (none) | Global timeout for the non-interactive modes and subcommands (`--json`, `--daemon`, `status`, `wait`). The interactive TUI ignores it, and `version --check` uses a fixed 10s timeout. 0 means no timeout. | | `--slow-threshold` | int | `500` | Slow request threshold in milliseconds. Requests above this are highlighted yellow; above 2x are red. | | `--frankenphp-pid` | int | `0` (auto) | FrankenPHP PID. Auto-detected if not set. | | `--json` | bool | `false` | JSON output mode (streaming JSONL). See [JSON Output](json-output.md). | | `--once` | bool | `false` | Output a single snapshot and exit. Requires `--json`. See [JSON Output](json-output.md). | | `--expose` | string | _(none)_ | Start Prometheus metrics endpoint on this address (e.g. `:9191`). See [Prometheus Export](prometheus-export.md). | | `--daemon` | bool | `false` | Headless mode: no TUI. Requires `--expose`. See [Prometheus Export](prometheus-export.md). | | `--metrics-prefix` | string | _(none)_ | Prefix for exported Prometheus metric names. See [Prometheus Export](prometheus-export.md). | | `--log-format` | string | `text` | Log format for daemon/json modes (`text` or `json`). JSON format is suitable for log aggregation systems. | | `--ca-cert` | string | _(none)_ | Path to CA certificate for TLS verification | | `--client-cert` | string | _(none)_ | Path to client certificate for mTLS | | `--client-key` | string | _(none)_ | Path to client private key for mTLS | | `--insecure` | bool | `false` | Skip TLS certificate verification | | `-f`, `--config` | string | `.ember.toml` | Path to the Ember config file (TOML). Read only when neither `--addr` nor `EMBER_ADDR` is set. See [Config file](#config-file). | | `--metrics-auth` | string | _(none)_ | Basic auth for the metrics endpoint (`user:password`). Requires `--expose`. See [Prometheus Export](prometheus-export.md). | | `--log-listen` | string | _(auto)_ | Bind a TCP listener at this address (e.g. `:9210`) and ask Caddy to push its logs to it via two hot-registered sinks (access + runtime). Required when Caddy is on a remote host; auto-bound on a free loopback port otherwise. See [Logs](logs.md). | | `--no-color` | bool | `false` | Disable colors. Also enabled by the `NO_COLOR` env var (see [no-color.org](https://no-color.org/)). | | `--version` | | | Print version and exit | ## Environment Variables Some flags can be set via environment variables. Explicit flags always take precedence over environment variables. | Variable | Flag | Example | |----------|------|---------| | `EMBER_ADDR` | `--addr` | `EMBER_ADDR=http://caddy:2019`, `EMBER_ADDR=unix//run/caddy/admin.sock`, or semicolon-separated for multi-instance: `EMBER_ADDR=web1=https://a;web2=https://b`. The semicolon (not a comma) is the address separator so each entry can carry comma-separated TLS suffixes (e.g. `web1=https://a,ca=/p/ca1.pem;web2=https://b,insecure`). | | `EMBER_INTERVAL` | `--interval` | `EMBER_INTERVAL=5s` | | `EMBER_EXPOSE` | `--expose` | `EMBER_EXPOSE=:9191` | | `EMBER_METRICS_PREFIX` | `--metrics-prefix` | `EMBER_METRICS_PREFIX=myapp` | | `EMBER_METRICS_AUTH` | `--metrics-auth` | `EMBER_METRICS_AUTH=admin:secret` | | `EMBER_LOG_LISTEN` | `--log-listen` | `EMBER_LOG_LISTEN=:9210` | | `EMBER_CONFIG` | `--config` | `EMBER_CONFIG=/etc/ember/prod.toml` | This is especially useful in container deployments where flags are less convenient than environment variables. Using `EMBER_METRICS_AUTH` is recommended over the flag to avoid exposing credentials in `ps` output. ## Config file Instead of repeating `--addr` on every run, the fleet can be described once in a TOML file (`.ember.toml` in the current directory by default). Point at another file with `-f`/`--config` or `EMBER_CONFIG`, which is handy for per-environment fleets (`.ember.prod.toml`, `.ember.staging.toml`). ```toml default = "production" # optional, used by the TUI only # Top-level keys are global fallbacks for every endpoint. interval = "2s" ca_cert = "/etc/ca.pem" [[endpoints]] name = "production" addr = "https://prod:2019" [[endpoints]] name = "staging" addr = "https://staging:2019" insecure = true # overrides the global for this instance only ``` The per-endpoint keys (`ca_cert`, `cert`, `key`, `insecure`, `interval`) mirror the `--addr` suffixes; a key set inside an `[[endpoints]]` table overrides the same top-level key, exactly like `,ca=` overrides `--ca-cert`. Endpoint names must start with a letter and contain only letters, digits and underscores. ### Precedence `--addr` > `EMBER_ADDR` > config file > built-in default (`http://localhost:2019`). If `--addr` or `EMBER_ADDR` is set, the file is ignored entirely (no merge). A missing default `.ember.toml` falls back silently to `http://localhost:2019`; an explicit `-f`/`EMBER_CONFIG` that is missing, or any malformed file, is a hard error. ### How each mode consumes the file - **TUI** (`ember`): single-instance. Zero endpoints uses the built-in default; one endpoint uses it; two or more with `default` set use that one; two or more without `default` show a small picker before the dashboard mounts. - **`--daemon`, `--json`, `status`, `wait`, `init`**: the file expands to N endpoints, exactly as if each had been passed as `--addr name=url`. `diff` and `version` never read the file. ## Examples ```bash # Default: connect to localhost:2019 ember # Connect to a remote Caddy instance ember --addr http://prod:2019 # Connect via Unix socket ember --addr unix//run/caddy/admin.sock # Pipe-friendly JSON output ember --json # Single JSON snapshot and exit ember --json --once # JSON output at a slower rate ember --json --interval 5s # TUI with Prometheus metrics on port 9191 ember --expose :9191 # Headless metrics exporter (no TUI) ember --expose :9191 --daemon # Stricter slow-request highlighting ember --slow-threshold 200 # Prefixed Prometheus metrics ember --expose :9191 --metrics-prefix myapp # Explicitly specify a FrankenPHP PID ember --frankenphp-pid 42 ``` ## Multi-instance monitoring A single `ember --daemon` (or `ember --json`) process can poll several Caddy instances and aggregate their metrics behind one Prometheus endpoint. This keeps the sidecar count low when running a fleet of small services. ```bash # Explicit aliases ember --daemon --expose :9191 \ --addr web1=https://web1.fr \ --addr web2=https://web2.fr # Without aliases the host is slugified (web1.fr -> web1_fr) ember --daemon --expose :9191 \ --addr https://web1.fr \ --addr https://web2.fr # Semicolon-separated env var (each entry may carry comma-separated TLS suffixes) EMBER_ADDR='web1=https://a;web2=https://b' ember --daemon --expose :9191 ``` When two or more `--addr` values are supplied, every emitted Prometheus metric (except `ember_build_info`) gains an `ember_instance=""` label. With a single `--addr` the output is unchanged: no extra label. ### Why no multi-instance TUI? The interactive TUI stays single-instance by design. Watching several Caddy hosts at the same time is what `--daemon` (Prometheus aggregation, then visualised with Grafana or any compatible dashboard) and `--json` (one JSONL line per instance per tick, ready for downstream tooling) are for. Passing repeated `--addr` to the default TUI mode fails fast with an explicit error pointing at those modes. ### Per-instance TLS Each `--addr` value can carry its own TLS knobs as comma-separated suffixes after the URL. Any field omitted falls back to the corresponding global flag (`--ca-cert`, `--client-cert`, `--client-key`, `--insecure`). | Suffix | Equivalent global flag | Notes | |--------|------------------------|-------| | `,ca=PATH` | `--ca-cert` | CA certificate used to verify the server | | `,cert=PATH` | `--client-cert` | Client certificate (mTLS); must be paired with `key=` | | `,key=PATH` | `--client-key` | Client private key (mTLS); must be paired with `cert=` | | `,insecure` or `,insecure=true\|false` | `--insecure` | Skip TLS verification; explicit `=false` overrides a global `--insecure` | ```bash ember --daemon --expose :9191 \ --addr web1=https://a,ca=/etc/ca1.pem \ --addr web2=https://b,ca=/etc/ca2.pem ``` On `SIGHUP`, each instance re-reads its own certificate files independently, so per-instance certs can be rotated in place without restarting Ember. ### Per-instance polling interval Each `--addr` can override the global `--interval` with a `,interval=DURATION` suffix. This matters for fleets where some instances are remote and benefit from a slower cadence without raising the global default. ```bash ember --daemon --expose :9191 \ --addr web1=https://web1.fr \ --addr remote=https://far.example,interval=10s ``` Active in `--daemon` and `--json` modes (each instance polls its own ticker), and in the single-instance TUI, which polls at its endpoint's interval. The `/healthz` and `/healthz/` staleness threshold is computed per-instance, so a slow instance is not flagged stale just because its cadence is slower than the global default. `status` and `wait` keep using the global interval. Constraints: - File paths in suffixes cannot contain a literal comma; the comma is reserved as the suffix separator. - TLS suffixes are rejected on `unix//` addresses (no TLS over a Unix socket). - Per-instance `interval=` is allowed on `unix//` addresses (it is not a TLS option). - Per-instance `interval=` must be at least `100ms`. When `--timeout` is set, it must be at least the largest effective polling interval (global or per-instance). - `--daemon`, `--json`, `status`, `wait`, `diff`, and `init` (with `-y`) accept multi-instance input. The TUI default mode is single-instance by design and refuses repeated `--addr` with an explicit error pointing at `--daemon` and `--json`. - Instance names must match `[a-zA-Z_][a-zA-Z0-9_]*` (Prometheus label rules: letters, digits and underscores only — no hyphens or dots). With more than one address, slugified names that start with a digit (typical for raw IPv4 hosts) require an explicit `name=url` alias. - `--frankenphp-pid` is ignored when `--addr` is repeated. Local instances (`localhost`, `127.0.0.1`, `::1` or `unix//`) get per-instance `process_cpu_percent`/`process_rss_bytes` via auto-detection on the admin endpoint; remote instances silently fall back to the `process_*` metrics exposed by Caddy. - Plugins are skipped in multi-instance mode unless they opt in by implementing `plugin.MultiInstancePlugin`. Non-aware plugins are disabled with a startup warning. See the [Plugin Development Guide](plugins.md#multi-instance-plugins) for the multi-instance contract. See [Prometheus Export](prometheus-export.md) for the resulting metric labels and `/healthz` body. ## Subcommands ### `ember init` Checks that Caddy is properly configured for Ember and offers to enable HTTP metrics via the admin API if they are missing. Does not modify any files on disk. **What it checks:** 1. Admin API is reachable 2. HTTP servers are configured 3. HTTP metrics directive is enabled 4. FrankenPHP presence, threads, and workers 5. Metrics are actually flowing **What it can do:** - Enable the `metrics` directive via `POST /config/apps/http/metrics` (no Caddy restart needed) **Examples:** ```bash ember init ember init --addr https://prod:2019 --ca-cert ca.pem ember init -y # skip confirmation prompts ember init -yq # skip prompts, suppress output ember init -y --addr web1=https://a --addr web2=https://b # multi-instance ``` With multiple `--addr` values, the checklist is run against every instance, output is grouped per instance under a `--- ---` header sorted by name, and the exit code is non-zero if any instance fails. Interactive prompts are disabled in multi-instance mode: `--yes` (`-y`) is required, otherwise the command errors out. | Flag | Type | Default | Description | |------|------|---------|-------------| | `-y`, `--yes` | bool | `false` | Skip confirmation prompts (required when multiple `--addr` are provided) | | `-q`, `--quiet` | bool | `false` | Suppress output (errors still reported via exit code) | ### `ember version` Prints the current version. With `--check`, queries the GitHub Releases API to see if a newer version is available. **Examples:** ```bash ember version ember version --check ``` | Flag | Type | Default | Description | |------|------|---------|-------------| | `--check` | bool | `false` | Check for newer version on GitHub | ### `ember status` One-line health check of Caddy. Performs two fetches separated by the polling interval to compute derived metrics, then prints a compact status line and exits. Exit code 0 means Caddy is reachable, 1 means unreachable. **Output format:** ``` Caddy OK | 5 hosts | 450 rps | P99 12ms | CPU 3.2% | RSS 48MB | up 3d 2h ``` With FrankenPHP: ``` Caddy OK | 5 hosts | 450 rps | P99 12ms | CPU 3.2% | RSS 48MB | up 3d 2h | FrankenPHP 8/20 busy | 2 workers ``` If Caddy is unreachable: ``` Caddy UNREACHABLE | http://localhost:2019 ``` **Examples:** ```bash ember status ember status --json ember status --json | jq .rps ember status --addr http://prod:2019 ember status --interval 2s ember status --addr web1=https://a --addr web2=https://b # multi-instance ``` ### Multi-instance status When `--addr` is repeated, `ember status` checks every instance in parallel and emits one block per instance, sorted alphabetically by name. Exit code is 0 only when **all** instances are reachable. **Text output:** ``` [web1] Caddy OK | 5 hosts | 450 rps | P99 12ms | CPU 3.2% | RSS 48MB | up 3d 2h [web2] Caddy UNREACHABLE | https://web2.fr ``` **JSON output (`--json`):** ```json { "status": "degraded", "instances": [ {"name": "web1", "addr": "https://web1.fr", "status": "ok", "rps": 450, "cpuPercent": 3.2, "rssBytes": 50331648, "p99": 12.3}, {"name": "web2", "addr": "https://web2.fr", "status": "unreachable", "rps": 0, "cpuPercent": 0, "rssBytes": 0} ] } ``` The top-level `status` is the worst of: `ok` (every instance reachable) < `degraded` (mixed) < `unreachable` (every instance down). FrankenPHP fields are emitted per instance when detected. With `--json` (single-instance mode), the output is a JSON object: ```json { "status": "ok", "hosts": 5, "rps": 450, "p99": 12.3, "cpuPercent": 3.2, "rssBytes": 50331648, "uptime": "3d 2h", "frankenphp": { "busy": 8, "total": 20, "workers": 2 } } ``` | Flag | Type | Default | Description | |------|------|---------|-------------| | `--json` | bool | `false` | Output status as JSON | The `--addr`, `--interval`, and `--frankenphp-pid` flags are also available. ### `ember wait` Blocks until the Caddy admin API is reachable, then exits with code 0. If `--timeout` is set and Caddy is still unreachable, exits with code 1. With repeated `--addr`, `ember wait` blocks until **every** instance is reachable (strict CI semantics). Pass `--any` to return as soon as the first one responds. On timeout, the error names the lagging instance(s). **Examples:** ```bash ember wait ember wait --timeout 30s ember wait -q --timeout 10s && ./deploy.sh ember wait --addr http://prod:2019 && ember status docker compose up -d && ember wait && ./deploy.sh ember wait --addr web1=https://a --addr web2=https://b --timeout 30s ember wait --any --addr http://primary --addr http://fallback ``` | Flag | Type | Default | Description | |------|------|---------|-------------| | `-q`, `--quiet` | bool | `false` | Suppress output (exit code only) | | `--any` | bool | `false` | Return as soon as any instance is reachable (default: wait for all) | The `--addr`, `--interval`, and `--timeout` flags are also available. `--addr` may be repeated to wait on multiple instances (with optional `name=url` aliases). ### `ember diff` Compares two JSON or JSONL snapshots produced by `ember --json --once` and shows the deltas for key metrics. Useful for validating deployments and benchmarks. Exit code 0 means no regressions detected, 1 means regressions found (>10% degradation on latency, error rate, or CPU; >10% drop on RPS). **Examples:** ```bash ember --json --once > before.json # ... deploy or benchmark ... ember --json --once > after.json ember diff before.json after.json ``` **Sample output:** ``` Global Requests 1720 -> 2023 +17.6% Avg (cumul.) 76.9ms -> 77.7ms +1.0% Errors 0 -> 0 = In-flight 3.0 -> 1.0 -66.7% CPU 0 -> 0 = RSS 39.5MB -> 39.5MB = Per-host changes api In-flight 3.0 -> 1.0 -66.7% app In-flight 1.0 -> 0 -100.0% No regressions detected ``` **Multi-instance input:** When a snapshot was produced with repeated `--addr` (multi-instance JSONL with one line per instance per tick), `ember diff` groups lines by the `instance` field, keeps the last entry per instance in each file, and emits one diff block per instance. The exit code is non-zero if any single instance regresses. ```bash ember --json --once --addr web1=https://web1.fr --addr web2=https://web2.fr > before.jsonl # ... deploy ... ember --json --once --addr web1=https://web1.fr --addr web2=https://web2.fr > after.jsonl ember diff before.jsonl after.jsonl ``` ``` == web1 == Global RPS 100/s -> 105/s +5.0% Avg latency 20.0ms -> 19.0ms -5.0% ... == web2 == Global RPS 200/s -> 210/s +5.0% Avg latency 15.0ms -> 14.0ms -6.7% ... No regressions detected ``` ### `ember config use ` Sets the top-level `default` key in the [config file](#config-file) to ``, so the single-instance TUI connects to that endpoint instead of showing the picker. Only the `default` line is rewritten; comments and formatting are left intact. `` must match one of the file's endpoints. ```bash ember config use production ember -f .ember.staging.toml config use staging ``` ## Keybindings ### Navigation | Key | Action | |-----|----------------------------------------------------| | `Up` / `Down` / `j` / `k` | Move cursor | | `Enter` / `Right` / `l` | Open detail panel / expand node (Caddy Config tab) | | `Left` / `h` | Collapse node (Caddy Config tab) | | `Esc` | Close panel / clear search / go back | | `Tab` / `Shift+Tab` | Switch tab | | `1` / `2` / `3` | Jump to tab | | `t` then `1`..`9` | Tab-select prefix: arm tab-select mode, next digit switches tab (`Esc` cancels) | | `Home` / `End` | Jump to first / last item | | `PgUp` / `PgDn` | Page up / page down | ### Actions | Key | Action | Context | |-----|--------|---------| | `s` / `S` | Cycle sort field forward / backward | Caddy / FrankenPHP tabs | | `p` | Pause / resume polling | Any view | | `/` | Enter filter / search mode | Any tab | | `e` / `E` | Expand / collapse all nodes | Config tab | | `n` / `N` | Jump to next / previous search match | Config tab | | `r` | Refresh config / restart workers | Config tab / FrankenPHP tab | | `g` | Toggle full-screen graphs | Any view | | `?` | Toggle help overlay | Any view | | `q` | Quit | Any view | ## Shell Completions ### Bash ```bash ember completion bash > /etc/bash_completion.d/ember ``` ### Zsh ```bash ember completion zsh > "${fpath[1]}/_ember" ``` ### Fish ```bash ember completion fish > ~/.config/fish/completions/ember.fish ``` ## See Also - [Getting Started](getting-started.md) - [JSON Output](json-output.md) - [Prometheus Export](prometheus-export.md)