Skip to content
CommonLayer
Documentation menu
On this page

CommonLayer container image standard, version 1

Every CommonLayer image follows these rules. Each rule has an id; a project that cannot meet a rule records a deviation in its commonlayer.yaml:

images:
  - name: example
    deviations:
      - standard: healthcheck          # rule id from the table below
        reason: Why the rule does not apply, in a full sentence.

The platform shows deviations on the artifact page; scripts/test.sh skips exactly the checks of the rules a project deviates from, and nothing else. The reference implementation is templates/project-template/; the base image and nginx projects (repositories commonlayer-org/base and commonlayer-org/nginx, kept under projects/base and projects/nginx until those repositories exist) are worked examples.

Rules

IdRuleHow it is checked
baseThe final stage starts FROM ${BASE_IMAGE}: the CommonLayer base image, pinned by digest (registry.commonlayer.org/base:<tag>@sha256:...).validate.sh (last FROM); build.sh refuses an unpinned BASE_IMAGE and refuses to push without images[].base.digest.
sourceUpstream software is built from the upstream release tarball, verified by SHA-256 (and by signature where upstream signs) in the build stage. The version and checksum are build arguments (UPSTREAM_VERSION, UPSTREAM_SHA256, UPSTREAM_URL) that mirror versions[] of commonlayer.yaml.validate.sh compares the Dockerfile defaults with the manifest; fetch-source.sh refuses any download whose checksum (or signature) does not verify.
multi-stageBuild tools exist only in build stages. The final image contains no compiler, no package manager, no curl/wget/nc or other network client, no debugging tools.test.sh lists the image filesystem and the BusyBox applets and fails on any forbidden binary; checks that the apk database is present.
non-rootRuns as UID/GID 10001:10001 (commonlayer). No USER root, no setuid/setgid files, no sudo/gosu/su-exec.test.sh: image User, UID of PID 1, setuid/setgid scan.
filesystem/app application files (read-only, owned by root), /etc/commonlayer/<app> configuration, /var/lib/commonlayer/<app> data, /var/log/commonlayer/<app> logs (applications log to stdout/stderr by default), /tmp scratch.Base image layout (the base project's tests/base.sh); reviews.
read-only-rootfsWorks with a read-only root filesystem when /tmp and the paths in images[].volumes are mounted writable.test.sh runs the container with --read-only, tmpfs on /tmp and the declared volumes, and fails on "Read-only file system" or "Permission denied" in the log.
entrypoint/usr/local/bin/commonlayer-entrypoint, POSIX sh, set -eu, ends in exec (the application becomes PID 1). Supports <VAR>_FILE for every secret variable. Never prints secrets. Uses /usr/local/lib/commonlayer/entrypoint-lib.sh.test.sh (ENTRYPOINT); the base project's tests/base.sh tests the library; project tests.
signalsThe application shuts down gracefully on the stop signal; STOPSIGNAL is set where upstream expects another signal (nginx: SIGQUIT).test.sh: docker stop must end the container with exit code 0 before the stop timeout.
healthcheckHEALTHCHECK using cl-probe or the application's own client.test.sh waits for healthy.
envVariables are prefixed with the application name (NGINX_*). Where upstream's official image has an established variable (for example POSTGRES_PASSWORD), the same name is supported.Review; project tests.
labelsOCI annotations org.opencontainers.image.{title,description,version,revision,created,source,url,documentation,licenses,vendor,base.name,base.digest} and org.commonlayer.{project,variant,upstream.version,revision,standard}.test.sh checks presence and the values that must agree with the manifest and the build. The platform checks them again (image.labels).
platformslinux/amd64 and linux/arm64, built on native runners and joined into one index.build.yml (matrix of native runners, build.sh merge); platform check platforms.complete.
reproducibilitySOURCE_DATE_EPOCH honoured; inputs pinned (base and builder images by digest, sources by checksum, tools by version and checksum). Best effort; never claimed as bit-for-bit reproducible.Review.
licensesLicense texts under /usr/share/licenses/ (the upstream application's own license file; an SPDX expression per Alpine package). A third-party license inventory generated from the SBOM is published with each release.generate-license-inventory.sh fails on unknown or non-allowlisted licenses.
sbom-sign-provenanceSPDX and CycloneDX SBOMs, cosign keyless signature, SLSA v1 provenance attestation, all attached to the digest.Release workflows; platform checks sbom.*, signature.*, provenance.*.

The base image

registry.commonlayer.org/base:<alpine minor> (the base image project, commonlayer-org/base) is the root filesystem every image starts from: musl, a BusyBox rebuilt from Alpine's own source with network clients and administration applets compiled out, CA certificates, tzdata, the commonlayer user (10001:10001), the standard directories, cl-probe, the entrypoint library, the apk database (so scanners can identify packages) and no package manager. See the base project's README. A project builds from a base release by digest (images[].base.digest). Before that digest exists, a build takes BASE_IMAGE, or builds the base project's repository with scripts/base-image.sh --build <checkout> (in CI: the repository variable CL_BASE_REPOSITORY); a release always needs the digest.

Runtime libraries from Alpine (for example OpenSSL or PCRE2) are added with image/build/add-runtime-packages.sh in a build stage, which installs them on a copy of the base root, emits only the new files plus the merged apk database, and refuses to continue if a base package would change version.

Build arguments

scripts/build.sh passes these to every Dockerfile:

ArgumentValue
UPSTREAM_VERSION, UPSTREAM_SHA256, UPSTREAM_URLversions[i].version, .source.sha256, .source.url
VERSION_LINE, CHANNELversions[i].line, .channel
IMAGE_REVISIONthe N of -rN (0 for local builds)
VCS_REF, CREATED, SOURCE_REPOSITORY, SOURCE_DATE_EPOCHcommit, RFC 3339 time derived from SOURCE_DATE_EPOCH, repository URL
BASE_IMAGE, BASE_NAME, BASE_DIGESTthe base reference name:tag@sha256:... and its parts (for the labels)

Entrypoint conventions

#!/bin/sh
set -eu
CL_APP=myapp
. /usr/local/lib/commonlayer/entrypoint-lib.sh
cl_require_non_root
cl_file_env MYAPP_PASSWORD          # MYAPP_PASSWORD or MYAPP_PASSWORD_FILE, never both
cl_check_value MYAPP_PORT '[0-9]{1,5}'
exec myapp --port "$MYAPP_PORT" "$@"

A healthcheck subcommand that runs cl-probe lets HEALTHCHECK and Kubernetes exec probes share one definition when the health endpoint is bound to loopback.

cl-probe

cl-probe [-t seconds] http <url> [expected-status]    # plain HTTP, e.g. 200 or 2xx (default 200)
cl-probe [-t seconds] tcp <host> <port>
cl-probe file <path>

Exit status 0 healthy, 1 unhealthy, 2 usage error. It replaces curl/wget in health checks; it has no TLS, follows no redirects and writes nothing.

Tags and revisions

<upstream version>-r<revision> is immutable (the platform refuses to move it); <upstream version>, <line>, the channel (stable, mainline, ...) and latest (stable channel) are mutable pointers. A rebuild of the same upstream version always gets a new revision.

Scanning and VEX

scripts/scan.sh first generates the SBOMs (scripts/generate-sbom.sh, so syft is needed), then runs Trivy and Grype on the image and on the CycloneDX SBOM; both gate. Software built from source is declared in the manifest (build.components) or, for CommonLayer's own helpers, in a JSON file shipped under /usr/share/commonlayer/sbom/; the build fails if a shipped declaration's version differs from the binary's. Only Grype matches declared components (by CPE); Trivy does not (ADR-0039). The gate fails on findings at or above security.gate that have a fix available, unless an OpenVEX statement assesses them not_affected (with a justification) or fixed. Statements live in the project's security/vex.openvex.json; statements about base packages ship in the base image (/usr/share/commonlayer/vex/) and are applied to every derived image. Suppressed findings are still counted in the scan summary. A clean scan is a point-in-time result, not a security guarantee.

Building behind a proxy

Every network-using RUN mounts the optional build secret extra_ca over the system CA bundle:

RUN --mount=type=secret,id=extra_ca,target=/etc/ssl/certs/ca-certificates.crt,required=false \
    apk add --no-cache build-base

scripts/docker-build.sh (used by build.sh) passes the proxy variables as build arguments, adds --network=host when the proxy listens on loopback, and passes CL_EXTRA_CA_FILE (default $SSL_CERT_FILE) as extra_ca. Without a proxy it behaves like docker buildx build.