Command Palette
Search for a command to run...

control doctor

A standalone, read-only health and security-posture pass over a deployment. It inspects the deploy .env, the on-disk certs, and (when reachable) the live Postgres / Valkey state — and never mutates anything.

The subcommand runs without booting the server — it intercepts before flag parsing, so it works on a host where the control server can't even start (bad config is exactly when you need it). Run it inside the control container or against the deploy directory:

docker compose exec control control doctor
# or, on the deploy host with the binary available:
control doctor --env-file deploy/.env
control doctor --json          # machine-readable, for CI / monitoring

--env-file (default .env) merges the deploy env file over the process environment — the file wins, since it's the operator's stored source of truth. The whole pass runs under a 15-second timeout.

Exit codes

The exit code is graduated so scripts and CI can gate on it:

ExitMeaning
0Healthy — only OK / info findings
1At least one warning
100At least one critical finding
2A check could not run at all (highest precedence — the report is incomplete, fix the doctor invocation/config first)
func (r Report) ExitCode() int {
	if len(r.ExecErrors) > 0 {
		return 2
	}
	switch r.Worst() {
	case SeverityCritical:
		return 100
	case SeverityWarning:
		return 1
	default:
		return 0
	}
}

A panicking check never aborts the suite — it's recovered into a "could not run" execution error (exit 2), distinct from a check that ran and found a problem: a down datastore is a critical finding, not an exec error.

What it checks

CheckLooks at
secretsWeak or placeholder secrets in the env (short JWT secret, default passwords)
encryption_keyCONTROL_ENCRYPTION_KEY present and well-formed — at-rest encryption is mandatory
corsCredentialed wildcard CORS origin
portsInternal mTLS listener bound to all interfaces
image_tagFloating IMAGE_TAG (e.g. latest) in a production deploy
cert_permsPrivate key files group/world-readable
cert_expiryCA / service certs missing, expired, not yet valid, or near end of life
datastoresPostgres + Valkey reachability
queuesAsynq dead-letter (archived) queue depth
searchExpected search indexes present, indexer alive
terminalValkey keyspace notifications + Traefik terminal-routing config (the silent terminal-404 trap)
adminBootstrap admin still on the default email

Each finding carries an ID, a severity (ok / info / warning / critical), a message, and — for warnings and criticals — a remediation hint. Findings never contain secret values: they name the variable or file and describe the shape of the problem only.

The cert-expiry horizon is lifetime-relative

cert_expiry doesn't use a fixed "30 days left" threshold. The warning horizon is derived from each cert's own lifetime: past 80% of its validity window (under 20% remaining) is a warning, mirroring the agents' 80%-lifetime auto-renewal — if a cert is past that point, auto-rotation should already have fired, so a warning here means the rotation machinery needs attention. Missing, unparseable, expired, or not-yet-valid certs are critical.

cert_perms fails closed: a configured private key it cannot stat is a critical finding ("cannot verify a security-relevant file"), never a silent skip. Keys that are group- or world-accessible are critical too — the remediation is chmod 0400.

When to run it

  • After every deploy or .env change — gate the pipeline on exit code 0.
  • On a schedule (cron + --json into your monitoring) to catch cert drift and dead-letter buildup.
  • First thing when "something is off" — it encodes the known silent-failure traps (terminal routing, keyspace notifications, floating image tags) so you don't rediscover them from scratch. For symptom-driven digging, see Troubleshooting.