Troubleshooting
Reference for diagnosing problems: the filesystem layout, conflict detection rules, the doctor / audit commands, common failure modes, and how to override paths for testing.
Global filesystem layout
Every path ecaddy touches derives from ECADDY_HOME, which defaults to ~/.config/caddy.
~/.config/caddy/
Caddyfile # global root: { admin ... } + import sites/*.caddy
ecaddy.yml # registry: name → { enabled, source_path }
sites/
fishme.caddy # enabled fragments — loaded by Caddy
letly.caddy
disabled/
traiderb.caddy # disabled fragments — preserved, not loaded
The global Caddyfile is also symlinked at /opt/homebrew/etc/Caddyfile so brew services start caddy picks it up automatically.
The registry
ecaddy.yml is a flat YAML hash keyed by site name. It only stores metadata — no Caddyfile content, no port/domain values:
fishme:
name: fishme
enabled: true
source_path: /projects/fishme/Caddyfile
letly:
name: letly
enabled: false
source_path: /projects/letly/Caddyfile
If you delete ecaddy.yml, the fragments stay on disk but ecaddy "forgets" them — list shows an empty registry, and the source-path linkage is lost. To recover, re-run ecaddy run or ecaddy ensure from each project.
Overriding ECADDY_HOME
Set ECADDY_HOME to redirect every filesystem operation. The default is ~/.config/caddy; an override is useful for testing, multi-account setups, or sandboxing experiments.
ECADDY_HOME=/tmp/ecaddy_test ecaddy list
# → reads /tmp/ecaddy_test/ecaddy.yml, never touches your real config
ECADDY_HOME controls the source-of-truth Caddyfile location, but brew services still loads /opt/homebrew/etc/Caddyfile. If you redirect ECADDY_HOME, the running Caddy service won't pick up your sandboxed config unless you also re-symlink. For testing, this is fine — the specs do exactly this and never invoke brew services.
Conflict detection
Before registering any Caddyfile, ecaddy parses it (regex-based) and checks the existing fragments for collisions:
- Domain collision — the same
*.localhostdomain is already registered by another enabled site → BLOCK - Port collision — the same
reverse_proxy localhost:PORTis already in use by another site → BLOCK
These checks run on ecaddy run, ecaddy ensure, and ecaddy up. The check reads fragments off disk, not the registry, so it's accurate even if ecaddy.yml is out of sync.
There is no bypass
If you genuinely need two projects to share a domain (rare), pick a different site name on one of them and adjust the localhost subdomain in its Caddyfile. ecaddy does not have a --force flag for conflict detection on registration — the cost of two fragments fighting for the same port is silent breakage at request time, which is worse than a clear BLOCK.
ecaddy doctor — cross-site audit
Where the per-register check looks at the new fragment vs. existing ones, doctor scans all registered sites cross-wise and TCP-probes every upstream port.
ecaddy doctor
| Severity | Trigger | Action |
|---|---|---|
| BLOCK | Two sites share a port or domain | Exit 1. Fix one of the fragments and re-run. |
| WARN | Port bound by something other than the registered upstream | Exit 0. Often means an old dev server is still alive — kill the orphan. |
| INFO | Upstream not listening on the expected port | Exit 0. Usually just means the project isn't running. |
ecaddy audit — system + TLS
audit goes wider than doctor. It probes:
- Whether
brew servicesreports Caddy as running - Whether a Caddy process is alive but unmanaged by brew (a common drift)
- TLS handshake on every
*.localhostdomain (looks forunknown CA, alert 80, connection refused) - Whether the local CA is actually in your system keychain (browser-trust check)
ecaddy audit # report-only
ecaddy audit --fix # walk findings, prompt to apply each fix
ecaddy audit --site fishme # limit to one site
In --fix mode, each finding ships with a proposed command and a verifier. If the primary fix doesn't resolve the finding (verifier still fails), audit chains to a next_fix — e.g. caddy trust → sudo caddy trust — until something verifies or the chain runs out.
Common failures and fixes
"Caddy service: stopped" but a caddy process is running
An old caddy process is alive but brew services has lost track of it (often after a system update or a manual caddy stop). audit detects this and proposes:
pkill -x caddy && brew services start caddy
Browser shows ERR_CERT_AUTHORITY_INVALID on https://*.localhost
The local CA isn't trusted by your system keychain yet. caddy trust seeds it; the first attempt typically needs root:
caddy trust
# → if that fails:
sudo caddy trust
This is what ecaddy setup runs for you on first install — if you skipped that step, run it now.
Rails returns "Blocked host: fishme.localhost"
Rails rejects unfamiliar hostnames by default. Allow the pattern:
# config/environments/development.rb
config.hosts << /.*\.localhost/
Caddy logs to the wrong directory
If your project Caddyfile says output file log/caddy.log, ecaddy rewrites that to an absolute path (/projects/fishme/log/caddy.log) before installing the fragment. If you see a relative path in ~/.config/caddy/sites/fishme.caddy, you've edited the installed fragment directly — re-run ecaddy ensure or ecaddy run from your project root to restore the rewrite.
Two projects accidentally pick the same app port
ecaddy run will BLOCK with a clear message. Either change the reverse_proxy localhost:PORT in one Caddyfile (and the matching Rails -p flag) or pick a different port range per project. See the table in Getting Started for a common allocation pattern.
Local edits keep getting overwritten
ecaddy edit fishme opens ~/.config/caddy/sites/fishme.caddy — the installed fragment. Any time you re-run ecaddy run or ecaddy ensure from the project, that fragment is overwritten with the project Caddyfile (plus log-path rewriting). For lasting changes, edit the project's Caddyfile, not the installed fragment.
Forcing a clean reset
If the global config gets into a bad state, the safest reset is:
brew services stop caddy
rm -rf ~/.config/caddy
ecaddy setup
# then re-register each project from its directory:
cd /projects/fishme && ecaddy ensure -c ./Caddyfile -s fishme
cd /projects/letly && ecaddy ensure -c ./Caddyfile -s letly
This loses no project source — it only discards the installed fragments and the registry, both of which are re-derivable from each project's Caddyfile.
Getting help
- Open an issue at github.com/pniemczyk/easy_caddy/issues
- Include the output of
ecaddy version,ecaddy status, andecaddy doctor - For TLS-specific issues, include
ecaddy auditoutput as well