Overview

Building Your First Image: A Complete Workflow

Why use dbuild?#

  • Jinja2 power — use templates (.j2) to keep your Containerfile DRY across multiple variants (e.g. :latest vs :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 build and dbuild test commands 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 \
  --github

dbuild 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:latest

4. 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 --> Build

Generate turns templates and compose.yaml into real files, updating Containerfile/Containerfile.pkg and a standardized README.md:

dbuild generate

Build 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 2

6. 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 string
dbuild test

If 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: inherit

This 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
Ready for the full CLI?

Every flag for every subcommand, including multi-arch builds and cleanup.

Updated

Was this page helpful?