CI/CD Pipeline
The Challenge: How to build native FreeBSD images without maintaining a private, physical build farm?
The Solution
Daemonless uses GitHub Actions with vmactions/freebsd-vm to run native FreeBSD 15 environments inside Ubuntu runners via QEMU/KVM.
Key Benefits
- Native Tooling: Run
pkg,podman, andbuildahnatively during CI - Real Kernel: Full FreeBSD 15 kernel, not emulation or compatibility layers
- The "Wheel" Factory: Compile complex native Python wheels (e.g., onnxruntime for Immich-ML) once, then inject into images
Automated Workflow
flowchart TD
A[generate_versions.py] -->|Detects new releases| B[Trigger Build]
B --> C[GHA Matrix]
C --> D1[Build :latest]
C --> D2[Build :pkg]
C --> D3[Build :pkg-latest]
D1 --> E1[CIT Test]
D2 --> E2[CIT Test]
D3 --> E3[CIT Test]
E1 -->|Pass| F1[Push to ghcr.io]
E2 -->|Pass| F2[Push to ghcr.io]
E3 -->|Pass| F3[Push to ghcr.io]
1. Version Detection
generate_versions.py scrapes upstream releases and FreeBSD package repositories:
- Monitors GitHub releases for upstream versions
- Queries FreeBSD quarterly and latest pkg repos
- Detects when rebuilds are needed
2. Build Matrix
GitHub Actions triggers parallel builds for all three tags:
strategy:
fail-fast: false
matrix:
include:
- build_type: latest
containerfile: Containerfile
base_version: "15"
- build_type: pkg
containerfile: Containerfile.pkg
base_version: "15"
- build_type: pkg-latest
containerfile: Containerfile.pkg
base_version: "15-latest"
3. Native FreeBSD Build
Each job runs inside a real FreeBSD VM:
- name: Build in FreeBSD VM
uses: vmactions/freebsd-vm@v1.3.5
with:
release: "15.0"
usesh: true
prepare: |
pkg install -y podman
kldload pf
sysctl net.inet.ip.forwarding=1
run: |
podman build -t $IMAGE:build .
4. Quality Gate
Every image passes CIT before push:
5. Publish
Only after CIT passes, images are pushed to the registry:
- name: Push to Registry
if: github.event_name != 'pull_request'
run: |
podman push $IMAGE:$VERSION
podman push $IMAGE:$TAG
Build Types
Containerfile (:latest)
Builds from upstream sources or binaries:
FROM ghcr.io/daemonless/base:15
# Download upstream binary
RUN fetch -qo /tmp/app.tar.gz \
"https://github.com/app/releases/download/v${VERSION}/app.tar.gz" \
&& tar xzf /tmp/app.tar.gz -C /usr/local/bin
Containerfile.pkg (:pkg, :pkg-latest)
Builds from FreeBSD packages:
The BASE_VERSION arg switches between quarterly (:15) and latest (:15-latest) packages.
Python Wheel Factory
For complex native dependencies like ONNX Runtime:
- Separate workflow compiles the wheel in CI
- Release artifact stores the wheel on GitHub Releases
- Image build fetches and installs the pre-built wheel
# Build wheel workflow
- name: Build ONNX Runtime
run: |
pip wheel onnxruntime --no-binary :all:
- name: Upload Release
uses: softprops/action-gh-release@v1
with:
files: onnxruntime-*.whl
# Image Containerfile
ARG ONNXRUNTIME_WHEEL=onnxruntime-1.23.2-cp311-freebsd.whl
RUN fetch -qo /tmp/${ONNXRUNTIME_WHEEL} \
"https://github.com/.../releases/download/.../${ONNXRUNTIME_WHEEL}" \
&& pip install /tmp/${ONNXRUNTIME_WHEEL}
Triggering Builds
Automatic
- Push to main: Triggers full build matrix
- Scheduled: Daily checks for upstream updates
- Dependency updates: Base image changes cascade to dependent images
Manual
# Via GitHub CLI
gh workflow run build.yaml -f image=radarr
# Via web UI
# Actions > Build > Run workflow
Workflow Files
Each image repository contains:
.github/
└── workflows/
└── build.yaml # Main build workflow
.daemonless/
└── config.yaml # CIT configuration
The main daemonless repo contains shared scripts:
scripts/
├── build-base.sh # Base image builder
├── build-app.sh # App image builder
├── generate_versions.py # Version scraper
└── compare-versions.py # Status page generator
Local Development
Build locally without CI:
# From daemonless/daemonless/scripts/
./local-build.sh 15 radarr latest
./local-build.sh 15 radarr pkg
./local-build.sh 15 radarr pkg-latest
Result
A fully automated, "hands-off" pipeline that ensures the registry is always current with both upstream features and FreeBSD security patches.
- 30+ images maintained automatically
- Zero manual intervention for routine updates
- Quality guaranteed by CIT gates
- Three tracks for different stability needs