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 Images - Target Red Hat UBI9 Micro or similar minimal runtime images
- Extensible - Add new build targets by following a simple directory structure
- Reproducible - Version-controlled configurations via
.envfiles
- Docker
make build <target>make help
make list-targets
make build nginxBuild artifacts are written to out/<target>/ by default for local
builds. In CI (CI=true or GITHUB_ACTIONS=true), artifacts remain
under <target>/ for release packaging compatibility. You can
override both behaviors with BUILD_OUTPUT_DEST.
.
├── build.sh # Main build entry point
├── download.sh # Download dispatcher (routes to module download.sh)
├── .github/
│ └── scripts/
│ └── common.sh # Shared common functions
├── out/ # Local build outputs (gitignored)
│ └── <target>/ # Local artifacts (for example `sbin/`, `bin/`)
└── <target>/ # Build target directory
├── .env # Version configuration
├── Dockerfile # Multi-stage build definition
├── download.sh # Source download script (optional)
└── ... # CI/release artifacts under `<target>/`
-
Create a new directory with your target name
-
Add a
.envfile with version variables:ALPINE_VERSION=3.23 YOUR_SOFTWARE_VERSION=1.0.0 -
Add a
Dockerfilewith your multi-stage build configuration -
Optionally add
download.shfor source downloads
Tip
Check existing targets (nginx/, haproxy/, apache-httpd/)
for reference implementations.
Each target must follow this structure:
<target>/
├── .env # Version configuration (required)
├── Dockerfile # Multi-stage build definition (required)
├── download.sh # Source download script (optional)
├── 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 .env file: Create
.envwith version variables: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 "src/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"]
-
Optional download.sh: Create
download.shto fetch source files:#!/usr/bin/env sh set -eu # Source common functions . "$(dirname -- "$0")/../.github/scripts/common.sh" download_tarball "https://example.com/your-target-${YOUR_TARGET_VERSION}.tar.gz" \ "src/your-target-${YOUR_TARGET_VERSION}.tar.gz"
-
Update Makefile: Add target to
TARGETSvariable:TARGETS := nginx haproxy apache-httpd coredns dnsmasq vector monit your-target -
Update release workflow: Add release configuration in
.github/workflows/release-from-tag.yaml:- startsWith(github.ref_name, 'your-target-') && 'your-target' - startsWith(github.ref_name, 'your-target-') && 'your-target/bin/your-target'
-
Validate: Run
make build your-targetto verify the build works
- 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 target-specific decisions in
AGENTS.mdif needed - Version variables: Follow naming convention
{TARGET}_VERSIONfor consistency
Releases are triggered by Git tags following the pattern <target>-<version>.<revision>:
- Format:
{target}-{official_version}.{revision} - Example:
nginx-1.28.2.18(target: nginx, version: 1.28.2, revision: 18) - Validation: Release tags are validated against
.envfile versions
-
Update versions: Update version variables in target
.envfile:YOUR_TARGET_VERSION=2.0.0 -
Test build: Verify that build works locally:
make build your-target
-
Commit changes: Commit version updates:
git add your-target/.env git commit -m "Update your-target to 2.0.0" -
Create tag: Create and push release tag:
git tag your-target-2.0.0.0 git push origin your-target-2.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}.{revision}- target: Target name (e.g., nginx, haproxy, apache-httpd)
- version: Official version from
.envfile (e.g., 1.28.2) - 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.0(haproxy version 3.2.13, 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)
- The
Makefileinvokesbuild.sh, which validates target directory and required files - Docker BuildKit executes the multi-stage Dockerfile via
docker buildx build - Built artifacts go to
out/<target>/for local builds, and to<target>/in CI for release packaging compatibility Build caching is automatically handled via per-target<target>/.cache/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 target from tag pattern and calls
reusable template
.github/workflows/template-release.yaml. - Template builds mapped target, scans for vulnerabilities,
uploads selected files as artifact, then uploads
${tag}.tar.gzthat contains selected release files.
Selected release binaries:
nginx:<target>/sbin/nginxhaproxy:<target>/sbin/haproxyapache-httpd:<target>/bin/httpdcoredns:<target>/corednsdnsmasq:<target>/sbin/dnsmasqvector:<target>/bin/vectormonit:<target>/bin/monit
Note
apache-httpd releases include both bin/httpd and
bin/rotatelogs for piped logging support.
The rotatelogs utility is included in apache-httpd releases and
can be used for log rotation:
- External rotatelogs: Use a system-installed
rotatelogsor provide it separately - Alternative rotation tools: Use
logrotate,multilog, or other log rotation solutions - Application-level logging: Configure applications to write directly to files managed by external rotation
For containerized deployments, leverage Docker's native logging drivers:
# Example: Use Docker's built-in log rotation
docker run --log-driver json-file --log-opt max-size=10m --log-opt
max-file=3 <image>
# Or use external logging drivers
docker run --log-driver fluentd --log-opt
fluentd-address=fluentd:24224 <image>Docker automatically handles log rotation and can forward logs to external systems like ELK, Splunk, or cloud logging services.
See apache-httpd/AGENTS.md for complete decision rationale and implementation guidance.
docker run --rm \
-v "$(pwd)/haproxy.cfg:/etc/haproxy/haproxy.cfg:ro" \
<image> -c -f /etc/haproxy/haproxy.cfg