Declarative, reproducible, CI-built Raspberry Pi OS images for production and development devices. Config lives in JSON, generation is driven by Python, actual image baking runs on top of CustomPiOS.
Supported hardware:
| Target | Status |
|---|---|
| Raspberry Pi 4 | β |
| Raspberry Pi 5 | β |
| Compute Module 4 (CM4) | β |
| Compute Module 5 (CM5) | β |
| Raspberry Pi Zero 2 W | β not supported β 512 MB RAM is insufficient for Docker CE + Portainer + base services |
Base OS: Raspberry Pi OS arm64 (trixie, 2026-06-18).
- Declarative config in JSON, validated against a strict schema.
- Variant composition via
extendsβ a child variant deep-merges onto a base, arrays of{name: ...}records merge by name. - Env-var resolver for secrets (
${VAR}/${VAR:-default}), fail-fast on missing required values. - Dockerised dev/build runtime β no host Python/qemu dependencies required; Linux Β· macOS Β· Windows (WSL / CMD / PowerShell).
- CI-ready image builds on GitHub Actions with matrix over all variants, artifact + release asset output, full metadata summary per run.
- Baked-in:
- SSH enabled with hardened
sshd_config.d(no root login, no challenge-response) - Docker CE + compose plugin, IPv6 NAT, sensible daemon.json
- Portainer CE via docker-compose (
restart: unless-stopped), installed by a first-boot oneshot β Docker daemon handles lifecycle from then on - Unattended upgrades with configurable maintenance + reboot windows,
event-driven via
apt-daily-upgrade.servicepost-hook - Dynamic MOTD banner showing variant, version, kernel, all interfaces with IPv4/IPv6, CAN state + bitrate, service health, pending reboots
- Admin user with sudo NOPASSWD,
suwithout password viapam_wheel
- SSH enabled with hardened
You need Raspberry Pi Imager v2.0.3 or later β earlier versions don't persist a custom repository between launches.
One-click:
Open BAUER GROUP repository in Raspberry Pi Imager
Imager opens with our catalog pre-loaded and asks for confirmation. After
that, every future release shows up automatically β including Compute
Module eMMC flashing via the built-in rpiboot.
Manual setup:
- Open Raspberry Pi Imager β β Settings β Custom repository
- Paste
https://bauer-group.github.io/XPD-RPIImage/rpi-imager.json - Close the dialog. Imager reloads the OS list automatically β our variants appear under CHOOSE OS β BAUER GROUP.
Landing page with direct downloads + full checksums: https://bauer-group.github.io/XPD-RPIImage/
Full flashing guide (SD, USB-SSD, CM4/CM5 via rpiboot, balenaEtcher, manual
dd): docs/flash.md.
# Linux / macOS / WSL
./tools/run.sh validate # validate every variant JSON
./tools/run.sh render canbus-plattform # generate module artifacts
./tools/run.sh build canbus-plattform # full image build (privileged)
# Windows CMD
tools\run.cmd build canbus-plattform
# Windows PowerShell
.\tools\run.ps1 build -Variant canbus-plattformSee docs/tools-container.md for launcher reference.
cp .env.example .env
# edit .env - set ADMIN_PASSWORD and WIFI_PSK
make deps # pip install requirements
make validate # schema-check every variant
make build VARIANT=canbus-plattform # build the image
# .env is picked up automatically when it sits at the repo root.
# From elsewhere, pass it explicitly: make build ENV_FILE=path/to/.envOutput lands in dist/bgrpiimage-<variant>-v<version>.img.xz.
Push to main or open a PR β automatic build with SHA-stamped artifact
(see docs/ci-cd.md).
A conventional commit on main β semantic-release cuts vX.Y.Z and the
GitHub Release, then dispatches the image build on that tag so the .img.xz,
.sha256 and .manifest.json assets are attached automatically.
The dispatch is a separate job using a PAT: the tag itself is pushed with
GITHUB_TOKEN, and GitHub never triggers a workflow from a GITHUB_TOKEN
event, so build.yml's tag trigger alone would never fire. If the PAT is ever
missing, the release job fails loudly and the assets can be attached by hand:
gh workflow run build.yml --ref vX.Y.Z| Variant | Description | Hostname | Extras |
|---|---|---|---|
base |
Generic Raspberry Pi image, Docker-ready, no application-specific hardware. | bg-rpi |
β |
canbus-plattform |
Base + Waveshare 17912 dual isolated CAN HAT (MCP2515 on SPI). | bg-canbus |
can0 + can1 at 500 kbit/s, can-utils, dialout/gpio/i2c/spi groups |
Adding a new variant is a 10-line JSON file β see
docs/variants.md.
βββββββββββββββββββββββ βββββββββββββββββββββ ββββββββββββββββββββββ
β config/variants/*.json ββββΆβ scripts/generate.py ββββΆβ src/modules/*/filesystem/ β
β (declarative, JSON) β β (validate + merge β β root/opt/bgrpiimage/ β
βββββββββββββββββββββββ β + env resolve) β β (inputs for CustomPiOS)β
βββββββββββββββββββββ ββββββββββββββ¬ββββββββ
β
βΌ
βββββββββββββββββββββββββββ
β guysoft/custompios β
β (privileged build in β
β docker or GH runner) β
βββββββββββββ¬ββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββ
β dist/bgrpiimage-β¦img.xz β
ββββββββββββββββββββββββββββ
More detail: docs/architecture.md.
| Topic | File |
|---|---|
| Architecture + build pipeline | docs/architecture.md |
| Flashing (RPi Imager catalog, CM4/CM5 eMMC, Etcher, dd) | docs/flash.md |
| JSON config reference + env resolver | docs/configuration.md |
| Hardware reference (camera, HDMI, RTC, fan, watchdog, overclock, β¦) | docs/hardware.md |
Creating a new variant (extends chain) |
docs/variants.md |
| Post-flash setup (password Β· WiFi Β· IP) | docs/post-flash-setup.md |
| Dockerised tools container | docs/tools-container.md |
| GitHub Actions CI/CD | docs/ci-cd.md |
| Login banner + unattended updates | docs/banner-and-updates.md |
β οΈ Default credentials shipped by this image:
adminuser password β12345678- WiFi PSK for
IOT @ BAUER-GROUPβ12345678These are published defaults, on purpose: the images are public, so a discoverable credential is what makes them usable at all. The admin password is shipped expired β the first login (console or SSH) forces you to set a new one before you get a session, so it cannot silently stay in place. The WiFi PSK is not protected that way: treat an untouched image as safe only on an isolated lab network until you have rotated it. On first boot the login banner screams about it and the MOTD keeps reminding you until you rotate the admin password.
Bake real values into the image during the build:
- Copy
.env.exampleβ.env, set real values. - Rebuild:
./tools/run.sh build <variant> --env-file ./.env. - Never commit
.env(already gitignored).
In CI, set ADMIN_PASSWORD and WIFI_PSK as repository secrets β the
workflow passes them through automatically.
Every image ships /usr/local/sbin/bgrpiimage-setup β a one-stop helper
for the routine post-flash changes:
sudo bgrpiimage-setup password # rotate admin pw
sudo bgrpiimage-setup password alice # rotate another user
sudo bgrpiimage-setup wifi "MyNet" "s3cret" DE # join a WiFi
sudo bgrpiimage-setup wifi --disable # tear down wlan0
sudo bgrpiimage-setup ip eth0 dhcp # back to DHCP
sudo bgrpiimage-setup ip eth0 static 10.0.0.5/24 10.0.0.1 1.1.1.1
sudo bgrpiimage-setup status # overviewAll IP changes land as /etc/systemd/network/05-bgrpiimage-<iface>.network
β the 05- prefix sorts before the shipped 10-eth.network /
20-wlan.network, and systemd-networkd applies only the first matching file,
so ours wins. The defaults are left in place, so reverting is just deleting
the file.
See docs/post-flash-setup.md for the full
subcommand reference.
.
βββ config/
β βββ schema.json # JSON schema for variant config
β βββ variants/
β βββ base.json # generic base variant
β βββ canbus-plattform.json # extends base + CAN additions
βββ scripts/
β βββ generate.py # JSON β CustomPiOS module files
β βββ bootstrap.sh # clones CustomPiOS into ./CustomPiOS
β βββ build.sh # full image build (privileged docker)
β βββ requirements.txt
βββ src/ # CustomPiOS distro
β βββ config # distro-level config
β βββ modules/ # bgrpiimage-{base,users,network,boot,
β β # hardware,can,docker,portainer,
β β # unattended-upgrades}
β βββ variants/ # per-variant shell config (generated)
βββ tools/ # portable dev/build runtime
β βββ Dockerfile
β βββ run.sh / run.cmd / run.ps1
βββ .github/workflows/build.yml # CI pipeline
βββ Makefile # local convenience targets
βββ docs/
MIT β see LICENSE.