Getting started with add-ons
Add-ons (shown as Apps in the ApexOS UI) are containerized services that run next to ApexOS on the same device, managed by the Supervisor. An add-on is defined by a small directory of files: a metadata file that tells the Supervisor what the add-on needs, a Dockerfile that describes how the container image is built, and whatever scripts and assets your service requires at runtime.
This page walks through the anatomy of an add-on, the base images you build on, and how to develop an add-on locally against a real device.
Anatomy of an add-on
A complete add-on directory in the official store looks like this (taken from
the real samba and ssh add-ons):
my_addon/
├── config.yaml # Required. Add-on metadata and Supervisor contract
├── build.yaml # Optional. Build-time configuration (base image, args)
├── Dockerfile # Required for locally built add-ons
├── rootfs/ # Files copied into the container image
│ └── etc/... # init scripts, service definitions, templates
├── DOCS.md # End-user documentation, rendered in the UI
├── CHANGELOG.md # Version history, rendered in the UI
├── README.md # Store/long description
├── icon.png # Square icon shown in the store
├── logo.png # Wide logo shown on the add-on page
├── apparmor.txt # Optional. Custom AppArmor profile
└── translations/
└── en.yaml # UI labels/descriptions for configuration options
config.yaml
The heart of every add-on. It declares identity (name, slug, version,
description), supported architectures, requested privileges, port mappings,
folder mounts, user-facing options and their validation schema. The Supervisor
validates this file against a strict schema and silently drops unknown keys.
YAML (.yaml/.yml) and JSON (.json) are both accepted. See the
configuration reference for every key.
A minimal example:
name: Example add-on
version: "1.0.0"
slug: example
description: My first add-on
arch:
- aarch64
- amd64
startup: services
options:
message: Hello world
schema:
message: str
build.yaml
Optional build-time configuration, validated separately from config.yaml.
It supports four keys:
| Key | Type | Meaning |
|---|---|---|
build_from | string, or map of arch → image | Base image passed to the Dockerfile as BUILD_FROM |
args | map of string → string | Extra --build-arg values |
labels | map of string → string | Extra image labels |
squash | bool (default false) | Ignored on modern builders (BuildKit); a warning is logged |
The per-architecture form is what the official add-ons use:
---
build_from:
aarch64: ghcr.io/apexinfosysindia/base:3.23-2026.06.1
amd64: ghcr.io/apexinfosysindia/base:3.23-2026.06.1
args:
MY_TOOL_VERSION: 1.2.3
If build.yaml exists but sets no build_from, the Supervisor defaults it to
ghcr.io/apexinfosysindia/base:latest. If there is no build file at all, your
Dockerfile must specify its own complete FROM.
Dockerfile
The Supervisor builds local add-ons with docker buildx build, passing:
BUILD_FROM— the resolved base image (only when a build file exists)BUILD_ARCH— the target CPU architecture (aarch64oramd64)BUILD_VERSION— the add-on version fromconfig.yaml- every entry from
build.yamlargs
So the canonical opening is:
ARG BUILD_FROM
FROM $BUILD_FROM
RUN apk add --no-cache my-package
COPY rootfs /
You can also provide an architecture-specific Dockerfile.aarch64 or
Dockerfile.amd64; when present, it is preferred over the plain Dockerfile
for that architecture. The build is labeled automatically with
io.apexos.version, io.apexos.arch, io.apexos.type, and — when set —
io.apexos.name, io.apexos.description, and io.apexos.url.
rootfs/
By convention, everything under rootfs/ is copied into the image root with
COPY rootfs /. This is where init scripts, service supervision trees
(the official add-ons use s6-overlay directories such as
etc/services.d/<name>/run and etc/cont-init.d/*.sh), and configuration
templates live. For example, the ssh add-on ships
rootfs/etc/services.d/sshd/run and rootfs/etc/cont-init.d/keygen.sh.
DOCS.md, CHANGELOG.md, README.md
DOCS.mdis the user manual, shown in the add-on's Documentation tab.CHANGELOG.mdis shown to users when updates are available.README.mdis used as the add-on's long description in the store.
All three are read from fixed paths in the add-on directory — the filenames are not configurable. See presentation.
icon.png and logo.png
Fixed filenames read from the add-on directory: icon.png (square, shown in
listings) and logo.png (wide, shown on the add-on page). See
presentation for the sizes used by the official store.
translations/
Per-language YAML files (translations/en.yaml, etc.) providing human-readable
names and descriptions for your configuration options and network ports. The
Supervisor reads every file in this directory and keys the content by filename
stem (the language code). See presentation.
The base images
Official add-ons build from the ApexOS base-image family published at
ghcr.io/apexinfosysindia:
| Image | Flavor | Example tag in the official store |
|---|---|---|
ghcr.io/apexinfosysindia/base | Alpine Linux | 3.23-2026.06.1 (ssh, samba-family, zigbee2mqtt, cec_scan, configurator, spotify) |
ghcr.io/apexinfosysindia/base-debian | Debian | trixie-2026.06.1 (mosquitto, whisper, piper, vscode, plex, aircast, ...) |
ghcr.io/apexinfosysindia/addon-base | Community-style add-on base | 15.0.4 (samba, apex-cloud-link) |
ghcr.io/apexinfosysindia/base-python | Python variant | — |
ghcr.io/apexinfosysindia/base-ubuntu | Ubuntu variant | — |
These images ship the plumbing add-ons rely on — including the
bashio helper library and the s6 init/service supervision used by
the official add-ons. When the Supervisor builds an add-on without any base
image declared, it falls back to ghcr.io/apexinfosysindia/base:latest.
An add-on may also build from any other image, but third-party bases used by the official store are mirrored into the ApexOS registry first so devices and CI never depend on external registries at build time (see publishing).
Prebuilt images vs. local builds
An add-on either:
- builds locally on the device — no
image:key inconfig.yaml; the Supervisor runs the Docker build using your Dockerfile, or - pulls a prebuilt image — the
image:key names a published image, with{arch}expanded to the device architecture at install time:
image: registry.apexinfosys.in/apexos/{arch}-addon-samba
The image is pulled with the tag equal to the add-on version. All official
store add-ons use prebuilt images; the publish pipeline that produces them is
described in publishing. During development you will almost
always use local builds.
Local development against a device
Local add-ons live in the Supervisor's local store repository on the device
(repository slug local). The workflow:
- Get the add-on directory onto the device. The easiest path is the
Samba share add-on, which exposes an
addonsshare ("This is for your local apps"); the Terminal & SSH add-on works too, since it mounts the local add-ons folder at/addons. Copy your add-on directory (withconfig.yaml,Dockerfile, etc.) into it. - Reload the store. In the UI open the add-on store and refresh, or call the Supervisor store reload API. Your add-on appears under the local repository.
- Install it. The Supervisor builds the image on the device using
docker buildx buildwith theBUILD_FROM/BUILD_ARCH/BUILD_VERSIONarguments described above. - Iterate. After changing files, bump nothing — rebuild the add-on from
its page and restart it. Use the log view to debug; log to stdout/stderr
(or use
bashio::log.*).
Things worth knowing while iterating:
- Validation problems in
config.yamlare logged by the Supervisor with the reason; unknown keys are silently removed rather than rejected. - Invalid
map:entries are skipped with a warning, not fatal. - The container is connected to the internal
apexnetDocker network (172.30.32.0/23) unless you requesthost_network. - Every add-on container gets
SUPERVISOR_TOKENandTZin its environment; the token authenticates calls to the Supervisor API athttp://supervisor(see bashio and security). /datais always mounted read-write as the add-on's private, persistent storage. Everything else must be requested viamap:.
Once the add-on works locally, see publishing to get it into the official store.