Prebuilt, multi-arch container images are published to the GitHub Container
Registry (GHCR) for every release and every push to main. Two variants are
built, differing only in base image:
| Variant | Base image | Tag suffix |
|---|---|---|
| CentOS Stream | quay.io/centos/centos |
(none) |
| Alpine | alpine |
-alpine |
Both variants are built for linux/amd64 and linux/arm64 and published as a
single multi-arch manifest per variant, so docker/podman pulls the right
architecture automatically. The images are published as
ghcr.io/voxpupuli/openvox-ca.
$ docker pull ghcr.io/voxpupuli/openvox-ca:latest # CentOS Stream
$ docker pull ghcr.io/voxpupuli/openvox-ca:latest-alpine # Alpine| Tag | Points at |
|---|---|
latest / latest-alpine |
The most recent release |
1.2.3, 1.2, 1 (+ -alpine) |
A specific release and its semver aliases; the major-only tag (1) is published for v1+ releases only, so 0.x releases have no 0 tag |
edge / edge-alpine |
The latest build from the default branch (main) |
main / main-alpine |
Same as edge; the head of main |
Pin to a specific semver tag (e.g. 1.2.3) for reproducible deployments;
edge tracks unreleased changes and can break at any time.
Every published image is signed and carries SLSA v1.0 build provenance, through Sigstore — there is no long-lived signing key involved. Pin the identity to the release shape rather than accepting anything this repository signed, because pull-request builds are signed too:
$ cosign verify ghcr.io/voxpupuli/openvox-ca:1.2.3 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github\.com/voxpupuli/openvox-ca/\.github/workflows/container-images\.yml@refs/tags/v'Each per-architecture image also carries an SBOM in both SPDX-JSON and CycloneDX-JSON, catalogued from the image itself so the base-layer packages are included as well as the Go modules. Those are attached to the architecture digests, not to the multi-arch index a tag resolves to; see verifying what you downloaded for the two-step form, and verifying a release for what each check does and does not prove.
The image's entrypoint is openvox-ca; any arguments you pass are appended to
it, exactly like running the binary. Mount a volume for the CA directory so the
CA survives container restarts, and publish port 8140:
$ docker run -d --name openvox-ca \
-p 8140:8140 \
-v openvox-ca-data:/data \
ghcr.io/voxpupuli/openvox-ca:latest \
--cadir=/data --hostname=puppet.example.com \
--tls-cert=/data/ca_crt.pem --tls-key=/data/private/ca_key.pemOn first run this bootstraps a new CA under /data and serves HTTPS on port
8140, using the CA's own certificate as the TLS server certificate. (The
server refuses plain HTTP on a non-loopback address unless --no-tls-required
is set — only do that behind a trusted TLS-terminating proxy or in test
environments.) For a production deployment — a TLS certificate matching the
server's DNS name, mTLS, an alternative storage backend, autosigning — pass
the relevant flags (or mount a config file and set --config). See
configuring the server for the full reference, and the
HTTP API reference for the endpoints agents use.
Both variants run as the non-root user puppet, uid/gid 1000, declared
numerically so that a host which cannot read the image's /etc/passwd can
still tell who the process runs as. Under Kubernetes, a container whose image
only names its user fails to start when the pod sets runAsNonRoot without
also setting runAsUser — the kubelet cannot verify that a name is non-root,
and the container stops at CreateContainerConfigError.
A named volume like the one above is created with the right ownership
automatically. A bind mount is not, so chown 1000:1000 the host directory
before starting the container, or the CA cannot write its state. Under
rootless Podman the container's uid 1000 maps to a subordinate uid on the
host, so a plain chown leaves the directory unwritable — use podman unshare chown 1000:1000 <dir>, or mount with the :U suffix. Under
Kubernetes, fsGroup: 1000 on the pod covers PersistentVolumeClaims and
emptyDir, but the kubelet does not apply it to a hostPath, which needs
the same treatment as a bind mount.
The compose.yml at the repository root is the equivalent
Docker/Podman Compose deployment: edit --hostname, then docker compose up -d (or podman-compose up -d). The test/compose*.yml files, by contrast,
are the integration-test topologies — they build throwaway images from the
working tree and are not deployment examples.
Autosigning is off by default. Only set
--autosign-config=truein dev/test environments: it lets any CSR submitter obtain a signed certificate without operator review.
How these images are built and published (the GitHub Actions workflow, the tag matrix, and the one-time repository setup a maintainer performs) is documented in publishing container images.