Base images#
Every Juice environment is a container image built in two layers:
a base image — the bottom layer, baking in a pinned
orangeqs-juice-coreand stable dependencies;a user layer — built by the orchestrator on top of the base image at install time, adding the packages declared in the lab repository and its extensions.
It is possible to swap base images. Point it at a pre-built Juice image and the stack is already baked in, so only the thin user layer is built locally.
This page covers where the base image is configured, how its tag is derived from the installed Juice version, how it is built and published, and how it is pulled at install time.
Configuration#
The base image is the base_image field of UvEnvironmentSettings. Every environment in the orchestration.toml configuration has its own:
environments.default— the fallback for anything without an explicit environment;each service’s
services.<name>.environment;the single-user server’s
jupyterhub.singleuser.environment.
Services and the single-user server that do not set their own environment receive a copy of environments.default (applied by the _apply_default_environment validator). Note that an explicit per-service or single-user environment does not inherit from the default.
The field defaults to the untagged pre-built Juice image, so an installation uses pre-baked dependencies out of the box and the tag tracks the installed Juice version (see below):
base_image: str = "registry.gitlab.com/orangeqs/juice/ci"
Setting it to a plain OS image (e.g. quay.io/almalinuxorg/almalinux:10.1) instead opts out of the pre-baked stack, building every dependency in the local user layer.
Deriving the tag from the Juice version#
There is no migration command and no latest tag. A base image reference’s tag is derived automatically, in memory, every time the orchestration configuration is validated — by the _derive_base_image_tag model validator on UvEnvironmentSettings.
The rule is tag-presence-is-intent:
A reference that already carries a tag or digest —
almalinux:10.1,…/ci:26.27.0,…@sha256:…— is a deliberate pin and is left untouched.A reference with no tag —
registry.gitlab.com/orangeqs/juice/ci— has the tag matching the runningorangeqs.juice.__version__appended at load time.
Only the final path segment is inspected, so a registry port such as host:5000/image is not mistaken for a tag.
A Juice version is only guaranteed compatible with the base image published under its matching tag, which is why latest is never assumed. This same rule is how root OS images and hand-pinned custom images opt out of derivation: they already carry a tag.
The net effect: an administrator points an environment at an untagged Juice image once, and the effective tag then tracks the installed Juice version on every upgrade, with no further config edits.
Building and publishing#
A base image is built from the same environment.Containerfile.j2 template (in orangeqs.juice.orchestration.templates) used for ordinary environments. The build_base_image_cmd() command — juice build — drives it:
$ juice build --tag registry.gitlab.com/orangeqs/juice/ci:26.27.0 --version 26.27.0
Built registry.gitlab.com/orangeqs/juice/ci:26.27.0
Option |
Meaning |
|---|---|
|
Tag for the built image. Required unless |
|
Exact |
|
Directory for the rendered build files (a temporary directory by default). |
|
Additional Python package specifiers to bake in (repeatable). |
|
Only render the build context; do not invoke podman. Requires |
Under the hood, render_base_image_files() reads the single-user environment from the system config and renders two files into the build context:
a
pyproject.tomlpinningorangeqs-juice-core[runtime]==<version>plus any--extra-packages, withrequires-pythontaken from the single-user environment’spython_version. It deliberately bypasses the normal environment path so that juice core is installed from the package index at the pinned version, not from a local source.a
Containerfilerendered fromenvironment.Containerfile.j2, using the single-user environment’sbase_imageandsystem_packages.
Without --render-only, juice build then runs podman build on that Containerfile and tags the result. The resulting image is the configured OS base, with system packages installed and Juice core resolved and baked at the pinned version.
--render-only writes only the build context and skips podman, so an external, unprivileged builder (such as buildah or Kaniko on a CI runner) can perform the actual build. This is what CI does.
Pulling at install time#
juice install pulls every base image referenced in the configuration before building any user layer.
collect_base_images() returns the set of unique base_image values across all environments. By this point the references are already tag-derived, so an untagged Juice image has become …/ci:<juice-version>. pull_images() then pulls them in a single parallel podman pull.
Pulling up front means a base image shared by several environments is fetched once, and the per-environment builds that follow add only the user layer on top.
flowchart TD
A["juice install"] --> B["collect_base_images<br/>unique base images across environments"]
B --> C["pull_images<br/>parallel podman pull"]
C --> D["Build user layer per environment<br/>on top of the base image"]