Building Your First Image: A Complete Workflow
Why use dbuild?#
- Jinja2 power — use templates (
.j2) to keep yourContainerfileDRY across multiple variants (e.g.:latestvs:pkg). - Integrated testing (CIT) — automatically verify the container actually works (port checks, health endpoints, even screenshots) before pushing.
- GitHub-first — integrates with GitHub Actions and GHCR.io with zero-config reusable workflows.
- Local/CI parity — the exact same
dbuild buildanddbuild testcommands run on your laptop and in the cloud.
1. Prerequisites#
- FreeBSD 14+ or 15+
- dbuild and Podman installed:
pkg install sysutils/py-dbuild - A GitHub account (for pushing images via
ghcr.io) - Optional: a Woodpecker CI instance for self-hosted builds
2. Initialize the project#
Create an empty directory and run dbuild init. This example uses Traefik:
mkdir traefik && cd traefik
dbuild init \
--freebsd-port net/traefik \
--port 8080 \
--variants latest,pkg,pkg-latest \
--githubdbuild scaffolds a complete project structure:
traefik/
├── .daemonless/
│ └── config.yaml # Build variants and CIT test config
├── .github/workflows/build.yaml # GitHub Actions CI pipeline
├── compose.yaml # Image metadata + deployment example
├── Containerfile.j2 # Template for :latest (upstream binary)
├── Containerfile.pkg.j2 # Template for :pkg and :pkg-latest
└── root/
├── etc/services.d/traefik/run # s6 service supervisor script
└── healthz # Optional health check script
3. The single source of truth: compose.yaml#
In the Daemonless ecosystem, compose.yaml isn't just for deployment — it's the source of truth for the image's metadata and documentation. Refine the x-daemonless section:
name: traefik
x-daemonless:
title: "Traefik"
icon: ":material-server-network:" # Browse icons at pictogrammers.com
category: "Infrastructure"
description: "Modern HTTP reverse proxy and load balancer for FreeBSD."
upstream_url: "https://github.com/traefik/traefik" # Must be the SOURCE repo
web_url: "https://traefik.io/"
freshports_url: "https://www.freshports.org/net/traefik/"
upstream_binary: true # Tells dbuild :latest uses binaries
user: "bsd"
docs:
env:
PUID: "User ID for the application process"
PGID: "Group ID for the application process"
volumes:
/config: "Traefik configuration directory"
ports:
80: "HTTP"
443: "HTTPS"
8080: "Dashboard / API"
services:
traefik:
image: ghcr.io/daemonless/traefik:latest4. Crafting the templates#
dbuild uses Jinja2 templates to generate standard Containerfiles, injecting dynamic labels and reusing logic:
ARG BASE_VERSION=15
FROM ghcr.io/daemonless/base:${BASE_VERSION}
ARG UPSTREAM_URL="https://api.github.com/repos/traefik/traefik/releases/latest"
ARG UPSTREAM_JQ=".tag_name"
# [dbuild] labels will be automatically injected here
RUN pkg update && pkg install -y ca_root_nss jq && pkg clean -ay
RUN TRAEFIK_VERSION=$(fetch -qo - "${UPSTREAM_URL}" | jq -r "${UPSTREAM_JQ}") && \
fetch -qo /tmp/traefik.tar.gz \
"https://github.com/traefik/traefik/releases/download/${TRAEFIK_VERSION}/traefik_${TRAEFIK_VERSION}_freebsd_amd64.tar.gz" && \
tar xzf /tmp/traefik.tar.gz -C /usr/local/bin traefik && \
chmod +x /usr/local/bin/traefik && \
mkdir -p /app && echo "${TRAEFIK_VERSION}" > /app/version && \
rm /tmp/traefik.tar.gz
RUN mkdir -p /config && chown -R bsd:bsd /config
COPY root/ /5. Generate and build#
The core dbuild loop is Generate → Build → Test.
flowchart TD
Init["$ dbuild init\nScaffold a new project"] --> Compose
Init --> Template
Init --> Config
Compose["compose.yaml\nTitle, description, ports, env vars, docs"]
Template["Containerfile.j2\nHow the image is built"]
Config[".daemonless/config.yaml\nBuild variants and CIT settings"]
Compose --> Generate
Template --> Generate
Config --> Generate
Generate["$ dbuild generate\nRenders templates into Containerfile + README"]
Generate --> Build
Build["$ dbuild build\nBuilds OCI image with podman"]
Build --> Test
Test["$ dbuild test\nRuns CIT: shell, port, health, screenshot"]
Test --> CIT{Pass?}
CIT -- yes --> Push["$ dbuild push\nPushes to ghcr.io/daemonless/app:latest"]
CIT -- no --> Fix["Fix Containerfile.j2\nor .daemonless/config.yaml"]
Fix --> BuildGenerate turns templates and compose.yaml into real files, updating Containerfile/Containerfile.pkg and a standardized README.md:
dbuild generateBuild locally:
# Build the default variant
dbuild build
# Build all variants defined in .daemonless/config.yaml
dbuild build --variant latest --variant pkg --variant pkg-latest
# Build in parallel, or limit to 2 concurrent builds
dbuild build -p
dbuild build -p 26. Test with CIT#
dbuild test spins up the container and runs the checks defined in .daemonless/config.yaml:
cit:
mode: health
port: 8080
health: /ping
ready: "Configuration loaded" # Watch logs for this stringdbuild testIf the app doesn't bind to port 8080 or /ping fails, the build is considered failed. See Quality Gates (CIT) for the full spec.
7. GitHub integration#
dbuild init --github generates .github/workflows/build.yaml, which delegates everything to a shared reusable workflow:
name: Build FreeBSD Container
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
uses: daemonless/dbuild/.github/workflows/daemonless-build.yaml@main
with:
image_name: traefik
secrets: inheritThis gives you: managed FreeBSD runners (spins up FreeBSD VMs on GitHub Actions automatically), an automatic build matrix (dbuild detect builds all variants in parallel), GHCR.io auth via GITHUB_TOKEN, and CycloneDX SBOM generation.
Control CI behavior from commit messages: [skip test] skips CIT (useful for docs-only changes), [skip push] builds and tests but doesn't push.
8. Command summary#
| Command | Description |
|---|---|
dbuild init |
Scaffold a new project |
dbuild generate |
Update Containerfiles from templates |
dbuild build |
Build the container image(s) |
dbuild test |
Run integration tests (CIT) |
dbuild push |
Push to registry (GHCR/Docker Hub) |
dbuild info |
Show detected variants and config |
Every flag for every subcommand, including multi-arch builds and cleanup.