Adding a new container image

Step-by-step guide for adding a new service or cron-job container image with locked dependencies

This guide covers adding a new service/cron-job image to an existing repo. The main flow uses Python + RPM in cki-tools as the example; adjust for RPM-only builder images as noted.

Quick-reference checklist

[ ] builds/<name>/Containerfile.in
[ ] builds/<name>/rpms/rpms.in.yaml + rpms.lock.yaml
[ ] builds/<name>/requirements.lock (if Python deps)
[ ] .gitlab-ci.yml matrix entry (IMAGE_NAME, CHANGES, SMOKE_TEST_COMMANDS)

Detailed steps

1. Create Containerfile.in

Create builds/<name>/Containerfile.in with a digest-pinned base image. Fetch the current digest:

skopeo inspect --no-tags docker://quay.io/fedora/fedora:44 | jq -r .Digest

Start the file with:

/* renovate: datasource=docker depName=quay.io/fedora/fedora */
#define _BASE_IMAGE_TAG 44@sha256:<digest>
#include "setup"

The #define MUST come before the #include. Include the digest from the start for reproducible builds – do not rely on “Renovate adds it on first run.”

2. Create rpms.in.yaml

Create builds/<name>/rpms/rpms.in.yaml. Copy from an existing image (e.g., builds/pipeline-herder/rpms/rpms.in.yaml in cki-tools) as a template.

Required top-level keys:

installWeakDeps: false
zchunk: false

Required repos: fedora + updates with dependency-archive baseurls. For images targeting s390x/ppc64le, also add fedora-secondary and updates-secondary repos with skip_if_unavailable: true.

The context.image: must match the _BASE_IMAGE_TAG value (same version + digest) with its own # renovate: comment.

3. Generate RPM lockfile

make builds/<name>/rpms/rpms.lock.yaml

Or use podman directly with the dependency-tools image if no Makefile target exists yet.

4. Create Python lockfile (if needed)

Generate the per-image lockfile constrained by the root:

make builds/<name>/requirements.lock

The header command will look like:

#    uv pip compile --constraint=requirements.lock --exclude-newer=P7D --python-version=3.13 ...

Use --no-emit-package=<python-name> for any packages already provided by the RPM lockfile (e.g., --no-emit-package=pyyaml if python3-pyyaml is in rpms.in.yaml).

5. Tag build-only packages

If packages are only needed at build time (e.g., gcc, python3-devel, python3-pip, git-core), tag them with # builddep in rpms.in.yaml:

packages:
  - gcc  # builddep
  - python3-devel  # builddep
  - python3-pyyaml

6. Add includes to Containerfile.in

#include "rpm-lockfile"

#define _PYTHON_REQUIREMENTS_LOCKFILE
#include "python-requirements"

/* Add #include "verify-python-lockfiles" here if using --no-emit-package */

#include "cleanup"

The #define _PYTHON_REQUIREMENTS_LOCKFILE MUST come before #include "python-requirements". If the image uses --no-emit-package= to exclude RPM-provided packages, also add #include "verify-python-lockfiles" before #include "cleanup".

Alternatively, for more control over the install sequence, use inline pip commands instead of the python-requirements include (as done in datawarehouse and kernel-qe-tools – see Container images for both patterns).

7. Register in CI build matrix

Add to .gitlab-ci.yml under the parallel matrix:

- IMAGE_NAME: <name>
  CHANGES: builds/<name>
  SMOKE_TEST_COMMANDS: "python -c 'import my_package'"

8. Verify locally

In the containers repo: cpp -E -traditional -undef -Iincludes builds/<name>/Containerfile.in for quick preprocessing checks, or cki_build_image.sh for a full build.

In service repos (cki-tools, datawarehouse, etc.): Use cki_build_image.sh or podman build with the buildah image. Raw cpp won’t work because includes are baked into the buildah image, not in the local working tree:

podman run \
    --rm \
    -e IMAGE_NAME=<name> \
    --privileged \
    -w /code \
    -v .:/code \
    -v ~/.local/share/containers:/var/lib/containers \
    quay.io/cki/buildah:production \
    cki_build_image.sh

Lockfile header format

The uv pip compile command in the lockfile header must follow CI validation rules (see Lockfiles). Use long-form options with = separators, include --exclude-newer=P7D, include --no-strip-extras on the root lockfile. CI rejects MRs with non-conforming headers.

Variant: RPM-only builder images

Builder images in the containers repo are simpler: RPM lockfile only, no Python lockfile. May need the rpm-lockfile-repos include for RHEL builders to set up correct repository configuration.