ecaddy — One global Caddy for all your local Rails projects
Instead of fighting port conflicts from multiple Caddy processes, ecaddy manages a single shared Caddy instance. Each project keeps its own Caddyfile — ecaddy copies it in and out of the global config on demand.
How It Works
One Caddy process. Many project fragments. Each project ships a Caddyfile; ecaddy copies it into ~/.config/caddy/sites/<name>.caddy on start and removes it on stop. The global Caddyfile imports everything in sites/*.caddy, so reloads are atomic and instant.
Browser
│
▼
Caddy (~/.config/caddy/Caddyfile)
│ imports sites/*.caddy
├── fishme.localhost → localhost:3104
├── letly.localhost → localhost:3100
└── traiderb.localhost → localhost:3106
Features
One Global Caddy
A single brew services-managed process serves every project. No per-project Caddy, no port collisions on 80/443.
Procfile-Native
Drop ecaddy run --config ./Caddyfile --site name into Procfile.dev. On Ctrl-C the fragment is removed and Caddy reloads cleanly.
Conflict Detection
Before registering a fragment, ecaddy parses it and BLOCKs on shared *.localhost domains or reverse_proxy localhost:PORT collisions.
HTTPS by Default
ecaddy setup runs caddy trust once, so https://*.localhost shows up green in browsers with no per-project cert juggling.
Idempotent Setup
ecaddy setup is safe to re-run. Every step (Homebrew install, scaffold, trust, brew-services start) checks before acting.
Built-in Doctor
ecaddy doctor audits every registered site for cross-site port and domain collisions, plus dead upstreams (app not running).
Quick Example
Write a project Caddyfile, point a Procfile line at it, and visit the URL:
# Caddyfile (in your Rails project root)
fishme.localhost {
reverse_proxy localhost:3104
tls internal
log {
output file log/caddy.log
}
}
# Procfile.dev
web: bin/rails server -p 3104
js: yarn dev
caddy: ecaddy run --config ./Caddyfile --site fishme
bin/dev
# → visit https://fishme.localhost
Command Summary
| Command | Purpose |
|---|---|
ecaddy setup | One-time machine bootstrap (Caddy install, scaffold, trust, start) |
ecaddy run | Register a fragment, block, unregister on shutdown (for Procfile) |
ecaddy ensure | Register a fragment and exit (one-shot, persistent) |
ecaddy list | Show all registered sites |
ecaddy up NAME | Enable a previously disabled site |
ecaddy down NAME | Disable an enabled site without removing it |
ecaddy edit NAME | Edit a site's fragment in $EDITOR, then validate + reload |
ecaddy remove NAME | Delete a site's fragment and registry entry |
ecaddy reload | Validate and reload the global Caddy config |
ecaddy status | Show global Caddy state and per-site health |
ecaddy doctor | Audit all sites for port/domain conflicts and dead upstreams |
ecaddy audit | Full system + TLS audit with optional --fix prompts |
ecaddy logs --site NAME | Tail the project's Caddy log |
See the Commands reference for full flags and behaviour.
Why not just run Caddy per project?
Because ports 80 and 443 are global. Two Caddy processes can't both bind them. Multi-project local dev requires either: a per-project SOCKS-style port (ugly URLs), a shared reverse-proxy in front of N Caddies (extra hop), or a single shared Caddy with composed fragments (this approach).
ecaddy setup bootstrap (which calls brew services) is mac-specific.
Module Map
| Module | Description |
|---|---|
EasyCaddy::CLI | Thor CLI entry point; dispatches each subcommand to a dedicated command object |
EasyCaddy::Paths | Single source of every filesystem path; honours ECADDY_HOME |
EasyCaddy::Registry | Reads/writes ecaddy.yml (name → { enabled, source_path }) |
EasyCaddy::Site | Immutable value object: name, enabled, source_path |
EasyCaddy::Parser | Minimal regex Caddyfile parser — extracts domains, ports, log paths |
EasyCaddy::Conflicts | Domain / port collision detection (check on register, doctor on demand) |
EasyCaddy::Caddy | Wrapper around the caddy binary and brew services |
EasyCaddy::Commands::* | One class per CLI command (Setup, Run, Ensure, List, Up, Down, Doctor, …) |