Saltar al contenido

Docs / Usa tus propias máquinas

Bring your own boxes

Enrol a VM into your fleet: requirements, the join command, hardening, and what never touches the box.

The compute is yours. The control plane is ours.

That distinction is the whole of this page. developerz.ai is delivered as SaaS, there is no self-hosted edition of the platform, and there will not be one. What is self-hosted is the part that does the work: AI-dev sessions, CI jobs and code review all execute on VMs you own, enrolled into your fleet. We dispatch; your hardware runs.

What a box needs

Checked by the installer before anything is downloaded, and re-checked server-side before your enrolment token is spent:

Requirement Value
OS Ubuntu (the installer refuses anything else)
Architecture x86_64, aarch64 or arm64
RAM ≥ 3800 MiB, a nominal 4 GB VM qualifies
Free disk on / ≥ 20 GiB
Privileges root, or sudo without a password prompt
Docker required (installed for you if absent)

Inbound ports: none. The runner dials out; nothing listens for us. You may keep SSH open for yourself, and the installer's default hardening will make sure your detected SSH port stays allowed before it turns the firewall on.

Outbound, the box needs to reach the fleet transport (nats.developerz.ai:4222, TLS), github.com, and whichever model endpoint your key uses.

Enrolling

Two transports. Pick the one that matches how you like to hand over trust.

1. Token join: you run one command

Mint an enrolment token in the dashboard under Fleet → Add box, then on the VM:

read -rsp 'Enrollment token: ' DZ_ENROLL_TOKEN && DZ_JOIN="$(mktemp)" && \
  curl -sfL -H "X-Dz-Enroll-Token: $DZ_ENROLL_TOKEN" \
    https://api.developerz.ai/join -o "$DZ_JOIN" && bash "$DZ_JOIN"

The token travels in a header, not in the URL, so it does not land in proxy logs or shell history the way a query parameter would. The script is downloaded to a temp file and then executed, deliberately not curl … | bash, so a truncated download cannot run half an installer.

The script gates on the requirements above, installs git, unzip, gnupg, gh, Docker and Bun, applies the baseline firewall, installs the OS upkeep timers, writes its credentials to /etc/dz-runner (mode 0700, files 0600), and enrols.

It does not install the dz-runner service itself, see after the join below. The last thing it prints is what it actually observed on the box rather than what it hopes happened, so you never have to guess which of the two you got.

2. SSH pull: you hand over coordinates, we dial in

If you would rather not paste a secret into a shell, register the host and let the platform connect:

curl -X POST https://api.developerz.ai/v1/runners/enrollments/ssh \
  -H "Authorization: Bearer dev_pat_…" \
  -H "content-type: application/json" \
  -d '{"host":"box1.example.com","user":"root"}'

Scope: write:runners. The operator never holds a token in this mode, it is minted and consumed inside the platform, and it is not in the response.

Either transport prepares the machine, trades your one-time token for the box's permanent credential, installs the coding agent and the signed dz-runner binary, and starts the dz-runner service. The box reports into Fleet → Boxes within one heartbeat.

After the join

One command is the whole thing. It used to be two, with us installing the service for you, and that is no longer the case.

The runner binary is fetched from our signed release channel and verified before it is installed: its SHA-256 must match the release manifest, and its detached Ed25519 signature must verify against the signing key your box was handed during enrolment. There is no flag to skip that. If anything about the build cannot be verified (no release channel configured, no systemd on the host, an architecture we publish no build for, an OpenSSL too old to check an Ed25519 signature), the script installs nothing and says which one it hit. A box left without a service is loud and recoverable; a box running bytes nobody could attribute is neither.

So the join script still ends by saying which state it left the box in, and that report is the thing to read:

What it printed What it means
box <id> is ONLINE the service is running; the box reports in within one heartbeat
ENROLLED, BUT NOT BEATING the credential is written, the service is not installed yet
UNOBSERVABLE this host has no systemctl, so the script cannot tell either way

On either of the last two, the script's own output names the reason directly above that line. Fix it and re-run the join, but note the token: re-running needs a fresh one, because the one you used is spent. Leave /etc/dz-runner in place either way, and contact us with the runner id if the reason is ours.

Re-running the join on a box that is already running is safe: the binary is updated in place and the service is deliberately left alone, because it may be holding an assignment. The runner checks the channel before it takes its next piece of work, so it converges on the new build by itself.

Hardening

The installer runs a hardening pass by default (DZ_HARDEN=0 opts out, and says so in its output rather than silently skipping):

  • UFW: your detected SSH port(s) and 22 are allowed first, then default deny inbound / allow outbound, then enable.
  • Docker bypass closed: Docker publishes ports by writing iptables rules that sit in front of UFW. A DOCKER-USER block is injected into /etc/ufw/after.rules that returns for established traffic, loopback and RFC 1918, then drops. This is the difference between "I enabled a firewall" and "my Postgres container is not on the public internet".
  • Exposed-service scan: common data-store ports are checked for non-loopback listeners and reported. Nothing of yours is stopped or reconfigured, the scan tells you, you decide.

Once the runner service is installed, it runs as an unprivileged dz-agent system user under a locked-down systemd unit: no new privileges, an empty capability set, ProtectSystem=strict, private /tmp, and a memory/CPU ceiling.

What never touches the box

  • The GitHub App private key. It is not on your box and not on the runner path at all. CI jobs receive a repo-scoped installation token minted just in time with a 15-minute TTL.
  • Plaintext secrets. Vault entries in a task assignment are encrypted blobs; the assigner that routes them does not decrypt them.
  • Your source, permanently. Work happens in a workspace on your own disk.

Updates

The mothership pushes an update request; the runner unsubscribes, drains in-flight work (bounded at 30 seconds), downloads the new binary and its detached signature, verifies the Ed25519 signature, atomically replaces itself, and exits into Restart=always.

There is no git pull, no build step on your VM, and no unsigned artefact path. If the signature does not verify, the update does not happen.

Presence and draining

Each box heartbeats every 30 seconds into a key with a 90-second TTL, liveness is the key existing, not a timestamp in a table. On a graceful drain the heartbeat continues until in-flight work settles and the key is then left to lapse rather than deleted, so nothing can be double-executed mid-drain.

To take a box out of rotation:

box drain <box_id>     # MCP verb; in-flight tasks are re-enqueued

To retire it for good, revoke it: POST /v1/runners/:id/revoke.

How many boxes you may enrol

One AI-dev seat allows three enrolled servers, inclusive of the box running the AI-dev work itself, the other two are yours for CI, tests and review at no extra charge. Boxes are fungible. Trying to enrol past the cap returns 402 Payment Required rather than quietly accepting the box; see billing.