SKA Base Images

This repository builds the SKAO base-image catalogue used by application images, CI jobs, static webserver deployments, and CUDA workloads.

The repository model is:

  • slim, single-version runtime images for deployable Python and Node applications

  • multi-version build images for CI and test matrices

  • generated and committed Dockerfiles driven by versions.yaml

  • one root .release shared by every image in the catalogue

  • CI builds the full image DAG per Ubuntu-base subtree in sequenced stages, passing parent images between jobs with BASE_IMAGE and reusing layers through a BuildKit registry cache

Runtime And Build Model

versions.yaml is the source of truth for Ubuntu bases, Python and Node versions, tool pins, parent relationships, and test expectations. The generated Dockerfiles are committed so a user can inspect and build a normal Dockerfile without running the generator first.

---
config:
  layout: dagre
  theme: mc
---
flowchart LR
  V["versions.yaml<br/>(single source of truth)"] --> GEN["scripts/apply_templates.py<br/>orchestrates render + drift-check<br/>(generate_templates · generate_ci · template_models)"]
  T["templates/*.Dockerfile.j2"] --> GEN
  GEN --> DF["committed Dockerfiles<br/>images/*/Dockerfile"]
  GEN --> CI[".gitlab/images.generated.yml<br/>build + test DAG"]
  R["root .release<br/>one catalogue version"] --> GEN
  DF --> RT["slim runtime images<br/>ska-python-py313 · ska-node-ubuntu24 · ..."]
  DF --> BLD["multi-version build images<br/>ska-build-python · ska-build-node"]
  CI --> BUILD["CI builds and tests<br/>amd64 + arm64"]

  classDef source stroke:#6d28d9,stroke-width:2px,fill:#ede9fe,color:#3b0764
  classDef generated stroke:#1d4ed8,stroke-width:2px,fill:#dbeafe,color:#1e3a8a
  classDef runtime stroke:#15803d,stroke-width:2px,fill:#dcfce7,color:#14532d
  classDef build stroke:#c2410c,stroke-width:2px,fill:#ffedd5,color:#7c2d12
  class V,T,R source
  class GEN,DF,CI generated
  class RT runtime
  class BLD,BUILD build

Image Families

Family

Images

Purpose

Ubuntu base

ska-base, ska-base-ubuntu24, ska-base-ubuntu26

CIS hardened Ubuntu bases

Python runtime

ska-python, ska-python-py312, ska-python-py313, ska-python-py314, ska-python-ubuntu24, ska-python-ubuntu24-py313, ska-python-ubuntu24-py314, ska-python-ubuntu26

Slim deploy images with one Python runtime

Python build

ska-build-python, ska-build-python-ubuntu24, ska-build-python-ubuntu26

CI/build images with the native Python and managed extra Python versions

Node runtime

ska-node, ska-node-ubuntu24, ska-node-ubuntu26

Slim deploy images with Node 22 plus npm, yarn, and pnpm, one per Ubuntu base

Node build

ska-build-node, ska-build-node-ubuntu24, ska-build-node-ubuntu26

CI/build images with Node 22 plus npm, yarn, pnpm, and browser test dependencies, one per Ubuntu base

CI / deploy toolbox

ska-cicd-runner

CI/deploy image replacing the deprecated ska-cicd-k8s-tools repo, with Docker, Helm, kubectl, release tooling, and managed extra Python versions

Utility

ska-ser-utils, ska-webserver

Kubernetes utility image and Nginx static webserver image (both on Ubuntu 26.04)

CUDA

ska-cuda, ska-cuda-ubuntu24, ska-build-cuda, ska-build-cuda-ubuntu24, ska-build-cuda-11

NVIDIA CUDA runtime and build images

The current runtime matrix is:

Ubuntu base

Python runtime images

Python build image

22.04

ska-python (3.10), ska-python-py312 (3.12.13), ska-python-py313 (3.13.13), ska-python-py314 (3.14.4)

ska-build-python (3.10, 3.12.13, 3.13.13, 3.14.4)

24.04

ska-python-ubuntu24 (3.12), ska-python-ubuntu24-py313 (3.13.13), ska-python-ubuntu24-py314 (3.14.4)

ska-build-python-ubuntu24 (3.12, 3.13.13, 3.14.4)

26.04

ska-python-ubuntu26 (3.14)

ska-build-python-ubuntu26 (3.14)

Ubuntu base

Node runtime image

Node build image

22.04

ska-node (22.22.3)

ska-build-node (22.22.3)

24.04

ska-node-ubuntu24 (22.22.3)

ska-build-node-ubuntu24 (22.22.3)

26.04

ska-node-ubuntu26 (22.22.3)

ska-build-node-ubuntu26 (22.22.3)

Image Build Graph

The generated build graph follows the parent relationships in versions.yaml. Changing a parent image causes every descendant to rebuild and test in CI.

---
config:
  layout: dagre
  theme: mc
---
flowchart TB
  U22["ubuntu:22.04"] --> B22["ska-base"]
  U24["ubuntu:24.04"] --> B24["ska-base-ubuntu24"]
  U26["ubuntu:26.04"] --> B26["ska-base-ubuntu26"]

  B22 --> P22["ska-python<br/>3.10"]
  B22 --> P312["ska-python-py312"]
  B22 --> P313["ska-python-py313"]
  B22 --> P314["ska-python-py314"]
  B22 --> BP22["ska-build-python"]
  B22 --> N22["ska-node"]
  B22 --> BN22["ska-build-node"]
  B22 --> BD22["ska-cicd-runner"]

  B24 --> P24["ska-python-ubuntu24"]
  B24 --> P24_313["ska-python-ubuntu24-py313"]
  B24 --> P24_314["ska-python-ubuntu24-py314"]
  B24 --> BP24["ska-build-python-ubuntu24"]
  B24 --> N24["ska-node-ubuntu24"]
  B24 --> BN24["ska-build-node-ubuntu24"]
  B26 --> P26["ska-python-ubuntu26"]
  B26 --> BP26["ska-build-python-ubuntu26"]
  B26 --> N26["ska-node-ubuntu26"]
  B26 --> BN26["ska-build-node-ubuntu26"]
  B26 --> SU["ska-ser-utils"]
  P26 --> W["ska-webserver"]

  classDef upstream stroke:#4b5563,stroke-width:2px,fill:#e5e7eb,color:#111827
  classDef base stroke:#6d28d9,stroke-width:2px,fill:#ede9fe,color:#3b0764
  classDef runtime stroke:#15803d,stroke-width:2px,fill:#dcfce7,color:#14532d
  classDef build stroke:#c2410c,stroke-width:2px,fill:#ffedd5,color:#7c2d12
  class U22,U24,U26 upstream
  class B22,B24,B26 base
  class P22,P312,P313,P314,N22,N24,N26,SU,W,P24,P24_313,P24_314,P26 runtime
  class BP22,BN22,BN24,BN26,BP24,BP26,BD22 build

CUDA images are hand-written because they follow NVIDIA-published CUDA/Ubuntu pairs rather than the generated language-runtime matrix.

---
config:
  layout: dagre
  theme: mc
---
flowchart TB
  C12["nvidia/cuda:12.9.1-runtime-ubuntu22.04"] --> R12["ska-cuda"]
  D12["nvidia/cuda:12.9.1-cudnn-devel-ubuntu22.04"] --> B12["ska-build-cuda"]
  C13["nvidia/cuda:13.2.0-runtime-ubuntu24.04"] --> R13["ska-cuda-ubuntu24"]
  D13["nvidia/cuda:13.2.0-cudnn-devel-ubuntu24.04"] --> B13["ska-build-cuda-ubuntu24"]
  C11["nvidia/cuda:11.8.0-devel-ubuntu22.04"] --> B11["ska-build-cuda-11"]

  classDef upstream stroke:#4b5563,stroke-width:2px,fill:#e5e7eb,color:#111827
  classDef runtime stroke:#15803d,stroke-width:2px,fill:#dcfce7,color:#14532d
  classDef build stroke:#c2410c,stroke-width:2px,fill:#ffedd5,color:#7c2d12
  class C12,D12,C13,D13,C11 upstream
  class R12,R13 runtime
  class B12,B13,B11 build

Generated Image Definitions

Language and utility Dockerfiles are generated from versions.yaml and templates/*.Dockerfile.j2. The generator writes:

  • images/*/Dockerfile

  • .gitlab/images.generated.yml

The generator is split across focused modules that scripts/apply_templates.py orchestrates:

  • scripts/template_models.py — parses and validates versions.yaml

  • scripts/generate_templates.py — renders the per-image Dockerfiles

  • scripts/generate_ci.py — renders the CI build/test DAG

  • scripts/apply_templates.py — the entry point that ties them together and runs the drift check

To update runtime or tool versions, edit versions.yaml, regenerate, and commit the rendered files:

uv run python scripts/apply_templates.py
uv run python scripts/apply_templates.py --check

CI runs the check form to catch template drift.

The generated CI DAG groups image builds by Ubuntu-base subtree and runs those groups one at a time in dedicated stages (build-ubuntu22, build-ubuntu24, build-ubuntu26, build-cuda). Serialising the groups keeps peak load bounded so the QEMU-emulated arm64 leg does not exhaust the runner. Within a group each base build exports a BuildKit registry cache (--cache-to) that its descendants consume (--cache-from) to reuse warm layers. The cache is written to a dedicated buildcache-<sha> tag, kept separate from the published image tag so that plain docker pull in the scan/compliance jobs is unaffected.

Generated Dockerfiles keep ARG BASE_IMAGE=<ref> defaults set to the current root release. versions.yaml supports an optional parent_refs map to pin a parent to an exact published ref instead of the release-derived default (useful when a parent is not yet published for the release being built); it is currently commented out, so all images default to the root release. CI overrides BASE_IMAGE for child jobs so descendants can build from parent images produced earlier in the same pipeline.

All images use the root .release file. Per-image .release files are not used.

Provenance and image labels

Image provenance is recorded in three complementary layers. The previous approach of hand-injecting int.skao.image.version/created/tags labels from the Makefile and writing a skao.metadata side-file has been removed.

OCI standard annotations — committed in every Dockerfile. Each image carries the cross-ecosystem org.opencontainers.image.* labels that registries, vulnerability scanners, and Docker Scout read. Static fields are baked in; version/revision come from build args the build passes (empty on a bare docker build). Generated images share templates/_provenance.j2; the hand-written CUDA Dockerfiles carry the same block inline.

Label

Value

org.opencontainers.image.title

image name

org.opencontainers.image.description

short description

org.opencontainers.image.vendor

SKA Observatory

org.opencontainers.image.licenses

BSD-3-Clause

org.opencontainers.image.url / .source / .documentation

repository URLs

org.opencontainers.image.base.name

the BASE_IMAGE build arg (parent ref)

org.opencontainers.image.version

${SKA_BUILD_VERSION} build arg (published tag)

org.opencontainers.image.revision

${SKA_BUILD_COMMIT} build arg (commit SHA)

org.opencontainers.image.created is intentionally not baked in — a build timestamp would make layers non-reproducible. Build time is captured by CI_COMMIT_TIMESTAMP (below) and by buildx attestations.

SKA identity labels — committed in every Dockerfile. Kept for SKA tooling: int.skao.image.team, int.skao.image.url, int.skao.image.source (images/<image>/Dockerfile), and int.skao.image.baseImage (the BASE_IMAGE arg).

Dynamic build provenance — added at build time by .make. make oci-build (the target every CI job runs) attaches version and CI-context labels automatically via the SKA OCI support, so nothing is set per image:

Label

Source

SKA_BUILD_RELEASE

catalogue version from the root .release

SKA_BUILD_VERSION

published tag (<release>-dev.c<sha> for dev builds)

SKA_BUILD_COMMIT

short commit SHA

CI_COMMIT_SHA, CI_COMMIT_TIMESTAMP, CI_COMMIT_REF_NAME, …

GitLab commit metadata

CI_JOB_URL, CI_PIPELINE_URL, GITLAB_USER_*, …

GitLab build context

These replace the old int.skao.image.version/tags/created labels (→ SKA_BUILD_VERSION/SKA_BUILD_RELEASE, SKA_BUILD_VERSION, and CI_COMMIT_TIMESTAMP respectively).

Build attestations. Pipeline builds attach SLSA build provenance and an SBOM to each pushed image index (buildx --provenance=mode=max --sbom=true). These are signed build evidence (in-toto format) rather than metadata; inspect them with docker buildx imagetools inspect <ref>, and they are consumed by vulnerability scanners and policy engines.

Building Images

Build every image through the .make OCI targets:

make oci-build-all

Build a single generated image locally from its committed Dockerfile:

cd images/ska-python
docker build .

To build a child image from a locally produced or CI-produced parent, override BASE_IMAGE:

docker build --build-arg BASE_IMAGE=registry.example/ska-base:dev .

Maintenance Scripts

Two maintenance scripts share scripts/config.json as their configuration source:

  • scripts/bump_base_images.py — clones the downstream repositories listed in the config and patches their Dockerfiles to the latest published base_images tags.

  • scripts/deprecate_harbor_base_images.py — moves base_images artefacts older than deprecation.timedelta_days from the source Harbor project to the deprecated project (Harbor URL, projects, and robot-account credentials are supplied via environment variables). Run from the repository root, as the scheduled deprecate-images CI job does.

scripts/config.json holds:

  • base_images — the catalogue images shared by both scripts

  • repositories — downstream repos patched by the bump script

  • deprecation.timedelta_days — the age threshold for deprecation

ska-webserver

The ska-webserver differs from the other images as it is offering a tailored solution for teams to easily create OCI images to serve static web content while being able to proxy calls to backend APIs using best-practices, much like what is provided by NextJS and similar solutions. The main goal is to avoid having teams crafting their own nginx.conf files, while giving them an easy-to-setup yet configurable image.

The Nginx configuration is done by templating the jinja2 template with a custom configuration. The configuration context is created by providing one or more YAML configuration files, and mounting them to ${NGINX_ENVSUBST_TEMPLATE_DIR}/conf.d/. The configuration files will be loaded in alphabetical order and merged together. Note that as it is jinja2 based, we also template the configuration itself, meaning we can have variables in configuration files referring to itself.

The default configuration contains the full spec with examples, commenting what is not necessary as a default.

An example configuration file could be:

config:
  log_level: debug
  source_env:
    - name: NAMESPACE
      default: some-namespace
    - name: BACKEND_URL
      default: http://some-backend.some-namespace.svc
  disable_default_locations: true
  before_snippets:
    - |
      location ^~ /{{ env.NAMESPACE }}/static {
        alias {{ config.root }}/static/$1;
        expires {{ config.static_file_cache_expiration }};
        add_header Pragma public;
        add_header Cache-Control "public";
        try_files $uri $uri/ =404;
      }
  locations:
    - location: ~* ^/{{ env.NAMESPACE }}/api(.*)$
      proxy_pass: "{{ env.BACKEND_URL }}/api$1$is_args$args"
      proxy_read_timeout: 120s
    - location: ~* ^/{{ env.NAMESPACE }}/another/api(.*)$
      proxy_pass: "{{ env.BACKEND_URL }}/api$1$is_args$args"
      proxy_buffering: 'on'

Most notably, through proper configuration, this image allows:

  • Using references in the configuration to other variables in the configuration (recursive, has some limitations) using {{ config.<variable> }}

before_snippets:
- |
    location ^~ /static {
    alias {{ config.root }}/static/$1;
    expires {{ config.static_file_cache_expiration }};
    add_header Pragma public;
    add_header Cache-Control "public";
    try_files $uri $uri/ =404;
    }
  • Capability of “importing” environment variables and giving them a default value

source_env:
  - name: NAMESPACE
    default: some-namespace
  - name: BACKEND_URL
    default: http://some-backend.some-namespace.svc
locations:
  - location: ~* ^/{{ env.NAMESPACE }}/api(.*)$
    proxy_pass: "{{ env.BACKEND_URL }}/api$1$is_args$args"
  • Any number of locations with configurable location expression, proxy_pass and other proxy configurations

  • Add snippets to Nginx configuration before the defaults and locations with before_snippets and after with after_snippets

If you want to test what the nginx configuration would look like, you can:

mkdir -p /tmp/ska-webserver-configs
cp images/ska-webserver/resources/conf.yml /tmp/ska-webserver-configs/0-conf.yml

# Create any number of configuration files
vi /tmp/ska-webserver-configs/[...].yml

python3 images/ska-webserver/resources/generate.py -i images/ska-webserver/resources/nginx.conf.j2 -c /tmp/ska-webserver-configs -o /tmp/nginx.conf
cat /tmp/nginx.conf

Other dynamic procedures

As this image installs Nginx from native sources, we also carried the docker-entrypoint capabilities the official Nginx images have. If you wish to do other dynamic procedures that do not involve generating the nginx.conf file, you can mount any bash script (note the alphabetical order of execution) to /docker-entrypoint.d.

A common use-case for this is to replace in the static HTML, JS and CSS files, paths to other resources by including the namespace of the deployment as a prefix.

CUDA Images

These images are not built using ska-base; they are based directly on NVIDIA CUDA images. The CUDA runtime and build images are maintained manually because their supported Ubuntu/CUDA combinations are determined by NVIDIA.

Deprecations and removals

  • The ska-build image was removed in favor of having language/platform specific base images

Beta images

All images based on the Ubuntu 24.04 and Ubuntu 26.04 base images are currently in beta and won’t be supported by the System Team. We do welcome any bug reports on their usage, but they are not guaranteed to be stable or production-ready.

Development

This repository uses uv for dependency management and Ruff for linting and formatting. (These replace the previous Poetry + black/isort/flake8/pylint toolchain.)

Install uv (see the uv installation docs), then set up the environment from the lockfile:

uv sync --all-groups

Lint and format the Python sources (Ruff configuration lives in pyproject.toml):

uv run ruff format .      # apply formatting
uv run ruff check .       # lint
make python-lint          # format --check + lint (the CI entrypoint)

Build the documentation locally:

uv sync --group docs
uv run sphinx-build -b html docs/src docs/_build/html