Skip to content

Docker implementation#1219

Open
Jrice1317 wants to merge 60 commits into
conda:mainfrom
Jrice1317:docker-implementation
Open

Docker implementation#1219
Jrice1317 wants to merge 60 commits into
conda:mainfrom
Jrice1317:docker-implementation

Conversation

@Jrice1317
Copy link
Copy Markdown
Contributor

@Jrice1317 Jrice1317 commented Apr 22, 2026

Description

Summary

This PR adds Docker output support to constructor via two new opt-in flows:

Flow 1: Dockerfile generation (installer_type: docker)

Generates a multi-stage Dockerfile alongside the .sh installer and stages both
into a named output directory. No Docker CLI required — the output can be used
directly or customized before building.

Output structure:

{output_dir}/<name>-<version>-<platform>/
Dockerfile
<name>-<version>-<platform>.sh

Usage:

installer_type: docker
docker_base_image: "debian:13.4-slim@sha256:..."

The generated Dockerfile uses a two-stage build: Stage 1 runs the .sh installer
in batch mode and cleans up build artifacts (.a files, __pycache__, optionally
pkgs/). Stage 2 copies the finished environment into a clean final image and
runs conda init.

Flow 2: Portable image tarball (docker_image: tar)

Builds a Docker image from the generated Dockerfile using docker buildx and
exports it as a portable .tar file via docker save. Requires the Docker CLI
to be installed on the host. The target platform must be Linux.

Output:

{output_dir}/<name>-<version>-<platform>-docker.tar

Usage:

docker_image: tar
docker_base_image: "debian:13.4-slim@sha256:..."

New schema keys

Key Description
docker_base_image Required for all Docker features. Base image reference.
docker_image Set to tar to build and export a portable image tarball. Can be used standalone without installer_type: docker.
docker_tag Optional tag for the built image. Defaults to name:version.
docker_labels Additional OCI labels. title and version are set automatically.

Platform support

  • Target platform must be linux-* for all Docker features.
  • No host-level restriction for Dockerfile-only output.
  • docker_image: tar additionally requires the Docker CLI on the host.

Notes

  • docker_image is designed to be extendable. Additional output formats
    (gz, zst) are defined in the schema but not yet implemented.
  • The .sh installer is always built first and reused as the Docker build
    context, keeping the two outputs consistent.
  • The feature is intended to support Dockerfile generation from any host when targeting Linux, but CI currently validates the Docker flows on Linux only because we do not yet have a cross-platform --platform linux-* test matrix for macOS/Windows hosts.

Changes

New:

  • constructor/docker_build.py: Handles Docker output by rendering template and optionally building portable image
  • constructor/dockerfile_template.tmpl: Template used to generate Dockerfile
  • examples/dockerfile/construct.yaml: Example for installer_type: docker flow
  • examples/docker_image/construct.yaml: Example for docker_image: tar flow

Updated:

  • constructor/_schema.py: Adds docker to installer_type and adds docker_base_image, docker_tag, docker_labels
  • constructor/main.py: Adds docker to installer types
  • tests/test_examples.py: Adds test_dockerfile_generation and test_docker_image_build to cover both flows

Checklist - did you ...

  • Add a file to the news directory (using the template) for the next release's release notes?
  • Add / update necessary tests?
  • Add / update outdated documentation?

@Jrice1317 Jrice1317 requested a review from a team as a code owner April 22, 2026 19:32
@github-project-automation github-project-automation Bot moved this to 🆕 New in 🔎 Review Apr 22, 2026
@conda-bot conda-bot added the cla-signed [bot] added once the contributor has signed the CLA label Apr 22, 2026
@Jrice1317 Jrice1317 marked this pull request as draft April 22, 2026 19:45
@Jrice1317 Jrice1317 changed the title Docker implementation Docker implementation [skip windows] May 6, 2026
@Jrice1317 Jrice1317 force-pushed the docker-implementation branch from 9fa7db8 to 9f20cbe Compare May 6, 2026 16:10
@Jrice1317 Jrice1317 marked this pull request as ready for review May 19, 2026 13:32
Comment thread constructor/docker_build.py Outdated
Comment thread constructor/data/construct.schema.json Outdated
}
],
"default": null,
"description": "If set, builds a docker image using the Dockerfile generated by constructor and saves it as a portable tarball either uncompressed or compressed. ``<name>-<version>-<platform>-<arch>-docker.tar`` will be created in the output docker directory.",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the format <name>-<version>-<platform>-<arch> true?
From what I could see in the code it looks like:

tag = info.get("docker_tag", f"{info['name'].lower()}:{info['version']}")

Comment thread constructor/_schema.py
The labels `org.opencontainers.image.title` and `org.opencontainers.image.version`
are set automatically from `name` and `version`.
"""
docker_image: Literal["tar", "gz", "zst"] | None = None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add explicit NotImplementedError for those values that are not yet implemented? I see in the PR it says:

(gz, zst) are defined in the schema but not yet implemented.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!

@Jrice1317 Jrice1317 requested a review from lrandersson May 20, 2026 22:16
lrandersson
lrandersson previously approved these changes May 21, 2026
Copy link
Copy Markdown
Contributor

@lrandersson lrandersson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well done Jaida! In the future I think we should split these large PRs into smaller separate ones (to simplify review) If the comments are addressed from Marco I approve!

@github-project-automation github-project-automation Bot moved this from 🆕 New to ✅ Approved in 🔎 Review May 21, 2026
Copy link
Copy Markdown
Contributor

@marcoesters marcoesters left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good changes - a few more issues to address.

Comment thread constructor/_schema.py
The labels `org.opencontainers.image.title` and `org.opencontainers.image.version`
are set automatically from `name` and `version`.
"""
docker_image: Literal["tar", "gz", "zst"] | None = None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't allow inputs we don't support. It's okay to only allow "tar" for now. Also, should we call it docker_image_format? I feel like that's more descriptive.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, and we don't allow inputs we don't support and"tar" is the only one allowed because we reject anything outside of tar with a NotImplementedError. gz and zst are left intentionally as placeholders to document the planned formats so the research isn't lost and users understand the direction. The other compressions including bz2 may be used as well, but they would need to be used manually. Eventually, constructor could handle this, but I think it's better suited for a different PR. Is this wording better?

    docker_image: Literal["tar", "gz", "zst"] | None = None
    """
    If set, builds a docker image using the Dockerfile generated by constructor and saves it as a portable file. Currently, only ``tar``(uncompressed) is supported. Additional formats (``gz``, ``zst``) are planned for future releases.
``<name>-<version>-<platform>-docker.<extension>`` will be created in the output directory.
    """

As far as the docker_image_format, it is more descriptive, but I think docker_image communicates that the user is opting into having the image built, which feels right — the name is the feature toggle, not just a format selector. docker_image_format reads more like a sub-option for something already enabled.

Comment thread constructor/_schema.py
docker_image: Literal["tar", "gz", "zst"] | None = None
"""
If set, builds a docker image using the Dockerfile generated by constructor and saves it as a portable tarball either uncompressed or compressed.
``<name>-<version>-<platform>-<arch>-docker.tar`` will be created in the output docker directory.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
``<name>-<version>-<platform>-<arch>-docker.tar`` will be created in the output docker directory.
``<name>-<version>-<platform>-<arch>-docker.<extension>`` will be created in the output docker directory.

Comment thread constructor/docker_build.py Outdated
name=info["name"],
version=info["version"],
labels=info.get("docker_labels", {}),
init_cmd="$PREFIX/bin/mamba shell" if has_mamba else "$PREFIX/bin/python -m conda",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still assuming that either mamba or conda must be in the base environment. This is not necessarily true. We need to implement the same has_conda check as for mamba. Also note that mamba v1 uses a different init command than mamba v2.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Moving this to the template and adding a v1 fallback

Comment thread constructor/_schema.py Outdated
use the `default_prefix_all_users` key. If not provided, the default prefix
is `%USERPROFILE%\\<NAME>`. Environment variables will be expanded at
install time.
install time. If creating a Docker output, the default is `/opt/<NAME>` and can be overridden during the Docker build process.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should mention which variable to use to override it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok

Comment thread constructor/docker_build.py
Comment thread constructor/main.py Outdated
Comment on lines +240 to +242
"Install Docker Buildx to proceed, or "
"use `installer_type: docker` in construct.yaml to "
"generate the Dockerfile without building the image."
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presumably, this is already the case. The correct remedy is to remove docker_image.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is nearly identical to dockerfile. You could use Jinja and an environment variable to consolidate the two.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overlap is intentional. One solely tests generating the Dockerfile, while the other tests building the docker image without needing the installer_type: docker set. If I use a single file, then it would make the tests a little chaotic and harder to read, but if you have something specific in mind to handle both options, while maintaining clarity in the tests, then I'm all ears.

Comment thread tests/test_examples.py
Comment on lines +1572 to +1574
yaml = YAML()
with open(input_path / "construct.yaml") as f:
config = yaml.load(f)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

config only appears to be used to read values from construct.yaml here. I think for testing, it's okay to hard-code what we expect to be in the final Dockerfile. That would also allow you to use Jinja to consolidate the test files.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather keep reading from construct.yaml. If the example is updated without updating the test, reading from config keeps them in sync automatically.

Comment thread tests/test_examples.py
Comment on lines +1580 to +1581
if installer.suffix == ".sh":
installer_stem = installer.stem
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be clear, this does not mean we have an SH installer in output_path directly, right? The SH installer only exists in output_path / "installer" and nowhere else? Can we test this to ensure that the SH installer isn't duplicated anywhere else?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok

Comment thread tests/test_examples.py Outdated
assert f"FROM {config['docker_base_image']}" in dockerfile_text

for key, value in config.get("docker_labels", {}).items():
assert f'{key}="{value}"' in dockerfile_text
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert f'{key}="{value}"' in dockerfile_text
assert f'LABEL {key}="{value}"' in dockerfile_text

Right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is more precise.

COPY {{ installer_filename }} /tmp/installer.sh

RUN sh /tmp/installer.sh -b -p "${PREFIX}" && \
rm -f "${PREFIX}/uninstall.sh" && \
Copy link
Copy Markdown
Contributor

@marcoesters marcoesters May 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
rm -f "${PREFIX}/uninstall.sh" && \

uninstall.sh is not produced by constructor. This is an extra file that should just not be included in the first place.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uninstall.sh is written to the prefix by the installer script when it runs. The rm -f handles it cleanly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed [bot] added once the contributor has signed the CLA

Projects

Status: ✅ Approved

Development

Successfully merging this pull request may close these issues.

4 participants