Skip to content

prepare: agent mode does not install Caddy, the nextdeployd service, or Doppler — a fresh host fails on first ship #47

Description

@aynaash

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions