Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 67 additions & 1 deletion src/docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const config = {
organizationName: 'peridio',
projectName: 'peridio-docs',
trailingSlash: false,
clientModules: ['./src/clientModules/copyInlineCode.js'],
clientModules: ['./src/clientModules/copyInlineCode.js', './src/clientModules/fieldNotesGate.js'],
i18n: {
defaultLocale: 'en',
locales: ['en'],
Expand Down Expand Up @@ -57,6 +57,30 @@ const config = {
breadcrumbs: true,
},
],
[
'@docusaurus/plugin-content-blog',
{
id: 'field-notes',
routeBasePath: 'field-notes',
path: 'field-notes',
blogTitle: 'Field Notes',
blogDescription: 'Dense technical notes from the Peridio engineering team.',
showReadingTime: true,
postsPerPage: 20,
blogSidebarCount: 0,
authorsMapPath: 'authors.yml',
exclude: ['CONTRIBUTING.md', '_template.mdx'],
feedOptions: {
type: 'all',
title: 'Peridio Field Notes',
description: 'Engineering notes from Peridio',
},
},
],
// Injects <meta name="robots" content="noindex,nofollow"> into every
// /field-notes HTML page. Feed deletion happens in scripts/build.sh
// because postBuild hooks run concurrently across plugins.
'./plugins/field-notes-preview-gate',
...(process.env.NODE_ENV === 'production'
? [
[
Expand Down Expand Up @@ -129,6 +153,12 @@ const config = {
position: 'left',
activeBasePath: 'developer-reference',
},
{
to: '/field-notes',
label: 'Field Notes',
position: 'left',
activeBasePath: 'field-notes',
},
{
to: '/changelog/latest',
label: 'Changelog',
Expand Down Expand Up @@ -200,6 +230,35 @@ const config = {
// document.documentElement.setAttribute('data-site-theme', 'avocado');
`,
},
{
// Synchronous /field-notes gate. Runs in <head> before any paint,
// so visitors without the preview flag never see field-notes content.
// The companion clientModule (fieldNotesGate.js) handles the body
// class for the navbar link; this is just the redirect.
tagName: 'script',
attributes: {},
innerHTML: `
(function () {
if (typeof window === 'undefined') return;
var path = window.location.pathname;
if (path !== '/field-notes' && path.indexOf('/field-notes/') !== 0) return;
try {
var params = new URLSearchParams(window.location.search);
var flag = params.get('preview');
if (flag === '1') {
window.sessionStorage.setItem('peridio:fn-preview', '1');
return;
}
if (flag === '0') {
window.sessionStorage.removeItem('peridio:fn-preview');
} else if (window.sessionStorage.getItem('peridio:fn-preview') === '1') {
return;
}
} catch (e) { /* sessionStorage unavailable — fall through to redirect */ }
window.location.replace('/');
Comment on lines +245 to +258
})();
`,
},
{
tagName: 'script',
attributes: {},
Expand Down Expand Up @@ -234,6 +293,13 @@ const config = {
href: 'https://fonts.googleapis.com/css2?family=Montserrat:wght@100..900&family=Space+Grotesk:wght@300..700&family=Spline+Sans:wght@300..700&display=swap',
},
},
{
tagName: 'link',
attributes: {
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Geist:wght@300..700&family=Geist+Mono:wght@400;500;600&display=swap',
},
},
{
tagName: 'link',
attributes: {
Expand Down
40 changes: 40 additions & 0 deletions src/field-notes/2026-06-15-power-pull-orin-nx.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
title: 'Pulling the power 200 times mid-OTA on an Orin NX'
description: "200 scripted power cuts at random points during an A/B OTA update. Zero bricks at steady state — and the two early failures weren't where we expected."
date: 2026-06-15
Comment thread
bbrock25 marked this conversation as resolved.
Comment on lines +1 to +4
authors: [jschneck]
Comment on lines +1 to +5
tags: [ota, rollback, jetson, orin-nx]
tested_against: 'Avocado 1.0.0, JetPack 7.2, Orin NX 16GB'
hn_title: "We cut power 200 times mid-update on a Jetson. Here's what bricked."
poster: jschneck
lift_for_blog: 'Zero bricks across 200 forced failures — the 100-device-wall fear, quantified, for the budget holder.'
promote_to_track: "Tutorial 4 candidate — the 'break it on purpose' step."
---

import TestedAgainst from '@site/src/components/TestedAgainst'

<TestedAgainst avocado="1.0.0" jetpack="7.2" boards={['Orin NX 16GB']} />

**TL;DR.** We scripted 200 power cuts at random points during an A/B OTA update on an Orin NX. Zero bricks — every device booted the last-known-good slot. Two early runs did fail, and the cause wasn't the bootloader.

{/* truncate */}

## What this is

Peridio builds Avocado OS, an immutable embedded Linux runtime for devices like NVIDIA Jetson. Its updates are A/B and atomic: a failed or interrupted update should always fall back to the previously working slot. We wanted to know how literally true "always" is, so we tried to break it on purpose.

## How we did it

[Relay-controlled PSU, the update payload, the randomized power-cut harness, commands + output.]

## What we learned

[200 forced interruptions, zero bricks at steady state; what the boot logs show on recovery.]

## What didn't work

[Two early failures traced to a data-partition sync gap, not the OS slots; the fix and why it mattered.]

## Reproduce it

[Link to the power-cycle harness repo + step-by-step.]
68 changes: 68 additions & 0 deletions src/field-notes/2026-06-16-rpi5-from-macos.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
title: 'Provisioning a Raspberry Pi from macOS natively, no Linux VM and no USB passthrough'
description: 'Flashing embedded boards from a Mac usually means a Linux VM and fragile USB passthrough. The Avocado Desktop preview provisions a Raspberry Pi 5 natively from macOS, so there is no VM to babysit and no passthrough to drop.'
date: 2026-06-16
authors: [jschneck]
tags: [rpi5, provisioning, macos, usb, preview]
tested_against: 'Avocado [ENGINEER: preview version], Raspberry Pi 5, macOS host [ENGINEER: macOS version and chip, Apple Silicon or Intel]'
hn_title: 'Provisioning a Raspberry Pi from macOS natively, no Linux VM (Avocado Desktop preview)'
poster: jschneck
lift_for_blog: 'The Linux-VM-and-USB-passthrough dance for flashing boards from a Mac, removed: native provisioning on the laptop the engineer already owns.'
promote_to_track: 'Tutorial candidate: native macOS provisioning of a Pi 5 as the first device bring-up on the on-ramp.'
---

import TestedAgainst from '@site/src/components/TestedAgainst'

<div style={{ aspectRatio: '16 / 9', marginBottom: '1.5rem' }}>
<iframe
style={{ width: '100%', height: '100%', border: 0 }}
src="https://www.youtube.com/embed/vrIe4CzTavs"
title="Provisioning a Raspberry Pi 5 from macOS with Avocado Desktop"
loading="lazy"
referrerPolicy="strict-origin-when-cross-origin"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
/>
Comment thread
Copilot marked this conversation as resolved.
Comment thread
bbrock25 marked this conversation as resolved.
</div>

<TestedAgainst avocado="[ENGINEER: preview version]" jetpack="" boards={['Raspberry Pi 5']} />

**TL;DR.** Provisioning an embedded board from macOS normally means running a Linux VM and passing the board's USB device through to it, which is fragile, more so on Apple Silicon. The Avocado Desktop preview provisions a Raspberry Pi 5 natively from macOS, no VM and no passthrough. [ENGINEER: confirm it is truly native (no VM, no passthrough), on which Mac architecture, and the install-to-boot time.]

{/* truncate */}

## The problem: provisioning a board over USB from a Mac

Most board bring-up happens over USB. The host puts the board into a recovery or device mode and pushes an image over the USB link. The vendor tooling that does this tends to assume Linux, so on a Mac the usual move is to run a Linux VM and pass the board's USB device through to it.

That passthrough is where it falls apart. USB resets and mode switches during flashing drop the device out of the VM, enumeration races leave the tool waiting on a device that is sitting on the host side, and Apple Silicon's virtualization stack makes all of it less predictable. [ENGINEER: keep only the failure modes you actually hit, on the board(s) you actually used. Do not ship a list of problems you did not personally see.]

Orientation for anyone new to this: Peridio makes Avocado OS, an immutable embedded Linux runtime shipped as a binary distribution, for boards like the Raspberry Pi 5 and NVIDIA Jetson. Avocado Desktop is the local tool that builds and provisions it. The developer preview is the macOS build rolling out now, ahead of the Avocado 1.0 release. [ENGINEER: confirm the rollout status and timing you want to state.]

## How Avocado Desktop does it

This is the technical core of the post, and it is the first thing the audience will ask about. [ENGINEER: the real mechanism. How does it drive the board's USB recovery or device mode from macOS without a VM and without passthrough? Native libusb on the Mac, a helper that owns the USB handshake, something else? How does it survive the USB reset or mode switch mid-flash that breaks the passthrough approach? Write the actual explanation here, not a summary. If there is any case where it still falls back to something Linux-side, say so in this section.]

The provisioning flow itself, on the Mac:

[ENGINEER: the provision command and its output, including the USB enumeration and the recovery or device-mode handshake. In the demo it prompts before overwriting the board, you confirm, and it streams the device output back to the host. Note the cable, the port, and any board prep on the Pi 5.]

[ENGINEER: paste the first boot log from the Pi 5, so the provision is a verified result and not a description.]

## What this gets you

[ENGINEER: replace each line with a result from a real run. Nothing here is a claim until it is verified.]

- The board provisioned from macOS with no VM and no USB passthrough. [ENGINEER: confirm, and on which Mac architecture.]
- The reliability angle is the real claim, not speed. [ENGINEER: if you have a before and after, for example how many passthrough attempts dropped versus how many native runs completed cleanly, put the real counts here. A reliability claim needs repeated runs, not one success.]
- This was a Raspberry Pi 5. Jetson uses a different and harder USB recovery flow and is supported but was not tested here. [ENGINEER: do not extend the USB win to Jetson unless you actually provisioned one.]

## What didn't work

This is a preview, and a note with no rough edges reads as a press release. [ENGINEER: required. Where did it break, stall, or surprise you? Likely spots: a specific macOS version or chip where it still misbehaves, a board or recovery mode it does not cover yet, permissions prompts on macOS, or a USB quirk it does not fully handle. If it genuinely did not break across your runs, say how many runs that was and what is still out of scope.]

## Reproduce it

Developer preview, macOS, Homebrew. [ENGINEER: the exact brew tap and install command, and whether early access is an open tap or gated by a signup or token. Include prerequisites such as Xcode command line tools and a minimum macOS version, then the step list to provision a Pi 5 from the Mac. Note the Mac architecture you verified on, since USB behavior differs between Apple Silicon and Intel.]

Docs and the rest of the Peridio ecosystem are at [docs.peridio.com](https://docs.peridio.com). [ENGINEER: confirm the preview landing URL.]
108 changes: 108 additions & 0 deletions src/field-notes/2026-06-17-pre-seeded-docker-cache.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
---
title: 'Shipping a device that runs its containers on first boot, with no internet'
description: "Avocado pre-pulls container images into the device's writable /var at build time, inside a temporary dockerd running inside the SDK container. The device boots offline and starts its stack on first power-on, no registry pull needed."
date: 2026-06-17
authors: [jschneck]
tags: [var, docker, offline-first, build, provisioning]
tested_against: 'Avocado [ENGINEER: version], SDK [ENGINEER: image:tag], [ENGINEER: board(s) verified — e.g. Raspberry Pi 5, Jetson Orin NX]'
hn_title: "We pre-seed an embedded device's /var partition with its Docker cache at build time"
poster: jschneck
lift_for_blog: "The 'factory floor with no internet' problem: every Avocado device ships with its container images already in /var, so first power-on starts the stack without a registry pull."
promote_to_track: 'Tutorial candidate after the Pi 5 bring-up: declaring docker_images on an extension and watching the cache come up populated on the device.'
---

import TestedAgainst from '@site/src/components/TestedAgainst'

<TestedAgainst
avocado="[ENGINEER: version]"
jetpack=""
boards={['[ENGINEER: board(s) verified]']}
/>

**TL;DR.** A long-standing pain in embedded is that a device booting on a factory floor, behind a customer firewall, or in the field can't reliably reach a container registry — so any stack that relies on `docker pull` to start is dead on arrival. We bake the image cache into the device's writable partition at build time, so on first power-on the containers are already there. [ENGINEER: confirm with a real first-boot log and the install-to-running-stack time on the board you used.]

{/* truncate */}

## Why this is hard

If your device runs containers (and most embedded products do now), there's a window between first power-on and the moment your application starts. The conventional path is: device boots, network comes up, `docker pull`, containers start. Each step is a place the bring-up can stall — and "network comes up" is the one outside your control. Customers have firewalls. Factories don't have WiFi. Cellular modems take their time. And nothing makes a field engineer happier than a brand-new device that won't run its app because it can't reach `registry-1.docker.io`.

Orientation for anyone seeing this for the first time: Avocado OS is the immutable embedded Linux runtime we ship at Peridio as a binary distribution, for boards like the Raspberry Pi 5 and NVIDIA Jetson. An Avocado image has two storage areas — a read-only root assembled from extension images, and a writable `/var` partition that survives OTA updates. The trick this note is about is **pre-populating `/var` at build time** so the device ships with everything it needs on first boot, including its container image cache.

## How it works

You declare the images you want on an extension:

```yaml
extensions:
my-app:
version: '1.0.0'
docker_images:
- image: docker.io/library/redis
tag: '7-alpine'
- image: docker.io/library/nginx
tag: '1.25'
var_files:
- var/lib/docker/** # keep this out of the read-only .raw
```

During `avocado build`, the CLI sees `docker_images` on an extension and:

1. Adds `--privileged` to the SDK container args (required for Docker-in-Docker)
2. Starts a temporary `dockerd` _inside_ the SDK container, with its data-root pointed at the staging area that will become the device's `/var`
3. Pulls each declared image for the target architecture (`linux/arm64`, `linux/amd64`, …)
4. Shuts down the temporary `dockerd`
5. Packs the now-populated Docker data-root into the var partition image

The `var_files: var/lib/docker/**` on the extension is the other half of the trick. Without it, `avocado ext image` would helpfully bake the Docker storage directory into the read-only erofs image, which is exactly what you do _not_ want — Docker needs to write to that directory on the device. The exclusion tells `ext image` to leave it out of the read-only `.raw` so the writable copy on `/var` is the one Docker sees.

[ENGINEER: paste the relevant snippet of `avocado build` output that shows the dockerd-in-SDK pull happening, and the line where it finalizes the var partition image.]

## On the device

[ENGINEER: paste the first-boot log on the board you used. The thing that matters: `docker images` listing the pre-pulled images with no registry round-trip, and the application container starting cold. If you have an air-gap repro — pull the network cable, power on, watch it come up — include the timing.]

## What this gets you

- **Containers running on first power-on with no network.** [ENGINEER: confirm on a board you actually booted offline, and note install-to-running-stack time.]
- **One build artifact, one provision step.** The image cache isn't a second deploy — it's part of the OS image and ships with it. Same `avocado provision` as everything else.
- **Atomic OTAs that don't touch the cache.** Updates swap the read-only root; `/var` (including the Docker cache) persists across updates, so the cache is also a hedge against a partial OTA leaving you without container layers.

## What's also worth knowing

Same `var_files` mechanism on a runtime block lets you seed _any_ static file onto `/var` at build time — TLS certs, default app config, seed data for an embedded database, anything the device needs writable on first boot. Docker is just the loudest case because of the multi-gigabyte cache size and the cost of pulling it again.

```yaml
runtimes:
dev:
extensions: [my-app]
var_files:
- source: certs/device.pem
dest: lib/myapp/certs/device.pem
- source: config/app-defaults/
dest: lib/myapp/
```

The seeding-var guide in the docs has the full configuration reference: [Seeding the var partition](/developer-reference/seeding-var).

## What didn't work

[ENGINEER: required, and the most credible part of this note. Real failure modes I'd ask you to consider:

- A glob in `var_files` that was too narrow and let Docker storage leak into the read-only `.raw`, which surfaces as a confusing erofs-write error at runtime rather than a build failure.
- Multi-arch pulls picking the wrong platform when the SDK and target architecture don't match, especially on Apple Silicon hosts pulling `linux/arm64` into the SDK.
- SDK images missing `dockerd` / `containerd` / `runc`, which only shows up the moment the privileged step kicks in.
- The var partition staging size blowing past a sensible default and the build erroring out late.
Pick the one you actually hit and write the diagnosis + the fix. If nothing broke, say across how many runs and what you'd still expect to bite someone.]

## Reproduce it

```bash
avocado install
avocado build # var seeding (static + docker) happens here, no extra step
avocado provision dev
```

The seeding runs as part of `avocado build` (specifically `avocado runtime build`) — there's no separate `avocado seed-var` command, and there shouldn't be. Full configuration reference: [Seeding the var partition](/developer-reference/seeding-var).

Docs and the rest of the Peridio ecosystem are at [docs.peridio.com](https://docs.peridio.com).
Loading