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.
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 updateruns on the device at any point in the first-boot path. - No
pip install,npm install,cargo install,go install, orgem installruns at any point in the first-boot path. - No
git cloneof agent code runs on the device. Ever. - The only network traffic on a first boot is, in this order:
- DHCP, DNS, NTP, mDNS broadcast.
- (Optional) An API health-check against the model provider you chose, to validate your key.
- (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.