Summary
Since 18591d1 (feat(prepare)!: provision over static Go agent), nextdeploy prepare defaults to the Go agent path. That path provisions the runtimes and the NextDeploy directory tree, but not Caddy, the nextdeployd service, or Doppler — all of which the Ansible playbook it replaced does provision.
On a host that has never been prepared with --ansible, prepare reports success and the first nextdeploy ship then fails.
What's missing
Plan() (daemon/internal/prepare/plan.go:90) emits: package index refresh, base packages, the nextdeploy user/group, /opt/nextdeploy/{,apps,uploads,tmp}, Node/Corepack/Bun, /var/log/caddy + /etc/caddy/nextdeploy.d, the logrotate policy, and the fail2ban filters/jails.
| Missing |
Playbook equivalent |
Consequence |
| Caddy, with the Coraza WAF module |
prepare.yml:558–800 |
caddyManager.Validate() fails during activateRelease. Every generated fragment carries a coraza_waf block and EnsureMainCaddyfile writes order coraza_waf first, neither of which a stock Caddy can parse — and on a host with no Caddy at all, nothing serves |
nextdeployd.service, /etc/nextdeployd/config.json, socket ownership |
prepare.yml:882–1007 |
the binary is installed but never runs as a service. nextdeployd ship is only a socket client, so ship exits with Error contacting daemon … (Is nextdeployd running?) |
| Doppler CLI |
prepare.yml:524 |
a deploy carrying a dopplerToken gets ExecStart=/usr/local/bin/doppler run -- … (process_manager.go:189) and the unit fails at start |
the fail2ban package itself |
prepare.yml:1026 |
the jails step checks for fail2ban-client, finds nothing, and warns — so the ban-on-abuse layer is silently off while prepare reports success |
Reproduction
On a fresh Debian/Ubuntu VPS with only a sudo-capable user:
$ nextdeploy prepare
...
Host prepared. Run `nextdeploy ship` from your project to deploy.
$ systemctl is-active nextdeployd
Unknown unit: nextdeployd.service
$ command -v caddy
(nothing)
$ nextdeploy ship
... Error contacting daemon: dial unix /run/nextdeployd/nextdeployd.sock: connect: no such file or directory (Is nextdeployd running?)
Impact
This is the new-user path. prepare is the one-time bootstrap, and it currently claims a host is ready when it is not — the failure surfaces later, from a different command, with an error that points at the daemon rather than at provisioning.
Workaround until fixed: bootstrap once with nextdeploy prepare --ansible, then use agent mode for subsequent runs.
Suggested fix
Add three step groups to Plan(), ordered edge → control plane → hardening (the daemon unit is After=caddy.service, and the hardening steps chown /var/log/caddy to the caddy user that installing Caddy creates):
- Caddy — install (apt: Cloudsmith repo; dnf/yum: COPR with a static-release fallback), ensure the Coraza module, seed a valid
/etc/caddy/Caddyfile + fragment dir, enable and start. The static fallback also needs to backfill the caddy user and a unit, which the packages supply.
- Control plane — write
/etc/nextdeployd/config.json with a random HMAC secret at 0600, install the unit (RuntimeDirectory=nextdeployd is load-bearing — it creates /run/nextdeployd before ExecStart), start it, wait for the socket to bind, then root:nextdeploy / 0660.
- Doppler and the fail2ban package — both optional, same rationale as Bun: an unreachable installer shouldn't fail a run for a host that never uses the feature.
One deliberate deviation from the playbook is worth considering: it builds the Coraza-enabled Caddy with xcaddy, which is why it also installs a full Go toolchain on the target. Caddy's download API (caddyserver.com/api/download?os=linux&arch=…&p=github.com/corazawaf/coraza-caddy/v2) serves the same binary prebuilt, which removes the build dependency and collapses the apt and rpm paths into one.
An implementation along these lines is prepared locally (new services.go with the step groups, plus tests covering idempotency, the static-fallback path, socket ownership, and secret stability) and is not yet pushed.
Summary
Since
18591d1(feat(prepare)!: provision over static Go agent),nextdeploy preparedefaults to the Go agent path. That path provisions the runtimes and the NextDeploy directory tree, but not Caddy, thenextdeploydservice, or Doppler — all of which the Ansible playbook it replaced does provision.On a host that has never been prepared with
--ansible,preparereports success and the firstnextdeploy shipthen fails.What's missing
Plan()(daemon/internal/prepare/plan.go:90) emits: package index refresh, base packages, thenextdeployuser/group,/opt/nextdeploy/{,apps,uploads,tmp}, Node/Corepack/Bun,/var/log/caddy+/etc/caddy/nextdeploy.d, the logrotate policy, and the fail2ban filters/jails.prepare.yml:558–800caddyManager.Validate()fails duringactivateRelease. Every generated fragment carries acoraza_wafblock andEnsureMainCaddyfilewritesorder coraza_waf first, neither of which a stock Caddy can parse — and on a host with no Caddy at all, nothing servesnextdeployd.service,/etc/nextdeployd/config.json, socket ownershipprepare.yml:882–1007nextdeployd shipis only a socket client, so ship exits withError contacting daemon … (Is nextdeployd running?)prepare.yml:524dopplerTokengetsExecStart=/usr/local/bin/doppler run -- …(process_manager.go:189) and the unit fails at startfail2banpackage itselfprepare.yml:1026fail2ban-client, finds nothing, and warns — so the ban-on-abuse layer is silently off while prepare reports successReproduction
On a fresh Debian/Ubuntu VPS with only a sudo-capable user:
Impact
This is the new-user path.
prepareis the one-time bootstrap, and it currently claims a host is ready when it is not — the failure surfaces later, from a different command, with an error that points at the daemon rather than at provisioning.Workaround until fixed: bootstrap once with
nextdeploy prepare --ansible, then use agent mode for subsequent runs.Suggested fix
Add three step groups to
Plan(), ordered edge → control plane → hardening (the daemon unit isAfter=caddy.service, and the hardening steps chown/var/log/caddyto thecaddyuser that installing Caddy creates):/etc/caddy/Caddyfile+ fragment dir, enable and start. The static fallback also needs to backfill thecaddyuser and a unit, which the packages supply./etc/nextdeployd/config.jsonwith a random HMAC secret at0600, install the unit (RuntimeDirectory=nextdeploydis load-bearing — it creates/run/nextdeploydbeforeExecStart), start it, wait for the socket to bind, thenroot:nextdeploy/0660.One deliberate deviation from the playbook is worth considering: it builds the Coraza-enabled Caddy with
xcaddy, which is why it also installs a full Go toolchain on the target. Caddy's download API (caddyserver.com/api/download?os=linux&arch=…&p=github.com/corazawaf/coraza-caddy/v2) serves the same binary prebuilt, which removes the build dependency and collapses the apt and rpm paths into one.An implementation along these lines is prepared locally (new
services.gowith the step groups, plus tests covering idempotency, the static-fallback path, socket ownership, and secret stability) and is not yet pushed.