Build statically-linked binaries using Docker multi-stage builds for portable, minimal container deployments.
- Static Linking - Produce fully statically-linked binaries using musl libc
- Multi-stage Builds - Leverage Docker BuildKit for efficient, cacheable builds
- Minimal Outputs - Use a UBI9 Micro verify stage for runtime checks and package final artifacts from a
scratchstage - Extensible - Add new build targets by following a simple directory structure
- Reproducible - Version-controlled configurations via
metadata.json
- Docker
make build <target>Build artifacts are written to .out/<target>/ by default for both local and CI builds. You can override this with BUILD_OUTPUT_DEST.
docker run --rm \
-v "$(pwd)/haproxy.cfg:/etc/haproxy/haproxy.cfg:ro" \
<image> -c -f /etc/haproxy/haproxy.cfg.
├── metadata.json # Canonical build/release metadata
├── Makefile # Local development build orchestration
├── .gitlab-ci.yml # GitLab CI configuration
├── scripts/ # Shared build/release scripts
│ ├── download.sh # Source download dispatcher
│ ├── metadata.sh # Metadata query helper
│ └── package-release.sh # Release package helper
├── .github/
│ ├── scripts/
│ │ ├── build.sh # Docker Buildx build entry
│ │ └── release-guard.sh # Release tag validator
│ └── workflows/
│ ├── release-from-tag.yaml
│ └── template-release.yaml
├── .gitlab/
│ ├── ci/
│ │ └── package-pipeline.jsonnet # GitLab child pipeline generator
│ └── scripts/
│ └── build-rootless.sh # Rootless BuildKit build entry
├── templates/ # GitLab CI components
│ └── static-release.yml
├── .tmp/ # Downloaded source cache (gitignored)
├── .cache/ # Build cache (gitignored)
├── .out/ # Build outputs (gitignored)
│ └── <target>/ # Local artifacts (for example `sbin/`, `bin/`)
└── <target>/ # Build target directory
├── Dockerfile # Multi-stage build definition
└── ...
Each target must follow this structure:
<target>/
├── Dockerfile # Multi-stage build definition (required)
├── README.md # Target-specific documentation (optional)
└── AGENTS.md # Target-specific conventions (optional)
-
Create target directory: Create a new directory named after your target (e.g.,
your-target/) -
Add centralized metadata: Register the target in
metadata.json:{ "your-target": { "tag_prefix": "your-target", "version_env_var": "YOUR_TARGET_VERSION", "release_files": [ "bin/your-target" ], "env": { "ALPINE_VERSION": "3.23", "YOUR_TARGET_VERSION": "1.0.0", "UBI9_MICRO_VERSION": "9.5" } } } -
Create Dockerfile: Implement a multi-stage build:
# Build stage FROM alpine:${ALPINE_VERSION} AS build ARG YOUR_TARGET_VERSION ADD ".tmp/your-target-${YOUR_TARGET_VERSION}.tar.gz" /build/ # ... build steps ... # Verify stage (optional but recommended) FROM redhat/ubi9-minimal:${UBI9_MICRO_VERSION} AS verify ARG YOUR_TARGET_VERSION COPY --from=build /your-target /target/your-target # ... verification steps (ELF check, static linking, strace) ... # Final stage FROM scratch COPY --from=verify /target /target ENTRYPOINT ["/target/your-target"]
-
Add download metadata: Define each upstream resource directly in
metadata.json:{ "your-target": { "downloads": [ { "url": "https://example.com/your-target-{YOUR_TARGET_VERSION}.tar.gz", "name": "your-target-{YOUR_TARGET_VERSION}.tar.gz" } ] } } -
Verify: Targets are loaded from
metadata.json. -
Update release trigger mapping: Add the tag trigger and target selection case in
.github/workflows/release-from-tag.yaml. Release file selection and official versions now come frommetadata.json.on: push: tags: - 'your-target-*' jobs: release: with: target: >- startsWith(github.ref_name, 'your-target-') && 'your-target'
-
Validate: Run
make build your-targetto verify the download and build flow works
Targets share the same root contract, but some targets intentionally vary in builder image, release contents, or runtime packaging.
- Document approved target-specific variations in that target's
README.md. - Keep the root
README.mdfocused on shared repository behavior. - Treat
nginx,nginx-resty-upstream-healthcheck,apache-httpd,coredns,vector,haproxy,dnsmasq, andmonitdifferences as documented target profiles, not as undocumented exceptions.
- Security hardening: Use static PIE builds (
-fPIE -pie) - Verification: Always include a verify stage with ELF checks, static linking verification, and strace validation
- Caching: Use
--mount=type=cachefor Alpine/DNF caches - Documentation: Document approved target-specific variations in each target
README.md; useAGENTS.mdfor repository-wide policy - Version variables: Follow naming convention
{TARGET}_VERSIONfor consistency insidemetadata.json
Releases are triggered by Git tags following the pattern <target>-<version>[-<prerelease>].<revision>:
- Format:
{target}-{official_version}[-{prerelease}].{revision} - Example:
nginx-1.28.2.18(target: nginx, version: 1.28.2, revision: 18) - Validation: Release tags are validated against
metadata.json
Current release tag triggers and target selection still live in .github/workflows/release-from-tag.yaml because GitHub event filters must stay static, but release-file selection and tag-version validation now come from metadata.json.
-
Update versions: Edit the target entry in
metadata.json:{ "your-target": { "env": { "YOUR_TARGET_VERSION": "2.0.0" } } } -
Test build: Verify that build works locally:
make build your-target
-
Commit changes: Commit version updates:
git add metadata.json git commit -m "Update your-target to 2.0.0" -
Create tag: Create and push release tag. Use the target name as the tag prefix, except
apache-httpd, which useshttpd-:git tag your-target-2.0.0.0 git push origin your-target-2.0.0.0
-
CI automation: GitHub Actions automatically:
- Validates tag format and version
- Builds target
- Scans for vulnerabilities (Trivy)
- Uploads artifacts to GitHub Actions
- Creates GitHub Release with
.tar.gzpackage
Tags MUST follow this format:
{target}-{version}[-{prerelease}].{revision}- target: Target tag prefix (e.g., nginx, haproxy, httpd)
- version: Official version from
metadata.json(e.g., 1.28.2) - prerelease: Optional pre-release suffix (e.g., beta, rc1)
- revision: Release revision suffix starting at 0, incrementing for rebuilds (e.g., 18)
Valid examples:
nginx-1.28.2.18(nginx version 1.28.2, revision 18)httpd-2.4.66.5(apache-httpd version 2.4.66, revision 5)haproxy-3.2.13-beta.0(haproxy version 3.2.13-beta, revision 0)
Invalid examples:
nginx-1.28.2(missing revision suffix)custom-1.0.0.0(unknown target)nginx-1.28.2.x(non-numeric revision)
scripts/download.shresolves each target's download resources frommetadata.json, then the build script runs Docker Buildx- Docker BuildKit executes the multi-stage Dockerfile via
docker buildx build - Built artifacts go to
.out/<target>/for both local builds and CI - Verify stages run inside UBI9 Micro, and the final exported artifact comes from the target's
scratchstage
Build caching is automatically handled via root .cache/<target>/ directories.
- Archive upload includes selected release files as a workflow artifact.
- Release upload is optional and packages selected release files into one
.tar.gzper tag. - Tag push release uses unified workflow
.github/workflows/release-from-tag.yaml. - Workflow automatically determines the target from tag pattern and calls reusable template
.github/workflows/template-release.yaml. - Template builds mapped target, scans for vulnerabilities, uploads the full
.out/<target>/tree as artifact, then uploads${tag}.tar.gzthat contains selected release files.
Selected release contents:
nginx:sbin/nginxnginx-resty-upstream-healthcheck:sbin/nginx,lualib/resty/core.lua,lualib/resty/core/,lualib/resty/upstream/haproxy:sbin/haproxyapache-httpd:bin/httpd,bin/rotatelogscoredns:corednsdnsmasq:sbin/dnsmasqvector:bin/vectormonit:bin/monit
Note
apache-httpd releases include both bin/httpd and bin/rotatelogs for piped logging support.
For details, see apache-httpd/AGENTS.md.
- GitLab pipelines on
mainandfeature/*expose manual package jobs that generate a child pipeline with.gitlab/ci/package-pipeline.jsonnet. feature/*branches append-betato the package version.- GitLab uploads to Package Registry only. GitHub uploads to Release only.
- The generated child pipeline includes the local component at
templates/static-release.yml. - GitHub release packaging and GitLab package generation both reuse
scripts/package-release.sh.