From balena-tools
Guide for converting Docker projects to run on balenaCloud. Use this skill when balenifying or converting an existing Docker/docker-compose project for balena, writing or modifying Dockerfile.template files, setting up docker-compose.yml for balena fleets, choosing between Dockerfile.template and plain Dockerfile for balena services, debugging balena build failures, converting volume mounts for balena, handling environment variables on balena, configuring hardware access (GPIO, USB, serial) for containers, or understanding balena networking. Also use when the user mentions balena device types, %%BALENA_ARCH%%, %%BALENA_MACHINE_NAME%%, balenalib base images, bh.cr registry references, or asks how to run something on balena.
npx claudepluginhub klutchell/claude-skills --plugin balena-toolsThis skill uses the workspace's default tool permissions.
This skill covers the full workflow for converting a standard Docker or docker-compose project to run on balenaCloud. That includes Dockerfile templates for multi-architecture builds, volume mount conversion, networking differences, environment variable handling, hardware access, docker-compose.yml adaptations, and balena.yml fleet metadata.
Provides UI/UX resources: 50+ styles, color palettes, font pairings, guidelines, charts for web/mobile across React, Next.js, Vue, Svelte, Tailwind, React Native, Flutter. Aids planning, building, reviewing interfaces.
Fetches up-to-date documentation from Context7 for libraries and frameworks like React, Next.js, Prisma. Use for setup questions, API references, and code examples.
Analyzes competition with Porter's Five Forces, Blue Ocean Strategy, and positioning maps to identify differentiation opportunities and market positioning for startups and pitches.
This skill covers the full workflow for converting a standard Docker or docker-compose project to run on balenaCloud. That includes Dockerfile templates for multi-architecture builds, volume mount conversion, networking differences, environment variable handling, hardware access, docker-compose.yml adaptations, and balena.yml fleet metadata.
Use this as a reference whenever you're porting an existing project to balena or building a new balena fleet from scratch.
Quick reference for the conversion steps:
./data:/app) with named volumes.env files — use dashboard/CLI variables insteadbalena.yml with fleet name and supported device types.template if arch-specific base images are neededprivileged: true or selective devices:/cap_add: for hardware accessnetwork_mode: host if the service needs host networking (multicontainer defaults to bridge)links: — bridge network provides service discovery by container nameversion: "2.1" in docker-compose.yml (balena uses Compose v2/2.1)Balena's build system supports Dockerfile templates — files named Dockerfile.template that undergo variable substitution before Docker processes them. The template system exists because Docker images are architecture-specific, and balena fleets often span multiple device types (e.g., Raspberry Pi 3, Pi 4, Intel NUC) with different CPU architectures.
Template variables use %%VARIABLE%% syntax. The balena builder replaces them with fleet/device-specific values before the Docker build begins — Docker never sees the %% tokens.
| Variable | Description | Example Value |
|---|---|---|
%%BALENA_ARCH%% | Target CPU architecture | aarch64, amd64, armv7hf |
%%BALENA_MACHINE_NAME%% | Yocto machine name for the device type | raspberrypi4-64, genericx86-64-ext |
%%BALENA_APP_NAME%% | Fleet name | my-fleet |
%%BALENA_RELEASE_HASH%% | Release hash | abc123... |
%%BALENA_SERVICE_NAME%% | Service name from docker-compose.yml | pihole |
Use %%BALENA_ARCH%% by default. Only use %%BALENA_MACHINE_NAME%% when you genuinely need device-type-specific behavior (rare). Machine name won't evaluate correctly if a fleet has mixed device types.
The balena builder picks the first match from this priority list:
Dockerfile.<device-type> — exact device type match (e.g., Dockerfile.raspberrypi4-64)Dockerfile.<arch> — architecture match (e.g., Dockerfile.aarch64)Dockerfile.template — universal fallback with variable substitutionDockerfile — plain Dockerfile, no substitutionThis applies during balena push, balena build, balena deploy, and git push.
When to use templates vs plain Dockerfiles:
DockerfileDockerfile.templateBalena has its own architecture naming convention, distinct from Docker and Go:
Balena (%%BALENA_ARCH%%) | Docker Platform | Go GOARCH |
|---|---|---|
aarch64 | linux/arm64 | arm64 |
amd64 | linux/amd64 | amd64 |
armv7hf | linux/arm/v7 | arm (GOARM=7) |
rpi | linux/arm/v6 | arm (GOARM=6) |
i386 | linux/386 | 386 |
This matters because balenaCloud doesn't support multi-arch fleets yet — each fleet targets a single device type and architecture. Fleets and blocks on balena hub (bh.cr) are typically published as separate images per balena arch (e.g., my-block-aarch64, my-block-amd64), not as multi-platform manifests. Templates with %%BALENA_ARCH%% map directly to these naming conventions.
For most cases, a single FROM line with %%BALENA_ARCH%% interpolated into the image reference is all you need:
FROM bh.cr/some-org/my-block-%%BALENA_ARCH%%/1.0.0
Or with a standard registry:
FROM myregistry/myimage-%%BALENA_ARCH%%:1.0.0
COPY . /app
CMD ["/app/start.sh"]
This pattern works especially well with bh.cr, where blocks and fleets are published per-architecture using balena's naming convention. The builder substitutes %%BALENA_ARCH%% → aarch64 (or amd64, armv7hf, etc.) and Docker pulls the right image.
Use this when the simple pattern won't work. The two main reasons:
arm64, arm64v8) instead of balena names (aarch64, armv7hf). A simple FROM image-%%BALENA_ARCH%%:tag would try to pull image-aarch64:tag which doesn't exist.%%BALENA_ARCH%% pattern embeds a variable inside the image reference, which Renovate can't parse.ARG BALENA_ARCH=%%BALENA_ARCH%%
# Stage names use balena arch convention — map to whatever the upstream publishes
FROM linuxserver/wireguard:amd64-latest AS wireguard-amd64
FROM linuxserver/wireguard:arm64v8-latest AS wireguard-aarch64
# hadolint ignore=DL3006
FROM wireguard-${BALENA_ARCH}
The key trick: name each stage with the balena arch suffix (e.g., wireguard-aarch64) regardless of what the upstream tag calls it (e.g., arm64v8-latest). Then FROM wireguard-${BALENA_ARCH} resolves correctly.
How it works:
%%BALENA_ARCH%% is substituted by the balena builder before Docker runsFROM wireguard-${BALENA_ARCH} resolves to the correct stageThe # hadolint ignore=DL3006 comment suppresses the "always tag the version of an image explicitly" lint warning on the dynamic FROM line.
%%BALENA_ARCH%% | Devices |
|---|---|
aarch64 | Raspberry Pi 3/4/5 (64-bit), Jetson, most modern ARM boards |
armv7hf | Raspberry Pi 2/3 (32-bit), BeagleBone |
amd64 | Intel NUC, generic x86_64 |
rpi | Raspberry Pi 1 / Zero (armv6) |
Host bind mounts (./data:/app/data) do not work on balena. The host filesystem layout is managed by balenaOS and is not directly accessible to containers.
Declare volumes in the top-level volumes: section and reference them in services:
version: "2.1"
volumes:
app-data:
config:
services:
my-service:
build: my-service
volumes:
- app-data:/app/data
- config:/etc/myapp
/var/lib/docker/volumes/<APP_ID>_<volume_name>/_dataresin-data volume mounted at /data — no explicit volume config needed| Docker (original) | Balena (converted) |
|---|---|
./data:/app/data | app-data:/app/data (+ declare app-data: in volumes:) |
./config.json:/etc/app/config.json | Bake into image with COPY, or use an env var |
/var/run/docker.sock:/var/run/docker.sock | Use io.balena.features.balena-socket: 1 label instead |
Host networking by default — the container shares the host's network stack. No port mapping needed.
Bridge network by default — each service gets its own network namespace. Services are routable by container name.
services:
frontend:
build: frontend
ports:
- "80:3000" # expose to host/external network
backend:
build: backend
# no ports needed — frontend reaches backend at http://backend:8080
ports: only needed to expose a service to the host network or external clients — not for inter-service communicationlinks: — the bridge network provides automatic service discovery by container namenetwork_mode: host on a service if it needs direct access to the host network stack (e.g., mDNS, DHCP, network scanning)networks:) are not supported — all multicontainer services share a single bridge network.env filesBalena does not read .env files. Instead, set variables through:
balena env set MY_VAR my_value --fleet my-fleet or --device <uuid>These are automatically available in every container:
| Variable | Value |
|---|---|
BALENA | 1 (use to detect running on balena) |
BALENA_DEVICE_UUID | Device UUID |
BALENA_APP_ID | Fleet/application numeric ID |
BALENA_APP_NAME | Fleet name |
BALENA_SERVICE_NAME | Service name from docker-compose.yml |
BALENA_SUPERVISOR_ADDRESS | Supervisor API URL (usually http://127.0.0.1:48484) |
BALENA_SUPERVISOR_API_KEY | API key for supervisor requests |
Values can be up to 1MB each.
.env files# Read each KEY=VALUE from .env and set as fleet variables
while IFS='=' read -r key value; do
[[ "$key" =~ ^#.*$ || -z "$key" ]] && continue
balena env set "$key" "$value" --fleet my-fleet
done < .env
Privileged by default — full access to /dev and all host devices. No extra config needed.
Not privileged by default. You must explicitly grant access:
Option 1 — Full privileged mode:
services:
my-service:
build: my-service
privileged: true
Option 2 — Selective access (preferred when possible):
services:
my-service:
build: my-service
devices:
- "/dev/i2c-1:/dev/i2c-1"
- "/dev/ttyUSB0:/dev/ttyUSB0"
cap_add:
- SYS_RAWIO
Use labels instead of manual /dev paths where possible — they're more stable across device types:
| Label | Grants access to |
|---|---|
io.balena.features.kernel-modules: 1 | Kernel module loading |
io.balena.features.dbus: 1 | Host D-Bus socket |
io.balena.features.supervisor-api: 1 | Supervisor REST API |
io.balena.features.balena-api: 1 | balenaCloud API |
io.balena.features.balena-socket: 1 | balenaEngine socket (like Docker socket) |
io.balena.features.sysfs: 1 | Host /sys filesystem |
io.balena.features.procfs: 1 | Host /proc filesystem |
io.balena.features.gpu: 1 | GPU device access |
Prefer feature labels over raw device paths — they abstract hardware differences across device types.
Balena uses Docker Compose v2/2.1. Each service with a build: directive points to a directory containing a Dockerfile (or Dockerfile.template).
version: "2.1"
services:
my-service:
build: my-service
network_mode: host
cap_add:
- NET_ADMIN
labels:
io.balena.features.kernel-modules: 1
tmpfs:
- /tmp
- /var/run
# Services using pre-built images don't need templates
helper:
image: bh.cr/some-org/some-block/1.0.0
restart: no
| Label | Purpose |
|---|---|
io.balena.features.kernel-modules: 1 | Access kernel modules |
io.balena.features.dbus: 1 | Access host D-Bus |
io.balena.features.supervisor-api: 1 | Access supervisor API |
io.balena.features.balena-api: 1 | Access balena API |
io.balena.features.balena-socket: 1 | Access balenaEngine socket |
io.balena.features.sysfs: 1 | Access host /sys |
io.balena.features.procfs: 1 | Access host /proc |
io.balena.features.gpu: 1 | GPU device access |
Control how balena updates containers:
| Label | Values | Default |
|---|---|---|
io.balena.update.strategy | download-then-kill, kill-then-download, delete-then-download, hand-over | download-then-kill |
io.balena.update.handover-timeout | Seconds (for hand-over strategy) | 60 |
download-then-kill — download new image, then stop old container (minimal downtime, needs 2x storage)kill-then-download — stop old container first, then download (saves storage, longer downtime)delete-then-download — delete old image and container, then download (maximum storage savings)hand-over — old and new containers run simultaneously during handover period (zero-downtime deploys)Every fleet project has a balena.yml at root:
name: "My Fleet"
type: "sw.application"
version: 1.0.0
description: "What this fleet does"
data:
defaultDeviceType: "raspberrypi4-64"
supportedDeviceTypes:
- "raspberrypi3-64"
- "raspberrypi4-64"
- "genericx86-64-ext"
The supportedDeviceTypes list determines which architectures your Dockerfile.template must handle.
| Symptom | Likely Cause |
|---|---|
invalid reference format | Template variable not substituted — check file is named .template |
manifest unknown | Architecture not published for that image tag |
no match for platform | Using a plain Dockerfile where a template is needed |
| Wrong arch binary runs | %%BALENA_MACHINE_NAME%% resolved to fleet default, not target device |
Read references/patterns.md for:
%%RESIN_MACHINE_NAME%% syntax (pre-2019 projects)FROM balenalib/%%BALENA_MACHINE_NAME%%-alpine)install_packages helper in balenalib imagesTARGETARCH fallbackINITSYSTEM on ENV (legacy balena supervisor integration)When converting a Docker project to balena:
balena.yml → what device types does this fleet target?%%BALENA_ARCH%% pattern..env files — migrate variables to balenaCloud dashboard or CLI.privileged: true or selective devices:/cap_add: for hardware access.balena build --deviceType <type> for each target.