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, writes its credentials to
/etc/dz-runner (mode 0700, files 0600), and enrols.
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 way the box appears in Fleet → Boxes once it reports in.
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-USERblock is injected into/etc/ufw/after.rulesthat 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.
The runner itself 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.
Related
- Self-hosted CI runners — the
runs-on:labels and the CI security model. - Security — scopes, encryption and the audit log.
- Billing — what a seat includes.