Documents
CLI Container Images: Standard and Builder Variants
CLI Container Images: Standard and Builder Variants
Type
Document
Status
Published
Created
Jun 4, 2026
Updated
Jun 4, 2026

CLI Container Images: Standard and Builder Variants#

Overview#

The aetherpak/cli repository publishes two container image variants to the GitHub Container Registry (GHCR), each providing the aetherpak binary alongside a complete Flatpak toolchain:

  • ghcr.io/aetherpak/cli:<version> — the standard CLI image, suitable for all non-build operations
  • ghcr.io/aetherpak/cli:<version>-builder — the builder CLI image, required whenever flatpak-builder must run

Both images ship the same statically-linked aetherpak binary compiled from the same source commit, with the difference between them lying entirely in the base image they are built upon . The standard variant is built on ghcr.io/aetherpak/flatpak; the builder variant is built on ghcr.io/aetherpak/flatpak-builder. This layered design means that every CLI release inherits its Flatpak toolchain from a specific snapshot of the aetherpak/flatpak-containers images.

Both variants are published as multi-architecture OCI manifests covering linux/amd64 and linux/arm64. Container runtimes (Docker, Podman) automatically select the correct variant for the host machine when pulling by tag. Architecture-specific tags (e.g. v0.15.1-amd64, v0.15.1-builder-arm64) are also available for use cases that require an explicit architecture pin.

The aetherpak/actions reusable workflow (aetherpak/actions/.github/workflows/publish.yml@v3) uses these images internally for all pipeline jobs — no additional toolchain setup is required when running inside these containers . Understanding the two variants and their intended use cases helps you configure custom workflows and debug pipeline behaviour.

These images are for running the CLI, not developing it. If you are developing the aetherpak CLI itself and need a local build environment, use the underlying ghcr.io/aetherpak/flatpak or ghcr.io/aetherpak/flatpak-builder images directly — see Utility Images: flatpak and flatpak-builder for details.

Image Variants#

Comparison#

PropertyStandard (<version>)Builder (<version>-builder)
Full image referenceghcr.io/aetherpak/cli:<version>ghcr.io/aetherpak/cli:<version>-builder
Containerfile stageclicli-builder
Base imageghcr.io/aetherpak/flatpakghcr.io/aetherpak/flatpak-builder
aetherpak binary
Flatpak tooling (flatpak, ostree, git, jq, gpg)✓ (inherited)
flatpak-builder
flatpak-builder-lint
elfutils, appstream, desktop-file-utils
gobject-introspection, cairo, patch
Privileged mode requiredNoYes
Architecture supportlinux/amd64, linux/arm64linux/amd64, linux/arm64

Standard image use cases: plan, prep-bundle, publish-oci, publish-site — any step that only requires the Flatpak client, OSTree tooling, and the aetherpak binary .

Builder image use cases: build-manifest — the step that compiles a Flatpak application from a manifest using flatpak-builder .

When the builder tag is REQUIRED: any time you need to compile a Flatpak application from a manifest using flatpak-builder. The builder image requires --privileged mode because flatpak-builder needs to write to the system Flatpak installation to install runtimes and SDKs from the pre-configured Flathub remote. Running as root inside a privileged container lets flatpak-builder write directly to the system install location without requiring the polkit/dbus system helper — which is not available in a container environment . Non-build operations do not require --privileged.

Multi-Architecture and Single-Architecture Tags#

Both images are published as multi-arch OCI manifest lists. Architecture-specific tags are also available for workflows that must target an explicit architecture :

Tag patternDescription
ghcr.io/aetherpak/cli:v0.15.1Multi-arch manifest (auto-selects amd64 or arm64)
ghcr.io/aetherpak/cli:v0.15.1-builderMulti-arch builder manifest
ghcr.io/aetherpak/cli:v0.15.1-amd64Standard image, amd64 only
ghcr.io/aetherpak/cli:v0.15.1-arm64Standard image, arm64 only
ghcr.io/aetherpak/cli:v0.15.1-builder-amd64Builder image, amd64 only
ghcr.io/aetherpak/cli:v0.15.1-builder-arm64Builder image, arm64 only

For most workflows, pull the multi-arch manifest tag. Single-architecture tags are most useful when you know the runner architecture in advance and want to avoid the manifest-list resolution step.

Architecture: How CLI Images Use Utility Images#

The CLI images are not built from scratch. They are a thin layer on top of the foundational utility images from the aetherpak/flatpak-containers repository. The Containerfile in aetherpak/cli defines two build arguments that reference them directly :

ARG BASE_IMAGE=ghcr.io/aetherpak/flatpak:latest
ARG BUILDER_IMAGE=ghcr.io/aetherpak/flatpak-builder:latest

Multi-Stage Build Structure#

The cli Containerfile uses three build stages :

Stage 1 — cli-binary-builder: Compiles the aetherpak binary from source using golang:1.26-alpine. The binary is built with CGO_ENABLED=0 for full static linking and stripped of debug symbols with -ldflags="-s -w", producing a compact, dependency-free binary at /bin/aetherpak.

Stage 2 — cli: Starts from ${BASE_IMAGE} (ghcr.io/aetherpak/flatpak) and copies the compiled binary to /usr/local/bin/aetherpak. This produces the standard CLI image.

Stage 3 — cli-builder: Starts from ${BUILDER_IMAGE} (ghcr.io/aetherpak/flatpak-builder) and copies the same compiled binary. This produces the builder CLI image.

FROM ${BASE_IMAGE} AS cli
LABEL org.opencontainers.image.title="AetherPak CLI" \
      org.opencontainers.image.description="AetherPak image with the pre-baked aetherpak CLI"
COPY --from=cli-binary-builder /bin/aetherpak /usr/local/bin/aetherpak
WORKDIR /workspace
CMD ["/bin/bash"]

FROM ${BUILDER_IMAGE} AS cli-builder
LABEL org.opencontainers.image.title="AetherPak Builder CLI" \
      org.opencontainers.image.description="AetherPak Builder image with the pre-baked aetherpak CLI"
COPY --from=cli-binary-builder /bin/aetherpak /usr/local/bin/aetherpak
WORKDIR /workspace
CMD ["/bin/bash"]

The compiled binary is identical across both variants . The only difference is the base image each stage inherits from — and with it, the toolchain that is available in the container.

What the Utility Images Provide#

The utility images are the authoritative source of the Flatpak toolchain in both CLI variants. Both are based on registry.fedoraproject.org/fedora-minimal with the base digest pinned for reproducibility. A pre-configured Flathub remote is added during image build so that runtimes and SDKs can be resolved without any additional setup. See Utility Images: flatpak and flatpak-builder for the full package manifests and architecture details.

Flathub Parity#

A core design goal is parity with the Flathub GitHub Actions builder environment. The builder image installs flatpak-builder-lint directly from the canonical flathub-infra upstream source (not from a versioned PyPI release), ensuring that lint behaviour in AetherPak CI matches what Flathub itself expects . This parity is inherited transitively by the cli-builder image.

Image Lineage Summary#

registry.fedoraproject.org/fedora-minimal (pinned digest)
    └── ghcr.io/aetherpak/flatpak
    │ └── ghcr.io/aetherpak/cli:<version> (standard)
    └── ghcr.io/aetherpak/flatpak-builder
            └── ghcr.io/aetherpak/cli:<version>-builder (builder)

Using CLI Containers in GitHub Actions#

Container Jobs vs. aetherpak/setup-cli#

There are two ways to run aetherpak in a GitHub Actions workflow:

  1. Container-based: Declare a container: block in the job definition referencing the appropriate CLI image. The job's steps execute inside the container, which already has the aetherpak binary and all toolchain dependencies pre-installed.

  2. aetherpak/setup-cli@v1: Run on a standard runner (e.g. ubuntu-latest) and use the action to download and install the CLI binary, optionally installing system dependencies via apt-get, dnf, or microdnf.

The aetherpak/actions reusable workflow uses containers internally for all its jobs . When you call the reusable workflow, you are already running inside the CLI container — no additional setup is needed.

When to prefer containers over setup-cli#

  • Consistent, pre-configured environment. The CLI image bundles all required system libraries and Flatpak tooling. There is no apt-get install step, no version drift, and no risk of a package manager outage affecting your build.
  • Faster startup. Frequently used image layers are cached by the runner. Subsequent workflow runs skip the download entirely for cached layers.
  • Locked toolchain version. The image tag pins the aetherpak binary, the Flatpak version, ostree, and every other dependency together into a single, immutable unit.
  • Alignment with the reusable workflow. If you are writing a custom workflow that mirrors what the reusable workflow does, using the same container images ensures identical behaviour.

When to prefer setup-cli#

  • Self-hosted or custom runners. Container-based jobs require a container runtime on the runner. If your runners don't support Docker or Podman, setup-cli is the right choice.
  • Mixed workflows. If your job needs to interleave AetherPak steps with tools that are not available inside the CLI container, the bare-runner approach gives you more flexibility.
  • Additional tooling. When your workflow needs to install extra dependencies beyond what the CLI image provides, it is easier to layer them on a standard runner than to customise the container image.
  • No matching container image. If you specify a cli-version that does not have a published container image tag, non-build jobs automatically fall back to aetherpak/setup-cli . The setup-cli fallback runs on ubuntu-latest and installs dependencies via apt-get.

Fallback behaviour. If a specified cli-version has no corresponding published container image tag, non-build jobs automatically fall back to setup-cli. The build job (build-manifest) does not have a graceful fallback — it always requires the builder container image.

Basic Job Definition#

Use the container: key in your job definition to run inside the CLI image:

jobs:
  publish-oci:
    name: Push to registry
    runs-on: ubuntu-latest
    container:
      image: ghcr.io/aetherpak/cli:v0.15.1
    steps:
      - uses: actions/checkout@v6
      - run: aetherpak publish-oci --repo-path _repo

For a build-manifest job that needs to compile a Flatpak, use the builder image with --privileged :

jobs:
  build:
    name: Build Flatpak
    runs-on: ubuntu-latest
    container:
      image: ghcr.io/aetherpak/cli:v0.15.1-builder
      options: --privileged
    steps:
      - uses: actions/checkout@v6
      - run: |
          aetherpak build-manifest \
            --manifest org.example.App.json \
            --arch x86_64 \
            --repo-path _repo

Complete Workflow Example#

The following example mirrors how the reusable workflow structures its jobs — a plan job on the standard image feeding a build job on the builder image:

name: Publish Flatpak

on:
  push:
    tags: ['v*']

jobs:
  plan:
    name: Plan build matrix
    runs-on: ubuntu-latest
    container:
      image: ghcr.io/aetherpak/cli:v0.15.1
    outputs:
      matrix: ${{ steps.plan.outputs.matrix }}
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0
      - name: Plan
        id: plan
        run: aetherpak plan --output-file "$GITHUB_OUTPUT"

  build:
    name: Build ${{ matrix.app-id }} (${{ matrix.arch }})
    needs: plan
    runs-on: ubuntu-latest
    strategy:
      matrix:
        include: ${{ fromJSON(needs.plan.outputs.matrix) }}
    container:
      image: ghcr.io/aetherpak/cli:v0.15.1-builder
      options: --privileged
    steps:
      - uses: actions/checkout@v6
      - name: Build
        run: |
          aetherpak build-manifest \
            --manifest ${{ matrix.manifest }} \
            --arch ${{ matrix.arch }} \
            --repo-path _repo
      - uses: actions/upload-artifact@v7
        with:
          name: repo-${{ matrix.app-id }}-${{ matrix.arch }}
          path: _repo

Use the reusable workflow when you can. The aetherpak/actions/.github/workflows/publish.yml@v3 reusable workflow handles plan → build → publish orchestration including matrix expansion, artifact passing, and site deployment. Use a custom container job only when you need control that the reusable workflow doesn't expose.

Using CLI Containers in GitLab CI/CD#

The CLI container images are published to GHCR as public packages and can be pulled without authentication, making them straightforward to use as the image: in any GitLab CI/CD pipeline job.

GitLab CI Support in the CLI#

The aetherpak binary has explicit support for GitLab CI environments. Channel resolution, output formatting, and configuration all work without GitHub-specific plumbing:

Channel resolution via GitLab environment variables. The CLI detects GitLab CI environment variables to determine the channel to publish to :

VariableUsed for
CI_COMMIT_TAGIdentifies a tag event → resolves to the stable channel
CI_COMMIT_BRANCHIdentifies a branch push → resolves to the branch name (or beta for the default branch)
CI_DEFAULT_BRANCHIdentifies the default branch for beta channel resolution

The resolution priority order is: AETHERPAK_* overrides → GitHub Actions variables → GitLab CI variables → defaults. This means you can override channel behaviour in any CI platform using AETHERPAK_REF_TYPE, AETHERPAK_REF_NAME, and AETHERPAK_DEFAULT_BRANCH environment variables .

Dotenv output compatible with GitLab artifacts. The CLI's --output-file flag writes KEY=VALUE dotenv lines using the pkg/ciout package . GitLab's artifacts:reports:dotenv consumes this format natively, allowing step outputs to pass between jobs as artifact variables — the same mechanism as $GITHUB_OUTPUT on GitHub Actions.

AETHERPAK_-prefixed environment variables. Any aetherpak.yaml configuration key can be overridden via an AETHERPAK_-prefixed environment variable (e.g. AETHERPAK_REGISTRY, AETHERPAK_OCI_REPOSITORY). This lets you drive the full CLI configuration from GitLab CI/CD variables without committing secrets to the repository.

Privileged Mode for Build Jobs#

For jobs that invoke flatpak-builder (i.e. jobs using the builder image), the GitLab Runner must be configured with privileged = true in its config.toml. The requirement is identical to GitHub Actions: flatpak-builder must write to the system Flatpak installation when resolving and installing runtimes and SDKs from the pre-configured Flathub remote, which requires root access to the container's system directories.

Non-build jobs (plan, publish) using the standard image do not require privileged mode.

Example .gitlab-ci.yml#

The following example shows a two-job pipeline: a plan job using the standard image to resolve the channel, followed by a build job using the builder image to compile the Flatpak:

variables:
  AETHERPAK_REGISTRY: "registry.gitlab.com"
  AETHERPAK_OCI_REPOSITORY: "$CI_PROJECT_PATH"

stages:
  - plan
  - build
  - publish

plan:
  stage: plan
  image: ghcr.io/aetherpak/cli:v0.15.1
  script:
    - aetherpak plan --output-file plan.env
  artifacts:
    reports:
      dotenv: plan.env

build:
  stage: build
  image: ghcr.io/aetherpak/cli:v0.15.1-builder
  # GitLab Runner must have privileged = true in config.toml
  before_script:
    - echo "Building for arch ${ARCH}"
  script:
    - aetherpak build-manifest
        --manifest org.example.App.json
        --arch x86_64
        --repo-path _repo
  artifacts:
    paths:
      - _repo/
  needs:
    - job: plan
      artifacts: true

publish:
  stage: publish
  image: ghcr.io/aetherpak/cli:v0.15.1
  script:
    - aetherpak publish-oci --repo-path _repo
  needs:
    - job: build
      artifacts: true

GitLab Runner privileged mode. The build job above requires the GitLab Runner to be registered with privileged = true. This is a runner-level configuration, not a per-job setting. Consult your GitLab administrator or runner configuration if you are unsure whether privileged mode is enabled for your project's runners.

Image Tags and Versioning#

Tagging Scheme#

CLI image tags follow the same version scheme as the aetherpak CLI releases. Each release creates a corresponding pair of image tags — one for each variant :

TagDescription
v0.15.1Standard image, CLI v0.15.1
v0.15.1-builderBuilder image, CLI v0.15.1
latestStandard image, most recent release
latest-builderBuilder image, most recent release
mainStandard image, latest commit on main branch
main-builderBuilder image, latest commit on main branch
v0.15.1-amd64Standard image, CLI v0.15.1, amd64 only
v0.15.1-arm64Standard image, CLI v0.15.1, arm64 only
v0.15.1-builder-amd64Builder image, CLI v0.15.1, amd64 only
v0.15.1-builder-arm64Builder image, CLI v0.15.1, arm64 only

During container image development, pull request builds create temporary pr-{N} and pr-{N}-builder tags. These are not pushed to the registry — they exist only for local testing purposes .

When Tags Are Published#

  • Version tags (v*): Published when a commit is pushed with a refs/tags/v* ref. Both latest and the specific version tag (e.g. v0.15.1) are applied simultaneously .
  • main tag: Published on every push to the main branch.
  • latest refresh: The latest and latest-builder tags are also rebuilt weekly (every Sunday at midnight UTC) via the refresh-latest workflow to pick up updates to the underlying ghcr.io/aetherpak/flatpak-containers base images — even without a new CLI release .

Pinning Recommendations#

Use caseRecommended tag
Production / reproducible buildsv0.15.1 / v0.15.1-builder (specific version)
Always on the latest stable releaselatest / latest-builder
Tracking development buildsmain / main-builder
Explicit architecture targetingv0.15.1-amd64, v0.15.1-builder-arm64, etc.

Pin to a specific version in production. The latest tag moves forward with each CLI release, which may introduce behaviour changes. For reproducible CI pipelines, pin to a specific version tag (e.g. v0.15.1) so that your toolchain does not change unexpectedly between workflow runs. The aetherpak/actions reusable workflow accepts a cli-version input for exactly this purpose .