Skip to content

Blog

Why deputyOS does zero first-boot installs

The single design rule that shapes the deputyOS image: when the device boots, there's nothing left to install. Here's why that matters and what it cost us.

6 min read deputyOS team

The biggest single design decision in deputyOS is the one with the boring name: zero first-boot network installs. The image at rest is the complete machine. When the SD card boots, there is nothing to download.

This sounds obvious. It is not how most “easy install” stories work in practice. Most are a script that runs apt update, apt install, pip install -r requirements.txt, npm install, git clone, and cargo build in sequence. Each of those can fail. Each of them has, for someone, on some Sunday afternoon, on some Pi, with some flaky upstream.

We don’t do any of that. Here’s why, and how it works.

What “zero first-boot installs” actually means

Concretely:

  • No apt update runs on the device at any point in the first-boot path.
  • No pip install, npm install, cargo install, go install, or gem install runs at any point in the first-boot path.
  • No git clone of agent code runs on the device. Ever.
  • The only network traffic on a first boot is, in this order:
    1. DHCP, DNS, NTP, mDNS broadcast.
    2. (Optional) An API health-check against the model provider you chose, to validate your key.
    3. (Optional) The first channel webhook handshake (Telegram, Slack, etc.).

Everything else — runtimes, native modules, model files, ClamAV signatures, Magika model, AppArmor profiles, systemd units — is already in the image, with the right SHAs, with the right linker paths, with the right capability bits set.

Why this matters more than you’d think

Installation flake doesn’t scale

If 95% of your installs succeed, and you have 1000 users, 50 of them have a bad day. If the agent is something like a personal AI assistant — meant for friends, family, and small teams — those 50 users will mostly walk away. The technical part of “make installation work” is the un-skippable part of making the product useful.

Mirrors break, certs lapse, registries 503

registry.npmjs.org has, in the past three years, had non-trivial regional outages. pypi.org has had package-name confusion incidents. Debian mirrors silently rotated their signing keys. None of these are unsolvable problems, but each one breaks a first-boot install for hours.

deputyOS pins SHAs in the build host, not the device. The build host pulls from upstream once, verifies, vendors the bytes into the image, and signs the image. The device never sees a public registry.

Air-gapped mode is a flag, not a project

Because we already vendor everything, the air-gapped tier is just “use a baked-in apt mirror + nftables-deny egress” plus the LFM2 model bake. We didn’t have to redesign the agent or its dependencies — they were already self-contained. Most projects that try to add air-gapped support after the fact spend months fighting apt install cycles.

Reproducible builds become tractable

For two independent build hosts to produce byte-identical images, they need to consume the same inputs. If first boot resolves latest versions from public registries, the device’s actual contents diverge unpredictably across two boots. With everything baked, the input set is the git SHA plus a small list of pinned external sources — and that’s reproducible. We’re working toward SLSA L3 verified-by-third-party builds for exactly this reason.

Updates become atomic

If first boot can install random things, the update path can too. Then a half-applied update is a permanent broken state. With everything in the image, updates are just “swap to the other A/B partition and reboot” — atomic at the device, predictable from the user’s side.

What this cost us at build time

Every category of dependency had to be solved at bake time, with the right tool for the job.

Node.js native modules

@discordjs/opus won’t compile on arm64 against random toolchains. Pre-built binaries from npm are not always available for every (arch, Node version) combo. We use a build container that mirrors the device’s target arch + libc and runs npm rebuild against the actual native toolchain we’ll ship. The resulting node_modules/ tree is committed (in the image — not in the repo) so the device never needs to rebuild anything.

Python wheels

We pre-resolve every wheel for the device’s (arch, glibc) tuple via uv pip download --target=…. Wheels that don’t exist for arm64 (Magika did this for a while) get built from source on the build host. The result is a site-packages/ tree ready to use.

CMake and ld toolchains

llama.cpp builds against the target arch with the right CPU feature flags (NEON on arm64, AVX2/AVX-512 detection on x86_64). We produce one binary per target hardware variant — not “one universal binary” — because the speedup matters.

ClamAV signatures

The signature DB is large and updates daily. We bake the latest signatures into every image revision. New signatures arrive via the standard image update path. Devices don’t run freshclam; they take signed image updates instead. See ADR-0008.

Magika model file

Magika’s content-type classifier is a tiny TF model file. We bake it. Lifetime is years, not days.

AppArmor profiles + AppArmor enforce

The profiles live in /etc/apparmor.d/deputyos.<profile> and load in enforce mode at boot. The build process tests them against a smoke harness before signing the image.

A wizard with no installer

The first-boot wizard at deputyos.local:8088 only does configuration: hostname, timezone, Wi-Fi, profile, provider key, channels, backup bucket. It writes systemd unit files from templates that are already in the image. It writes secrets.env. It doesn’t compile, fetch, or build anything.

What this does not mean

It does not mean the image is frozen. Updates happen — they just travel as signed image revisions through deputyctl update --check. It does not mean you can’t add things. You can build your own profile by adding a manifest + an Ansible bake recipe + an AppArmor profile + a systemd unit (no Rust changes — see add-a-profile). It does not mean you can’t customize. Channels, hooks, skills, and providers are all knobs the wizard exposes.

It just means: the device, when it boots, is finished.

What kind of project this turns deputyOS into

The first-class output of deputyOS is the image, not the codebase. The codebase is the build pipeline that produces the image. This reframes a lot of decisions:

  • The git repo is a means; the image is the product.
  • A successful refactor is one that changes the image’s properties (smaller, more secure, more variants) without breaking the bake.
  • A passing CI run includes a built and smoke-tested image, not just unit tests.
  • A release is a tag → a workflow → a signed manifest → publishable artefacts on the CDN.

This is closer in spirit to how operating-system distros think than how typical Node/Python apps do. We adopted that framing deliberately. It’s slower per-feature but the resulting product is qualitatively different.

What you do with this

For most users this is invisible — the wizard does its thing, the device works, the agent responds. But if you’re considering self-hosting an AI assistant, this is the part of deputyOS that’s hardest to copy in a weekend project. It’s also the part that determines whether your Pi is still running unattended six months from now.

Try the picker to see your hardware’s image. Or read the architecture doc for how the build pipeline works in detail.

Related posts

Search

Search index isn't built yet. Run npm run build to generate it (Pagefind writes to dist/pagefind/).