Skip to content

Commit e840466

Browse files
committed
feat: replace container base images with pixi-managed environments
Move container builds from the multi-repo base image approach (micromamba + conda environment.yml + pip install --no-deps) to self-contained multi-stage Dockerfiles using pixi environments from the committed lock file. This ensures reproducible builds with exact version parity between development and production.
1 parent 84c7651 commit e840466

13 files changed

Lines changed: 37435 additions & 135 deletions

File tree

.github/workflows/deployment.yml

Lines changed: 19 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ jobs:
9797

9898
docker:
9999
needs: deploy-pypi
100-
timeout-minutes: 30
100+
timeout-minutes: 30
101101
runs-on: ubuntu-latest
102102
steps:
103103
- name: Checkout
@@ -112,58 +112,42 @@ jobs:
112112
registry: ghcr.io
113113
username: ${{ github.actor }}
114114
password: ${{ secrets.GITHUB_TOKEN }}
115-
- name: Download diracx wheels
116-
uses: actions/download-artifact@v7
117-
with:
118-
name: diracx-whl
119-
- name: "Find wheels"
120-
id: find_wheel
121-
run: |
122-
# We need to copy them there to be able to access them in the RUN --mount
123-
cp diracx*.whl containers/client/
124-
cp diracx*.whl containers/services/
125-
for wheel_fn in *.whl; do
126-
pkg_name=$(basename "${wheel_fn}" | cut -d '-' -f 1)
127-
echo "${pkg_name}-wheel-name=$(ls "${pkg_name}"-*.whl)" >> $GITHUB_OUTPUT
128-
done
129115

130-
- name: Build and push client (release)
116+
- name: Build and push services (release)
131117
uses: docker/build-push-action@v6
132118
if: ${{ needs.deploy-pypi.outputs.create-release == 'true' }}
133119
with:
134-
context: containers/client/
135-
push: ${{ needs.deploy-pypi.outputs.create-release == 'true' }}
136-
tags: "ghcr.io/diracgrid/diracx/client:${{ needs.deploy-pypi.outputs.new-version }}"
120+
context: .
121+
file: containers/services/Dockerfile
122+
push: true
123+
tags: "ghcr.io/diracgrid/diracx/services:${{ needs.deploy-pypi.outputs.new-version }}"
137124
platforms: linux/amd64,linux/arm64
138-
build-args: EXTRA_PACKAGES_TO_INSTALL=DIRACCommon~=9.0.0
139-
- name: Build and push services (release)
125+
- name: Build and push client (release)
140126
uses: docker/build-push-action@v6
141127
if: ${{ needs.deploy-pypi.outputs.create-release == 'true' }}
142128
with:
143-
context: containers/services/
144-
push: ${{ needs.deploy-pypi.outputs.create-release == 'true' }}
145-
tags: "ghcr.io/diracgrid/diracx/services:${{ needs.deploy-pypi.outputs.new-version }}"
129+
context: .
130+
file: containers/client/Dockerfile
131+
push: true
132+
tags: "ghcr.io/diracgrid/diracx/client:${{ needs.deploy-pypi.outputs.new-version }}"
146133
platforms: linux/amd64,linux/arm64
147-
build-args: EXTRA_PACKAGES_TO_INSTALL=DIRACCommon~=9.0.0
148134

149-
- name: Build and push client (dev)
135+
- name: Build and push services (dev)
150136
uses: docker/build-push-action@v6
151137
with:
152-
context: containers/client/
138+
context: .
139+
file: containers/services/Dockerfile
153140
push: ${{ github.event_name != 'pull_request' && github.repository == 'DIRACGrid/diracx' && github.ref_name == 'main' }}
154-
tags: ghcr.io/diracgrid/diracx/client:dev
141+
tags: ghcr.io/diracgrid/diracx/services:dev
155142
platforms: linux/amd64,linux/arm64
156-
build-args: |
157-
EXTRA_PACKAGES_TO_INSTALL=git+https://github.com/DIRACGrid/DIRAC.git@integration#egg=diraccommon\&subdirectory=dirac-common
158-
- name: Build and push services (dev)
143+
- name: Build and push client (dev)
159144
uses: docker/build-push-action@v6
160145
with:
161-
context: containers/services/
146+
context: .
147+
file: containers/client/Dockerfile
162148
push: ${{ github.event_name != 'pull_request' && github.repository == 'DIRACGrid/diracx' && github.ref_name == 'main' }}
163-
tags: ghcr.io/diracgrid/diracx/services:dev
149+
tags: ghcr.io/diracgrid/diracx/client:dev
164150
platforms: linux/amd64,linux/arm64
165-
build-args: |
166-
EXTRA_PACKAGES_TO_INSTALL=git+https://github.com/DIRACGrid/DIRAC.git@integration#egg=diraccommon\&subdirectory=dirac-common
167151

168152
update-charts:
169153
name: Update Helm charts

.github/workflows/main.yml

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -121,38 +121,17 @@ jobs:
121121
with:
122122
cache: false
123123
environments: ${{ matrix.extension == 'diracx' && 'default' || 'default-gubbins' }}
124-
- name: Build gubbins wheels
125-
if: ${{ matrix.extension == 'gubbins' }}
126-
run: |
127-
for pkg_dir in $PWD/diracx-*; do
128-
echo "Building $pkg_dir"
129-
pixi exec python-build --outdir $PWD/extensions/containers/services/ $pkg_dir
130-
done
131-
# Also build the diracx metapackage
132-
pixi exec python-build --outdir $PWD/extensions/containers/services/ .
133-
# And build the gubbins package
134-
for pkg_dir in $PWD/extensions/gubbins/gubbins-*; do
135-
# Skip the testing package
136-
if [[ "${pkg_dir}" =~ .*testing.* ]];
137-
then
138-
echo "Do not build ${pkg_dir}";
139-
continue;
140-
fi
141-
echo "Building $pkg_dir"
142-
pixi exec python-build --outdir $PWD/extensions/containers/services/ $pkg_dir
143-
done
144124
- name: Set up Docker Buildx
145125
if: ${{ matrix.extension == 'gubbins' }}
146126
uses: docker/setup-buildx-action@v3
147127
- name: Build container for gubbins
148128
if: ${{ matrix.extension == 'gubbins' }}
149129
uses: docker/build-push-action@v6
150130
with:
151-
context: extensions/containers/services
131+
context: extensions/gubbins
132+
file: extensions/containers/services/Dockerfile
152133
tags: gubbins/services:dev
153134
outputs: type=docker,dest=/tmp/gubbins_services_image.tar
154-
build-args: |
155-
EXTENSION_CUSTOM_SOURCES_TO_INSTALL=/bindmount/gubbins_db*.whl,/bindmount/gubbins_logic*.whl,/bindmount/gubbins_routers*.whl,/bindmount/gubbins_client*.whl
156135
- name: Load image
157136
if: ${{ matrix.extension == 'gubbins' }}
158137
run: |

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,6 @@ docs/source/_build
9797

9898
# pixi environments
9999
.pixi
100-
pixi.lock
101100
*.egg-info
102101
docs/templates/_builtin_markdown.jinja
103102

.pre-commit-config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ repos:
2828
- id: check-yaml
2929
args: ["--unsafe"]
3030
- id: check-added-large-files
31+
exclude: pixi\.lock$
3132

3233
- repo: https://github.com/astral-sh/ruff-pre-commit
3334
rev: "v0.15.2"
@@ -66,6 +67,7 @@ repos:
6667
hooks:
6768
- id: codespell
6869
args: ["-w"]
70+
exclude: pixi\.lock$
6971

7072
- repo: https://github.com/adamchainz/blacken-docs
7173
rev: 1.20.0

containers/client/Dockerfile

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,33 @@
1-
FROM ghcr.io/diracgrid/diracx/client-base
1+
FROM ghcr.io/prefix-dev/pixi:latest AS build
22

3-
ARG EXTRA_PACKAGES_TO_INSTALL
3+
WORKDIR /app
4+
COPY pixi.toml pixi.lock ./
45

5-
RUN --mount=type=bind,source=.,target=/bindmount DIRACX_CUSTOM_SOURCE_PREFIXES=/bindmount /entrypoint.sh bash -ec "pip install --no-deps ${EXTRA_PACKAGES_TO_INSTALL} && echo 'Running pip check' && pip check"
6+
# Copy source directories needed for path-based installs
7+
COPY diracx-core/ diracx-core/
8+
COPY diracx-client/ diracx-client/
9+
COPY diracx-api/ diracx-api/
10+
COPY diracx-cli/ diracx-cli/
11+
COPY diracx-testing/ diracx-testing/
12+
COPY pyproject.toml ./
613

7-
# In many clusters the container is ran as a random uid for security reasons.
8-
# If we mark the conda directory as group 0 and give it group write permissions
9-
# then we're still able to manage the environment from inside the container.
10-
USER 0
11-
RUN chown -R $MAMBA_USER:0 /opt/conda && chmod -R g=u /opt/conda
12-
USER $MAMBA_USER
14+
# Switch to non-editable installs (same workaround as CI)
15+
RUN sed -i 's@editable = true@editable = false@g' pixi.toml
16+
17+
RUN pixi install --locked -e container-client
18+
19+
# Generate activation script
20+
RUN pixi shell-hook -e container-client > /activate.sh
21+
22+
FROM ubuntu:24.04
23+
24+
# Copy the installed environment
25+
COPY --from=build /app/.pixi/envs/container-client /app/.pixi/envs/container-client
26+
COPY --from=build /activate.sh /activate.sh
27+
28+
# In many clusters the container is run as a random uid for security reasons.
29+
RUN chmod -R g=u /app/.pixi
30+
31+
# Use tini as init (installed by pixi in the env)
32+
ENTRYPOINT ["/app/.pixi/envs/container-client/bin/tini", "--", \
33+
"/bin/bash", "-c", "source /activate.sh && exec \"$@\"", "--"]

containers/services/Dockerfile

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,38 @@
1-
FROM ghcr.io/diracgrid/diracx/services-base
1+
FROM ghcr.io/prefix-dev/pixi:latest AS build
22

3-
ARG EXTRA_PACKAGES_TO_INSTALL
3+
WORKDIR /app
4+
COPY pixi.toml pixi.lock ./
45

5-
RUN --mount=type=bind,source=.,target=/bindmount DIRACX_CUSTOM_SOURCE_PREFIXES=/bindmount /entrypoint.sh bash -ec "pip install --no-deps ${EXTRA_PACKAGES_TO_INSTALL} && echo 'Running pip check' && pip check"
6+
# Copy source directories needed for path-based installs
7+
COPY diracx-core/ diracx-core/
8+
COPY diracx-db/ diracx-db/
9+
COPY diracx-logic/ diracx-logic/
10+
COPY diracx-routers/ diracx-routers/
11+
COPY diracx-testing/ diracx-testing/
12+
COPY pyproject.toml ./
613

7-
# In many clusters the container is ran as a random uid for security reasons.
8-
# If we mark the conda directory as group 0 and give it group write permissions
9-
# then we're still able to manage the environment from inside the container.
10-
USER 0
11-
RUN chown -R $MAMBA_USER:0 /opt/conda && chmod -R g=u /opt/conda
12-
USER $MAMBA_USER
14+
# Switch to non-editable installs (same workaround as CI)
15+
RUN sed -i 's@editable = true@editable = false@g' pixi.toml
16+
17+
RUN pixi install --locked -e container-services
18+
19+
# Generate activation script
20+
RUN pixi shell-hook -e container-services > /activate.sh
21+
22+
FROM ubuntu:24.04
23+
24+
EXPOSE 8000
25+
26+
# Copy the installed environment
27+
COPY --from=build /app/.pixi/envs/container-services /app/.pixi/envs/container-services
28+
COPY --from=build /activate.sh /activate.sh
29+
30+
# In many clusters the container is run as a random uid for security reasons.
31+
# If we mark the environment directory as group 0 and give it group write
32+
# permissions then we're still able to manage the environment from inside the
33+
# container.
34+
RUN chmod -R g=u /app/.pixi
35+
36+
# Use tini as init (installed by pixi in the env)
37+
ENTRYPOINT ["/app/.pixi/envs/container-services/bin/tini", "--", \
38+
"/bin/bash", "-c", "source /activate.sh && exec \"$@\"", "--"]

docs/dev/explanations/components/index.md

Lines changed: 26 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -81,57 +81,48 @@ flowchart BT
8181

8282
## Container Images
8383

84-
DiracX utilizes a structured approach to containerization:
84+
DiracX container images are built using [pixi](https://pixi.sh/) environments defined in `pixi.toml`. All dependencies — both conda and PyPI packages — are resolved via the committed `pixi.lock` file, ensuring reproducible builds with exact version parity between development and production.
8585

86-
1. **Base Image**:
86+
### Architecture
8787

88-
- All container images start from `diracx/base`.
88+
Each container image is built using a multi-stage Dockerfile:
8989

90-
2. **Specialized Base Images**:
90+
1. **Build stage**: Uses `ghcr.io/prefix-dev/pixi` to install a pixi environment from the lock file with `pixi install --locked`.
91+
2. **Runtime stage**: Uses a minimal `ubuntu:24.04` base image. The installed pixi environment is copied from the build stage. [Tini](https://github.com/krallin/tini) is used as the init process for proper signal handling.
9192

92-
- `diracx/services-base`
93-
- `diracx/tasks-base`
94-
- `diracx/client-base`
93+
The container environments are defined in `pixi.toml` alongside the development environments, sharing the same solve group to guarantee identical dependency versions:
9594

96-
3. **Image Versioning and Building**:
95+
- **`container-services`**: Includes `diracx-routers`, `diracx-logic`, `diracx-db`, and `diracx-core` plus runtime dependencies (tini, CA certificates, git).
96+
- **`container-client`**: Includes `diracx-cli`, `diracx-api`, `diracx-client`, and `diracx-core` plus runtime dependencies.
9797

98-
- Images are built periodically (e.g., every Monday) and tagged as `YYYY.MM.DD.P`.
99-
- A DiracX release triggers the creation of new `DiracXService`, `diracx/tasks`, and `diracx/client` images, based on specific `diracx/base` tags.
100-
- This approach ensures stability in production environments.
101-
- For testing purposes, the `latest` base images are used, with dependencies installed via `pip install`.
98+
### Building
10299

103-
See this diagram for an example of how this looks in practice:
100+
Container images are self-contained — the Dockerfile copies source code and uses `pixi install --locked` to resolve everything from the lock file. No separate base images or wheel-building steps are needed:
104101

105102
```
106-
┌──────────────────────────┐
107-
┌─────┤ diracx/base:YYYY.MM.DD.P ├─────┐
108-
│ └──────────────────────────┘ │
109-
│ │
110-
┌────────────────▼──────────────────┐ ┌────────────────▼───────────────┐
111-
│ diracx/services-base:YYYY.MM.DD.P │ │ diracx/tasks-base:YYYY.MM.DD.P │
112-
└────────────────┬──────────────────┘ └────────────────┬───────────────┘
113-
│ │
114-
┌───────────▼────────────┐ ┌──────────▼──────────┐
115-
│ diracx/services:v0.X.Y │ │ diracx/tasks:v0.X.Y │
116-
└────────────────────────┘ └─────────────────────┘
117-
103+
┌──────────────────────────────────────────┐
104+
│ pixi build stage │
105+
│ pixi.toml + pixi.lock + source → env │
106+
└────────────────┬─────────────────────────┘
107+
108+
┌───────────▼────────────┐
109+
│ ubuntu:24.04 runtime │
110+
│ + pixi env copied in │
111+
└────────────────────────┘
118112
```
119113

120-
### Dependencies
121-
122-
- There is a noted duplication between `setup.cfg` and `environment.yaml`.
123-
- The `diracx/base` image is built from a Dockerfile with `environment.yml`, primarily defining the Python version and `dirac_environment.yaml` containing the DIRAC specific dependencies. The latter is there as a "temporary" thing.
124-
- The `diracx/services-base` and `diracx/tasks-base` images extend `diracx/base` with additional Dockerfiles and `environment.yml`, tailored to their specific needs.
125-
- The `diracx/services` and `diracx/tasks` images are further built upon their respective base images, adding necessary diracx packages through `pip install --no-dependencies`.
114+
### Versioning
126115

127-
### Entrypoint
116+
- Release builds are tagged with the version (e.g., `ghcr.io/diracgrid/diracx/services:v0.X.Y`).
117+
- Development builds from `main` are tagged as `dev`.
128118

129-
TODO: document the entry point
119+
### Server Entry Points
130120

131121
- `diracx-routers`:
132122
- `diracx.diracx_min_client_version` entry-point defines the diracx minimum client version required by the server to prevent issues. This also searches for extension names instead of `diracx`. The minimum version number has to be updated in `diracx-routers/src/__init.py__`
133123

134124
## Extensions
135125

136-
- Extensions will extend one or more of `diracx`, `diracx-routers`, `diracx-tasks` images (e.g. `lhcbdiracx`, `lhcbdiracx-routers`, `lhcbdiracx-tasks`).
137-
- Extensions provide a corresponding container image based on a specific release of the corresponding DiracX image.
126+
Extensions (e.g., Gubbins, LHCbDIRACX) provide their own `pixi.toml` and `pixi.lock` with container environments that pull in both the extension and upstream DiracX packages transitively.
127+
128+
The extension container Dockerfile follows the same multi-stage pixi pattern, built from the extension's own source directory.
Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,33 @@
1-
FROM ghcr.io/diracgrid/diracx/client:dev
1+
FROM ghcr.io/prefix-dev/pixi:latest AS build
22

3-
#Extension
4-
ENV GUBBINS_IMAGE_PACKAGES=core,client,api,cli,.
3+
WORKDIR /app
4+
COPY pixi.toml pixi.lock ./
55

6-
RUN --mount=type=bind,source=.,target=/bindmount GUBBINS_CUSTOM_SOURCE_PREFIXES=/bindmount /entrypoint.sh bash -exc "ls /bindmount && echo 'Running pip check' && pip check"
6+
# Copy gubbins source directories
7+
COPY gubbins-core/ gubbins-core/
8+
COPY gubbins-client/ gubbins-client/
9+
COPY gubbins-api/ gubbins-api/
10+
COPY gubbins-cli/ gubbins-cli/
11+
COPY gubbins-testing/ gubbins-testing/
12+
COPY pyproject.toml ./
713

8-
# # In many clusters the container is ran as a random uid for security reasons.
9-
# # If we mark the conda directory as group 0 and give it group write permissions
10-
# # then we're still able to manage the environment from inside the container.
11-
USER 0
12-
RUN chown -R $MAMBA_USER:0 /opt/conda && chmod -R g=u /opt/conda
13-
USER $MAMBA_USER
14+
# Switch to non-editable installs (same workaround as CI)
15+
RUN sed -i 's@editable = true@editable = false@g' pixi.toml
16+
17+
RUN pixi install --locked -e container-client
18+
19+
# Generate activation script
20+
RUN pixi shell-hook -e container-client > /activate.sh
21+
22+
FROM ubuntu:24.04
23+
24+
# Copy the installed environment
25+
COPY --from=build /app/.pixi/envs/container-client /app/.pixi/envs/container-client
26+
COPY --from=build /activate.sh /activate.sh
27+
28+
# In many clusters the container is run as a random uid for security reasons.
29+
RUN chmod -R g=u /app/.pixi
30+
31+
# Use tini as init (installed by pixi in the env)
32+
ENTRYPOINT ["/app/.pixi/envs/container-client/bin/tini", "--", \
33+
"/bin/bash", "-c", "source /activate.sh && exec \"$@\"", "--"]

0 commit comments

Comments
 (0)