Container Images
Reference documentation for the container image build system (CC-0006). This covers the Dockerfile hierarchy, base image contents, release configuration file formats, named build context patterns, constraint override tooling, and local build instructions.
Dockerfile Hierarchy
Container images follow a three-layer hierarchy. Each layer builds on the previous one, separating concerns between runtime base, build tooling, and service-specific code:
ubuntu:noble
├── python-base Runtime base: Python 3.12, system libs, openstack user
│ ├── venv-builder Build stage: compilers, uv, virtualenv with common packages
│ │ └── keystone Stage 1 (build): install Keystone into virtualenv
│ └── keystone Stage 2 (runtime): copy virtualenv, add runtime apt packagesThe venv-builder image is used only as a build stage — it never runs in production. Service images (e.g., keystone) use a multi-stage build: stage 1 extends venv-builder to install the service, then stage 2 extends python-base and copies only the virtualenv from stage 1. This ensures the final image contains no build tools.
Base Images
python-base
Location: images/python-base/Dockerfile
The foundational runtime image for all OpenStack service containers.
| Property | Value |
|---|---|
| Base image | ubuntu:noble (Ubuntu 24.04 LTS) |
| Python | 3.12 (from Ubuntu Noble package repository) |
| User | openstack (UID 42424, GID 42424, shell /usr/sbin/nologin) |
| Home directory | /var/lib/openstack |
Environment variables:
| Variable | Value | Purpose |
|---|---|---|
PATH | /var/lib/openstack/bin:$PATH | Ensures virtualenv binaries take precedence |
LANG | C.UTF-8 | Consistent locale for Python string handling |
Runtime packages:
| Package | Purpose |
|---|---|
ca-certificates | TLS certificate verification |
netbase | /etc/protocols and /etc/services for network operations |
python3 | Python 3.12 runtime |
sudo | Privilege escalation for entrypoint scripts |
tzdata | Timezone data for datetime operations |
User convention: All service images share a single openstack user (UID/GID 42424) rather than creating per-service users. This is a deliberate deviation from the architecture document — see Design Deviations for rationale.
OCI labels (CC-0031): The Dockerfile includes static LABEL instructions for baseline OCI Image Spec annotations (title, description, licenses, vendor). These are always present on locally-built images. In CI, docker/metadata-action supplements these with dynamic labels (created, revision, source, url, version) — see Build Images Workflow — OCI Annotations.
venv-builder
Location: images/venv-builder/Dockerfile
Build-stage image that extends python-base with compilation tools and a prepared Python virtualenv. This image is never deployed — it exists only as a FROM target for multi-stage service builds.
| Property | Value |
|---|---|
| Base image | python-base (local) |
| Package manager | uv 0.6.3 (copied from ghcr.io/astral-sh/uv:0.6.3) |
| Virtualenv path | /var/lib/openstack |
Build-time packages:
| Package | Purpose |
|---|---|
build-essential | C compiler and make (for building Python C extensions) |
git | Fetching Python packages from git repositories |
libffi-dev | cffi/cryptography compilation |
libpq-dev | psycopg2 compilation (PostgreSQL client) |
libssl-dev | cryptography/pyOpenSSL compilation |
python3-dev | Python headers for C extensions |
python3-venv | venv module for virtualenv creation |
Pre-installed common packages:
The virtualenv includes four packages shared by all OpenStack services:
| Package | Purpose |
|---|---|
cryptography | TLS, token encryption, Fernet keys |
pymysql | MySQL/MariaDB database driver |
python-memcached | Memcached client for caching |
uwsgi | WSGI application server |
These packages are installed without the --constraint flag. The venv-builder image is release-independent — version constraints are release-specific and applied only in service Dockerfiles when installing the actual service.
OCI labels (CC-0031): Same static LABEL pattern as python-base — title, description, licenses, and vendor are embedded in the Dockerfile for local build visibility.
Service Images
keystone
Location: images/keystone/Dockerfile
The Keystone identity service image uses a two-stage build:
Stage 1 (build) — extends venv-builder:
- Declares
ARG PIP_EXTRASandARG PIP_PACKAGESfor build-time injection of extras and additional packages fromextra-packages.yaml(passed by the CI workflow) - Mounts
upper-constraints.txtand the Keystone source tree via named build contexts - Installs Keystone with extras into the virtualenv using
uv pip install --constraint
Stage 2 (runtime) — extends python-base:
- Declares
ARG EXTRA_APT_PACKAGESfor build-time injection of runtime system packages fromextra-packages.yaml(passed by the CI workflow) - Copies
/var/lib/openstackfrom the build stage usingCOPY --from=build --link(the--linkflag enables parallel layer extraction and deduplication) - Installs runtime system packages via
apt-get install ${EXTRA_APT_PACKAGES} - Sets
USER openstackfor non-root execution
Runtime packages:
| Package | Purpose |
|---|---|
libapache2-mod-wsgi-py3 | Apache WSGI module for serving Keystone |
libldap2 | LDAP client library (python-ldap runtime dependency) |
libsasl2-2 | SASL authentication library (LDAP SASL bind support) |
libxml2 | XML parsing library (lxml runtime dependency) |
Final image properties:
- Runs as
openstackuser (UID 42424, GID 42424) - Contains no build tools (
gcc,python3-dev,build-essential,uvare absent) - Virtualenv at
/var/lib/openstackwith all Keystone dependencies keystone-manageCLI available viaPATH
OCI labels (CC-0031): The LABEL instruction is placed in Stage 2 (runtime) before the USER instruction. Labels added in Stage 1 (build) are discarded by Docker's multi-stage build process — only the runtime stage labels appear on the final image. In CI, docker/metadata-action overrides org.opencontainers.image.version with the upstream OpenStack release version from source-refs.yaml via a type=raw tag strategy.
Named Build Contexts
Service Dockerfiles use Docker's named build context feature (--build-context) to inject release-specific files without embedding them in the Dockerfile or using COPY from the build directory. This keeps Dockerfiles release-independent.
The Keystone build requires two named build contexts:
| Context name | Contents | Mounted as |
|---|---|---|
upper-constraints | Release directory containing upper-constraints.txt | /tmp/upper-constraints.txt |
keystone | Keystone source tree (git checkout at the version from source-refs.yaml) | /tmp/keystone |
These are passed to docker build via --build-context flags:
docker build images/keystone \
--build-context keystone=src/keystone \
--build-context upper-constraints=releases/2025.2/Inside the Dockerfile, named build contexts are consumed via --mount=type=bind,from=. Extras are injected via ARG PIP_EXTRAS (comma-separated, e.g. ldap,oauth1) which the CI workflow reads from extra-packages.yaml:
ARG PIP_EXTRAS=""
ARG PIP_PACKAGES=""
RUN --mount=type=bind,from=upper-constraints,source=upper-constraints.txt,target=/tmp/upper-constraints.txt \
--mount=type=bind,from=keystone,target=/tmp/keystone \
PKG="/tmp/keystone" && \
if [ -n "$PIP_EXTRAS" ]; then PKG="${PKG}[${PIP_EXTRAS}]"; fi && \
uv pip install --prefix /var/lib/openstack \
--constraint /tmp/upper-constraints.txt \
"$PKG" $PIP_PACKAGESThe from=upper-constraints directive tells Docker to resolve the file from the named build context rather than the Dockerfile's primary build context. The source= parameter selects a specific file within that context.
Release Configuration
All release-specific configuration lives under releases/<release>/ (e.g., releases/2025.2/). These files are the single source of truth for what gets built. Adding a new service or updating a version requires editing only these files — not Dockerfiles.
source-refs.yaml
Location: releases/<release>/source-refs.yaml
Maps each OpenStack component to a git ref (tag, branch, or commit SHA) specifying the version to build.
Format:
# SPDX-FileCopyrightText: Copyright 2026 SAP SE or an SAP affiliate company
#
# SPDX-License-Identifier: Apache-2.0
keystone: "28.0.0"Each key is a service name matching the Dockerfile directory under images/. Values are quoted strings representing git refs — typically release tags (e.g., "28.0.0").
To add a new service, add a single line: <service>: "<git-ref>".
upper-constraints.txt
Location: releases/<release>/upper-constraints.txt
Contains pinned Python dependency versions from the OpenStack requirements repository (stable/<release> branch). This file is committed as-is from the upstream repository to enable Renovate tracking and git diff for constraint changes.
Format:
cryptography===44.0.0
oslo.limit===2.8.0
keystonemiddleware===10.9.0Each line pins a single package using the === (arbitrary equality) operator. This is the standard format used by OpenStack's global requirements process.
Source: https://raw.githubusercontent.com/openstack/requirements/stable/<release>/upper-constraints.txt
extra-packages.yaml
Location: releases/<release>/extra-packages.yaml
Defines per-service Python extras and runtime system packages that are not part of the core OpenStack package.
Format:
# SPDX-FileCopyrightText: Copyright 2026 SAP SE or an SAP affiliate company
#
# SPDX-License-Identifier: Apache-2.0
keystone:
pip_extras:
- ldap
- oauth1
pip_packages: []
apt_packages:
- libapache2-mod-wsgi-py3
- libldap2
- libsasl2-2
- libxml2| Key | Purpose |
|---|---|
<service>.pip_extras | Bare Python extra names combined with the service name to form install arguments (e.g. keystone[ldap,oauth1]). Passed as the PIP_EXTRAS build arg. |
<service>.pip_packages | Additional pip packages to install alongside the service (space-separated in the build arg PIP_PACKAGES). Use an empty list ([]) when none are needed. |
<service>.apt_packages | Runtime system packages installed via apt in the final image. Passed as the EXTRA_APT_PACKAGES build arg. |
To add packages for a new service, add a new top-level key matching the service name with both pip_extras and apt_packages lists.
Constraint Overrides
The constraint override system allows selective modification of individual package pins in upper-constraints.txt without replacing the entire file. This is useful for applying security fixes or version bumps for individual packages.
Override Format
Override files are placed at overrides/<release>/constraints.txt. Each line is one of three types:
| Syntax | Action | Example |
|---|---|---|
package===version | Replace the existing pin for package | cryptography===44.0.1 |
-package | Remove package from constraints entirely | -oslo.messaging |
# comment or blank | Skipped (no action) | # Security fix for CVE-2025-1234 |
Example override file (overrides/2025.2/constraints.txt):
# Security fix: bump cryptography for CVE-2025-1234
cryptography===44.0.1
# Remove oslo.messaging pin to allow newer version
-oslo.messagingScript Usage
Location: scripts/apply-constraint-overrides.sh
# Apply overrides for the 2025.2 release
./scripts/apply-constraint-overrides.sh 2025.2Behavior:
| Condition | Result |
|---|---|
overrides/<release>/constraints.txt exists | Each line is processed: replacements via sed, removals via sed -d |
overrides/<release>/constraints.txt does not exist | Script exits with code 0, no changes made (idempotent) |
The script reads releases/<release>/upper-constraints.txt relative to the current working directory (must be invoked from the repository root) and modifies it in-place. It uses GNU sed -i for modifications (default on Ubuntu/CI runners — BSD sed is not supported).
Arguments:
| Argument | Required | Description |
|---|---|---|
<release> | Yes | Release identifier (e.g., 2025.2), used to locate overrides/<release>/constraints.txt |
Local Build Instructions
Build the complete image chain locally for development and verification:
Step 1: Build base images
# Build python-base (tag must match FROM python-base in downstream Dockerfiles)
docker build images/python-base -t python-base
# Build venv-builder (tag must match FROM venv-builder in keystone Stage 1)
docker build images/venv-builder -t venv-builderThe tag names (python-base, venv-builder) must match the FROM directives in downstream Dockerfiles. Docker resolves FROM python-base to the local image.
To also apply canonical registry tags, add a second -t flag:
docker build images/python-base -t python-base -t c5c3/python-base:3.12-noble
docker build images/venv-builder -t venv-builder -t c5c3/venv-builder:3.12-nobleStep 2: Clone the service source
git clone --branch 28.0.0 --depth 1 \
https://github.com/openstack/keystone.git src/keystoneThe branch/tag must match the version specified in releases/2025.2/source-refs.yaml.
Step 3: Build the service image
Extras are read from extra-packages.yaml and passed as --build-arg:
docker build images/keystone \
-t c5c3/keystone:28.0.0 \
--build-arg PIP_EXTRAS=ldap,oauth1 \
--build-arg "EXTRA_APT_PACKAGES=libapache2-mod-wsgi-py3 libldap2 libsasl2-2 libxml2" \
--build-context keystone=src/keystone \
--build-context upper-constraints=releases/2025.2/Step 4: Verify the image
# Verify Keystone CLI is functional
docker run --rm c5c3/keystone:28.0.0 keystone-manage --version
# Verify non-root execution
docker run --rm c5c3/keystone:28.0.0 whoami
# Expected output: openstack
# Verify no build tools in final image
docker run --rm c5c3/keystone:28.0.0 which gcc \
&& echo "FAIL: gcc found in image" \
|| echo "PASS: gcc not found"Design Deviations
The implementation deviates from the architecture document (architecture/docs/08-container-images/01-build-pipeline.md) in one area, documented with # DEVIATION comments in the affected Dockerfiles:
Generic openstack user instead of per-service users:
The architecture document's Keystone Dockerfile example creates a per-service user (e.g., groupadd keystone / useradd keystone). The implementation uses a single generic openstack user (UID/GID 42424) defined in python-base and shared by all service images. This reduces complexity and image layers — each service image inherits the user via USER openstack without needing its own user creation step.
The # DEVIATION comment appears in both images/python-base/Dockerfile (where the user is created) and images/keystone/Dockerfile (where it is used instead of a per-service user).