A production guide for standing up a reproducible, SBOM-attested container image build host using Chainguard's apko and melange.
Overview
apko and melange are two complementary open-source tools from Chainguard. Together they replace the Dockerfile for building minimal, distroless, supply-chain-focused images:
- melange builds
.apkpackages from source using declarative YAML pipelines. These are the same package format the Alpine and Wolfi package managers consume. - apko assembles a set of
.apkpackages into an OCI image from a declarative YAML manifest. There is noRUNstep, no shell, and no container runtime involved in the build.
Because an apko build is a pure package install with no arbitrary shell execution, the images are bit-for-bit reproducible: the same config and package versions produce the same image hash on any machine. Every build also emits an SPDX SBOM, so scanners like Trivy, Grype, and Snyk can read every byte of the image rather than reporting "unknown" rows against a scratch image.
This guide sets up a RamNode VPS as a dedicated, hardened build host. Unlike the other guides in this series, the deliverable is not a long-running service; it is a repeatable build environment plus an optional private registry to publish signed images.
For new projects in 2026, prefer the Wolfi package repository over plain Alpine. Wolfi is glibc-based, ships only modern package versions, and is curated specifically for container use. apko can point at any apk repository, so Alpine remains available for musl or Alpine-tooling dependencies.
Prerequisites
- A RamNode VPS running Ubuntu 24.04 LTS. A 2 GB RAM plan handles most builds; multi-architecture builds benefit from 4 GB.
- Root or sudo access.
- A domain pointing at the VPS if you plan to run the optional private registry with TLS.
1. Initial server hardening
Create a non-root user and set the baseline firewall.
adduser build
usermod -aG sudo build
rsync --archive --chown=build:build ~/.ssh /home/buildIn /etc/ssh/sshd_config set PermitRootLogin no and PasswordAuthentication no, then systemctl reload ssh.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw enableIf you add the private registry later, you will open 443 at that point. Nothing else needs to be exposed for building.
2. Install Docker
melange and apko run cleanly from their official container images, which is the simplest way to get the correct apk tooling without polluting the host. Install Docker Engine:
sudo apt update && sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
| sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) \
signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo $VERSION_CODENAME) stable" \
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo usermod -aG docker buildLog out and back in so the build user picks up the docker group.
Define convenience aliases in ~/.bashrc for the build user:
alias melange='docker run --privileged --rm -v "$PWD":/work cgr.dev/chainguard/melange'
alias apko='docker run --rm -v "$PWD":/work -w /work cgr.dev/chainguard/apko'melange requires --privileged because it builds packages inside an isolated environment. apko does not.
3. Build your first package with melange
melange builds an apk from a declarative pipeline. Create a working directory and a melange.yaml. This example packages a simple Go HTTP server; adapt the pipeline to your own source.
package:
name: hello-server
version: 1.0.0
epoch: 0
description: "A minimal hello world HTTP server"
copyright:
- license: Apache-2.0
environment:
contents:
repositories:
- https://packages.wolfi.dev/os
keyring:
- https://packages.wolfi.dev/os/wolfi-signing.rsa.pub
packages:
- build-base
- go
pipeline:
- runs: |
go build -o ${{targets.destdir}}/usr/bin/hello-server .Generate a signing key so your packages are verifiable, then build:
melange keygen
# writes melange.rsa (private) and melange.rsa.pub (public)
melange build melange.yaml --signing-key melange.rsa --arch hostThis creates a packages/ directory with a subdirectory per architecture, each containing the signed .apk. To build for multiple architectures, replace --arch host with --arch amd64,aarch64.
Keep melange.rsa off the build host once you move to CI. For the manual workflow here, protect it with chmod 600 and never commit it.
4. Build the OCI image with apko
apko composes your package into a minimal image. Create apko.yaml referencing the local package repository built in the previous step:
contents:
keyring:
- https://packages.wolfi.dev/os/wolfi-signing.rsa.pub
- melange.rsa.pub
repositories:
- https://packages.wolfi.dev/os
- '@local /work/packages'
packages:
- wolfi-baselayout
- ca-certificates-bundle
- hello-server@local
accounts:
groups:
- groupname: nonroot
gid: 65532
users:
- username: nonroot
uid: 65532
run-as: 65532
entrypoint:
command: /usr/bin/hello-server
archs:
- amd64The @local notation tells apko to resolve packages from the mounted /work/packages directory, and hello-server@local pins the package to that local repository. Build the image:
apko build apko.yaml hello-server:1.0.0 hello-server.tar --arch hostThis produces hello-server.tar (a loadable OCI image) plus an SPDX SBOM file per architecture. Load and run it locally to verify:
docker load < hello-server.tar
docker run --rm -p 8080:8080 hello-server:1.0.0-amd64Because the image contains only your binary and its declared runtime dependencies, there is no shell and no package manager inside it, which is the point: a smaller attack surface and a clean scanner report.
5. Sign images with cosign
Install cosign to sign the resulting images and verify provenance. Keyless signing via Sigstore is the recommended approach in CI; for a standalone build host you can use a local key pair.
VERSION=$(curl -s https://api.github.com/repos/sigstore/cosign/releases/latest \
| grep -oP '"tag_name": "\K[^"]+')
curl -Lo cosign \
"https://github.com/sigstore/cosign/releases/download/${VERSION}/cosign-linux-amd64"
sudo install -m 0755 cosign /usr/local/bin/cosign
rm cosign
cosign generate-key-pairOnce an image is pushed to a registry (next section), sign and verify it:
cosign sign --key cosign.key registry.example.com/hello-server:1.0.0
cosign verify --key cosign.pub registry.example.com/hello-server:1.0.0In a CI pipeline, Chainguard publishes reusable GitHub Actions that wire melange and apko together with Sigstore keyless OIDC signing. That mints an ephemeral signing key per run and discards the private key, so no long-lived key sits in a secret store. Move to that model once you graduate from the manual workflow.
6. Optional: a private registry to publish images
If you want the build host to also publish images, run a lightweight OCI registry. zot is a good distroless choice; the standard registry:2 also works. Here is the standard registry behind Caddy for automatic TLS.
Run the registry bound to localhost:
docker run -d --restart unless-stopped \
--name registry \
-p 127.0.0.1:5000:5000 \
-v /opt/registry/data:/var/lib/registry \
registry:2Install Caddy and create a Caddyfile:
registry.example.com {
reverse_proxy 127.0.0.1:5000
basicauth {
ci JDJhJDE0... # bcrypt hash from: caddy hash-password
}
}Open the firewall for HTTPS only:
sudo ufw allow 443/tcp
sudo ufw allow 80/tcpNow publish directly from apko, which writes both per-architecture images and an OCI index in one call and attaches the SBOM:
apko publish apko.yaml registry.example.com/hello-server:1.0.0 \
--arch amd64,aarch647. Keeping builds reproducible and current
- Pin package versions in your melange and apko configs for reproducibility, and rebuild on a schedule to pull in security patches. Chainguard rebuilds its own catalog nightly for exactly this reason.
- Store your
melange.yaml,apko.yaml, and public keys in version control. Treat private signing keys as secrets kept outside the repo. - Verify the apko and melange images you pull. Chainguard signs them keylessly; you can confirm provenance with
cosign verifyagainst the GitHub Actions release identity.
RamNode platform notes
- This is a build host, not a network service, so its exposed surface is minimal. Keep only SSH open unless you run the optional registry.
- RamNode's prohibition on mail services is not relevant to apko or melange, since neither sends email. If you add build notifications, route them through an external API-based service rather than an on-box SMTP relay.
Troubleshooting
- melange fails with a permissions or mount error: confirm the
--privilegedflag is present and that the current directory is mounted to/work. - apko cannot find your local package: confirm the
@local /work/packagesrepository line is present and thathello-server@localuses the@localsuffix. Thepackages/directory must be in the mounted working directory. - Signature verification fails in apko: confirm
melange.rsa.pubis listed undercontents.keyringinapko.yaml. - SBOM warnings during build: warnings about SBOM upload types are generally safe to ignore for local builds.
