Caddy holds every active route in memory. Killing Caddy the wrong way drops them all at once. This page is the operating procedure for stopping, restarting, and recovering Caddy without losing routes.
If you only remember one thing: never kill, pkill, or
systemctl stop Caddy. Use asd caddy stop. The reason is below.
Routes added at runtime (every asd net apply, every asd expose <port>) are sent to Caddy's admin API and held in its in-memory
config. They are not mirrored to a Caddy config file on disk.
If Caddy exits — for any reason that isn't asd caddy stop — those
routes are gone. The next start re-loads from Caddyfile /
generated config (sparse), not from the live state.
.asd/workspace/caddy/caddy-state.json is the lifeline.
| Property | Value |
|---|---|
| Written by | asd caddy stop — fetches GET /config/ from Caddy's admin API just before sending SIGINT. |
| Read by | asd caddy start — merges saved routes into the generated config before launching Caddy. |
| Cleaned up | After successful start (state is now back in Caddy's memory; file no longer needed). |
| Non-fatal | Missing / corrupt / version-mismatched files are silently ignored. Routes can be re-applied with asd net apply. |
asd caddy stop asd caddy start
│ │
├→ GET /config/ → admin API ├→ Generate base config from manifests
├→ Write caddy-state.json ├→ Read caddy-state.json (if exists)
├→ SIGINT to Caddy process ├→ Merge: saved routes → generated config
└→ Update registry │ (only routes with @id not already
│ in generated config — no clobber)
├→ Launch Caddy with merged config
└→ Delete caddy-state.json
kill / pkill is destructiveThree failure paths:
State loss. Every route added by asd net apply after the
last start is in memory only. SIGKILL → memory gone → all those
routes gone → all services unreachable until next apply.
Stuck PIDs. pkill caddy can race with the asd registry's
process-tracking; you end up with the registry thinking Caddy
is alive but the process is gone. Subsequent commands try to
talk to a dead admin API.
No state-file snapshot. asd caddy stop writes
caddy-state.json before sending SIGINT. Bypassing it skips
the snapshot. Next start has no memory of what was running.
The recovery from a wrong-way-kill: asd caddy start, then
asd net apply to rebuild routes from manifests. You'll lose any
asd expose <port> exposures that weren't in a manifest.
asd's process manager has a fingerprint mechanism for tunnel
daemons — if .env changes the tunnel's parameters, the daemon
gets restarted automatically. This is correct for tunnels (each is
independent, restart-safe).
For Caddy it's destructive. Three reasons:
Caddy accumulates state. Routes added via asd net apply
after initial start aren't in any config file. A fingerprint
restart skips the state-file dance and drops them.
Restart can fail. If the new port isn't ready, the new
binary doesn't start, the old one is already gone. You're left
with no Caddy at all.
Blast radius. Caddy is the single point of routing for
every service. Killing it breaks all services + all tunnels
simultaneously.
The historical incident that hardened this: changing
ASD_CADDY_ADMIN_PORT in .env and running asd caddy start
killed the running Caddy, failed to start the new one, deleted the
fingerprint file, and left the system unrecoverable until
asd net apply rebuilt from scratch (with all asd expose
exposures lost).
If you really need to change ASD_CADDY_ADMIN_PORT /
ASD_CADDY_HTTP_PORT / ASD_CADDY_HTTPS_PORT:
# 1. Snapshot state
asd caddy stop
# 2. Edit .env to new ports
vim .env
# 3. Bring Caddy up on the new ports (restores from state file)
asd caddy start
# 4. Re-apply manifest routes (idempotent — handles any drift)
asd net apply
Never restart Caddy with a port change while it's running. The
admin-API URL would change mid-flight and the snapshot step would
fail.
# Is Caddy running?
asd caddy list
# What's its current config?
asd caddy config | jq '.apps.http.servers'
# Is the admin API reachable?
curl -s http://localhost:${ASD_CADDY_ADMIN_PORT}/config/ | head -c 200
If the admin API is unreachable but the process appears running,
the process is wedged — asd caddy stop followed by asd caddy start is safer than SIGKILL. If asd caddy stop itself hangs (no
admin API to talk to), only then is SIGKILL the right call —
followed immediately by asd net apply to rebuild routes.
cli/caddy — command reference.architecture — where Caddy fits.cookbook/debug-a-broken-route — Caddy as Step 2/3 of the decision tree.