Skip to main content

Core Concepts

Avocado OS is built around a few core concepts that work together to deliver a composable, immutable embedded Linux platform. This page introduces each concept and how they relate.


Yocto + BSPs ──▶ Package Feeds (RPM) <-- we do this for you
│
▼
avocado.yaml <-- your config
┌─────────────────────────┐
│ target: raspberrypi5 │
│ │
│ runtimes: │
│ dev: │
│ extensions: │
│ - my-app │ <-- your app
│ - avocado-ext-dev │
└─────────────────────────┘
│
▼

avocado install resolve and install packages
│
▼
avocado build compile, image, sign extensions
│
▼
avocado provision flash to hardware
│
▼

On-Device (hardware) <-- your os
┌─────────────────────────┐
│ /var BTRFS read-write │
├─────────────────────────┤
│ systemd-sysext merge │ extensions → /usr, /etc
├────────────┬────────────┤
│ rootFS (A) │ rootFS (B) │ immutable, read-only
├────────────┼────────────┤
│ BSP+K (A) │ BSP+K (B) │ kernel, initramfs
├────────────┼────────────┤
│ Boot (A) │ Boot (B) │ bootloader
└────────────┴────────────┘

Filesystem technology for the rootFS and extensions is configurable in avocado.yaml — erofs is the default.


avocado.yaml​

The avocado.yaml file is the single source of truth for your Avocado project. It declares everything — your target hardware, your runtimes, your extensions, your SDK configuration. One file defines your entire system. As Justin puts it, "it feels a lot like infrastructure as code."

target: raspberrypi5 # ◀ target hardware

runtimes: # ◀ named deployment profiles
dev:
extensions:
- my-app
- avocado-ext-dev
prod:
extensions:
- my-app
- monitoring

extensions: # ◀ your application + dependencies
my-app:
types:
- sysext
- confext
version: '1.0.0'
packages: # ◀ packages from Avocado feeds
curl: '*'
openssl: '*'
overlay: overlays/my-app # ◀ your files, configs, binaries
enable_services: # ◀ systemd services to start
- my-app.service
sdk:
packages: # ◀ host-side cross-compilation tools
nativesdk-rust: '*'

This configuration assembles what you want in your image. Change a line, run avocado build, and you have a new image in minutes. The config can also be multi-target — decorate it so that the same project builds for Raspberry Pi, Jetson, or NXP with target-specific extensions resolved automatically.


Runtimes​

Runtimes define which extensions are composed together for a given deployment. Think of them as named profiles — different combinations of the same building blocks for different purposes.

runtimes: # ◀ define as many as you need
dev: # ◀ for engineering
extensions:
- my-app
- avocado-ext-dev # SSH, debugging tools
- monitoring
prod: # ◀ for production
extensions:
- my-app
- monitoring
factory: # ◀ for manufacturing
extensions:
- my-app
- eol-tests # end-of-line hardware tests
- provisioner

A dev runtime includes SSH access and debugging tools so engineers can iterate on hardware. A prod runtime is stripped to essentials. A factory runtime runs end-of-line unit tests during manufacturing — did the camera enumerate? Is the network interface up? — then hands off to the production runtime.

Switch between runtimes with one command. The same extensions, different compositions. Every runtime produces a deployable image that's deterministic and reproducible.


Extensions​

Extensions are the building blocks of the system. Every piece of user-defined functionality ships as an extension — application binaries, configuration files, kernel modules, systemd services. Avocado's extension system builds on systemd's sysext and confext mechanisms, which means extensions inherit dm-verity integrity checking and LUKS encryption support out of the box. The secure boot chain extends all the way through the extension layer.

Extension types​

System Extensions (sysext) extend the /usr/ and /opt/ hierarchies. Use sysext for application binaries, libraries, systemd service units, and kernel modules.

Configuration Extensions (confext) extend the /etc/ hierarchy. Use confext for configuration files, user accounts, network settings, and service configurations.

An extension can be both simultaneously:

extensions:
my-app:
types: # ◀ extension types
- sysext # binaries → /usr/bin/, /usr/lib/
- confext # config → /etc/my-app/, /etc/systemd/
version: '1.0.0'

Defining extensions​

Extensions are declared in your avocado.yaml. Declare what packages you need, what files to overlay, what services to enable — Avocado handles the rest. Application developers declare their dependencies directly, without going back to a systems integration team. That's the bottleneck this architecture eliminates.

extensions:
my-app: # ◀ your application
types:
- sysext
- confext
version: '1.0.0'
packages: # ◀ declare dependencies
curl: '*'
gstreamer1.0: '*'
overlay: overlays/my-app # ◀ your compiled binaries + configs
enable_services: # ◀ start on boot
- my-app.service
- my-app-healthcheck.timer
sdk:
packages: # ◀ cross-compilation tools
nativesdk-rust: '*'

networking-tools: # ◀ a simple tools extension
types:
- sysext
version: '1.0.0'
packages:
wget: '*'
iperf3: '*'
tcpdump: '*'

External extension sources​

Extensions can be sourced from the Avocado package repository, git repositories, or local paths:

extensions:
avocado-ext-dev: # ◀ from Avocado package repo
source:
type: package
version: '*'

custom-drivers: # ◀ from your own git repo
source:
type: git
url: https://github.com/org/extensions.git
ref: v1.2.0

shared-libs: # ◀ from a local path
source:
type: path
path: ../shared/extensions/libs

Extension merging at runtime​

At boot time, avocadoctl merges extensions into the running system using systemd-sysext and systemd-confext. The base system remains immutable — extensions overlay on top, making their files visible at /usr/, /opt/, and /etc/ without modifying the read-only root filesystem.

This is fundamentally different from Docker containers. There's no isolation layer, no network namespace overhead, no --privileged flags to pass. As Justin explains — embedded systems are single-purpose special devices running a special task. You don't want isolation. You want integration. Systemd extensions give you the composability of containers without pretending your device is a cloud server.


Packages​

Avocado uses Yocto as an elaborate package builder — compiling the world into pre-built RPM packages so you never have to. These packages are not installed at runtime. They're composed into extensions at development time, producing deterministic images.

extensions:
my-app:
packages: # ◀ runtime packages → on device
curl: '*'
openssl: '*'
python3: '*'
sdk:
packages: # ◀ SDK packages → host-side tools
nativesdk-rust: '*'
nativesdk-go: '*'

Package feeds​

Package feeds are organized by architecture and target. Packages built generically for ARMv8 are available to any ARMv8 target. If a specific target has a machine-specific override — say, a hardware-accelerated video encoder — the target-specific package takes priority automatically.

Under the hood, Avocado uses DNF5 to resolve packages from RPM repositories. The feeds are versioned and immutable — the same feed version always produces the same packages. This is what makes builds deterministic.

Managed and unmanaged packages​

Managed packages are served from Avocado's repositories. Most are public and generally available.

don't see what you want?

If you wish a package was available from Avocado, please submit a request. Enterprise users can reach out via Slack, community users via Discord.

Unmanaged packages are built and included by you — via overlay files, cross-compilation in the SDK, or sourced from your own private RPM repositories.


Overlay​

Overlays let you include arbitrary files in your extension — application binaries you've compiled elsewhere, configuration templates, static assets, anything. The overlay property points to a directory whose structure mirrors where files end up on the device:

extensions:
my-app:
types:
- sysext
- confext
version: '1.0.0'
overlay: overlays/my-app # ◀ point to your files

The directory structure:

overlays/my-app/
├── usr/
│ └── bin/
│ └── my-app # → /usr/bin/my-app (sysext)
├── etc/
│ ├── my-app/
│ │ └── config.toml # → /etc/my-app/config.toml (confext)
│ └── systemd/
│ └── system/
│ └── my-app.service # → systemd service unit

Files under usr/ become part of the sysext image. Files under etc/ become part of the confext image. This is how you bring your own compiled binaries, scripts, models, or any file that isn't covered by a package.


systemd​

Avocado OS is built on systemd and leverages it throughout the system — from extension merging (systemd-sysext/confext) to service management, user creation (systemd-sysusers), and boot orchestration. Your application runs as a standard systemd service. No proprietary abstractions, no container runtimes — just Linux.

extensions:
my-app:
types:
- sysext
- confext
version: '1.0.0'
enable_services: # ◀ started automatically at boot
- my-app.service
- my-app-healthcheck.timer

The corresponding service unit lives in your overlay:

# overlays/my-app/etc/systemd/system/my-app.service
[Unit]
Description=My Application
After=network-online.target

[Service]
Type=simple
ExecStart=/usr/bin/my-app
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

When the extension is merged at boot, systemd picks up the service unit and starts it. Standard systemctl commands work — status, logs, restart. Everything that exists in the Linux ecosystem for managing systemd services applies directly.


Lifecycle Hooks​

Extensions can run custom commands at key points in their lifecycle. Use on_merge for commands that run after the extension is activated, and on_unmerge for cleanup before deactivation:

extensions:
my-app:
version: '1.0.0'
on_merge: # ◀ runs after extension activates
- systemctl restart --no-block my-app.service
on_unmerge: # ◀ runs before deactivation
- systemctl stop my-app.service

Certain hooks are added automatically based on extension contents — you don't need to declare these:

  • Extensions containing kernel modules automatically run depmod
  • Extensions containing shared libraries automatically run ldconfig
  • Extensions containing sysusers.d configuration automatically run systemd-sysusers

This means that if your extension ships a kernel module for a custom driver, the module dependency tree is rebuilt automatically when the extension merges. If it ships a shared library, the linker cache is updated. The system does the right thing without you having to remember the incantations.