Skip to content

Commit 234d2eb

Browse files
authored
chore: Split Dockerfile into base and pixi layers (#3521)
FreeSurfer downloads are extremely finicky, and I feel bad about hitting their server as much as we do. This is something we experimented on in PETPrep, to put the non-Pixi (conda, in PETPrep) dependencies in one image that gets pushed to ghcr.io, and then building on top of that.
2 parents d7e5d6e + a78f3ef commit 234d2eb

File tree

6 files changed

+196
-109
lines changed

6 files changed

+196
-109
lines changed

.circleci/config.yml

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,25 @@ jobs:
175175
- run: *docker_auth
176176
- run: *setup_docker_registry
177177
- run:
178-
name: Create named builder
179-
command: docker buildx create --use --name=builder --driver=docker-container
178+
name: Get base image information
179+
command: |
180+
export BASE_IMAGE=$( grep BASE_IMAGE= Dockerfile | cut -d= -f2 )
181+
export BASE_IMAGE_NAME=${BASE_IMAGE%:*}
182+
export BASE_TAG=${BASE_IMAGE#*:}
183+
echo "BASE_IMAGE=$BASE_IMAGE" >> $BASH_ENV
184+
echo "BASE_IMAGE_NAME=$BASE_IMAGE_NAME" >> $BASH_ENV
185+
echo "BASE_TAG=$BASE_TAG" >> $BASH_ENV
186+
env
187+
- run:
188+
name: Build base image, if needed
189+
command: |
190+
if ! docker manifest inspect $BASE_IMAGE; then
191+
docker buildx build --load \
192+
--cache-from $BASE_IMAGE_NAME:latest \
193+
-t $BASE_IMAGE \
194+
--platform linux/amd64 \
195+
-f Dockerfile.base .
196+
fi
180197
- run:
181198
name: Build Docker image (production environment)
182199
no_output_timeout: 60m
@@ -192,7 +209,7 @@ jobs:
192209
echo "them to your fork with ``git push origin --tags``"
193210
fi
194211
# Build docker image
195-
docker buildx build --load --builder builder \
212+
docker buildx build --load \
196213
--cache-from localhost:5000/fmriprep \
197214
--cache-from nipreps/fmriprep:latest \
198215
-t nipreps/fmriprep:latest \
@@ -204,7 +221,7 @@ jobs:
204221
name: Build Docker image (test environment)
205222
no_output_timeout: 60m
206223
command: |
207-
docker buildx build --load --builder builder \
224+
docker buildx build --load \
208225
--cache-from localhost:5000/fmriprep \
209226
--cache-from nipreps/fmriprep:latest \
210227
-t nipreps/fmriprep:test \

.dockerignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ fmriprep.egg-info
1414
.eggs
1515

1616
.pixi
17+
.tox

.github/workflows/docker.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ jobs:
3232

3333
- name: Setup Docker buildx
3434
uses: docker/setup-buildx-action@v3
35+
with:
36+
driver: docker
3537

3638
- name: Log into registry ${{ env.REGISTRY }}
3739
if: github.event_name != 'pull_request'
@@ -41,6 +43,37 @@ jobs:
4143
username: ${{ github.actor }}
4244
password: ${{ secrets.GITHUB_TOKEN }}
4345

46+
- name: Get base image tag, check
47+
id: base
48+
run: |
49+
export BASE_IMAGE=$( grep BASE_IMAGE= Dockerfile | cut -d= -f2 )
50+
export BASE_IMAGE_NAME=${BASE_IMAGE%:*}
51+
export BASE_TAG=${BASE_IMAGE#*:}
52+
echo "image=$BASE_IMAGE" >> $GITHUB_OUTPUT
53+
echo "name=$BASE_IMAGE_NAME" >> $GITHUB_OUTPUT
54+
echo "tag=$BASE_TAG" >> $GITHUB_OUTPUT
55+
env
56+
if docker manifest inspect $BASE_IMAGE; then
57+
echo "build=false" >> $GITHUB_OUTPUT
58+
else
59+
echo "build=true" >> $GITHUB_OUTPUT
60+
fi
61+
62+
- name: Build base image
63+
if: steps.base.outputs.build == 'true'
64+
uses: docker/build-push-action@v6
65+
with:
66+
file: Dockerfile.base
67+
load: true
68+
push: ${{ github.event_name != 'pull_request' }}
69+
tags: |
70+
${{ steps.base.outputs.image }}
71+
${{ steps.base.outputs.name }}:latest
72+
platforms: linux/amd64
73+
cache-from: |
74+
type=registry,ref=${{ steps.base.outputs.name }}:latest
75+
cache-to: type=inline
76+
4477
- name: Extract Docker metadata
4578
id: meta
4679
uses: docker/metadata-action@v5

Dockerfile

Lines changed: 2 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@
2222
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2323
# SOFTWARE.
2424

25-
# Ubuntu 22.04 LTS - Jammy
26-
ARG BASE_IMAGE=ubuntu:jammy-20250730
25+
ARG BASE_IMAGE=ghcr.io/nipreps/fmriprep-base:20250915
2726

2827
#
2928
# Build pixi environment
@@ -71,121 +70,20 @@ RUN uv pip install --system templateflow
7170
COPY scripts/fetch_templates.py fetch_templates.py
7271
RUN python fetch_templates.py
7372

74-
#
75-
# Download stages
76-
#
77-
78-
# Utilities for downloading packages
79-
FROM ${BASE_IMAGE} AS downloader
80-
ENV DEBIAN_FRONTEND="noninteractive" \
81-
LANG="en_US.UTF-8" \
82-
LC_ALL="en_US.UTF-8"
83-
84-
# Bump the date to current to refresh curl/certificates/etc
85-
RUN echo "2025.08.20"
86-
RUN apt-get update && \
87-
apt-get install -y --no-install-recommends \
88-
binutils \
89-
bzip2 \
90-
ca-certificates \
91-
curl \
92-
unzip && \
93-
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
94-
95-
# FreeSurfer 7.3.2
96-
FROM downloader AS freesurfer
97-
COPY docker/files/freesurfer7.3.2-exclude.txt /usr/local/etc/freesurfer7.3.2-exclude.txt
98-
RUN curl -sSL https://surfer.nmr.mgh.harvard.edu/pub/dist/freesurfer/7.3.2/freesurfer-linux-ubuntu22_amd64-7.3.2.tar.gz \
99-
| tar zxv --no-same-owner -C /opt --exclude-from=/usr/local/etc/freesurfer7.3.2-exclude.txt
100-
101-
# MSM HOCR (Nov 19, 2019 release)
102-
FROM downloader AS msm
103-
RUN curl -L -H "Accept: application/octet-stream" https://api.github.com/repos/ecr05/MSM_HOCR/releases/assets/16253707 -o /usr/local/bin/msm \
104-
&& chmod +x /usr/local/bin/msm
10573

10674
#
10775
# Main stage
10876
#
10977
FROM ${BASE_IMAGE} AS base
11078

111-
# Configure apt
112-
ENV DEBIAN_FRONTEND="noninteractive" \
113-
LANG="en_US.UTF-8" \
114-
LC_ALL="en_US.UTF-8"
115-
116-
# Some baseline tools; bc is needed for FreeSurfer, so don't drop it
117-
RUN apt-get update && \
118-
apt-get install -y --no-install-recommends \
119-
bc \
120-
ca-certificates \
121-
curl \
122-
libgomp1 \
123-
libopenblas0-openmp \
124-
lsb-release \
125-
netbase \
126-
tcsh \
127-
xvfb && \
128-
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
129-
130-
131-
# Install downloaded files from stages
132-
COPY --link --from=freesurfer /opt/freesurfer /opt/freesurfer
133-
COPY --link --from=msm /usr/local/bin/msm /usr/local/bin/msm
134-
135-
# Install AFNI from Docker container
136-
# Find libraries with `ldd $BINARIES | grep afni`
137-
COPY --link --from=afni/afni_make_build:AFNI_25.2.09 \
138-
/opt/afni/install/libf2c.so \
139-
/opt/afni/install/libmri.so \
140-
/usr/local/lib/
141-
COPY --link --from=afni/afni_make_build:AFNI_25.2.09 \
142-
/opt/afni/install/3dAutomask \
143-
/opt/afni/install/3dTshift \
144-
/opt/afni/install/3dUnifize \
145-
/opt/afni/install/3dvolreg \
146-
/usr/local/bin/
147-
148-
# Changing library paths requires a re-ldconfig
149-
RUN ldconfig
150-
151-
# Simulate SetUpFreeSurfer.sh
152-
ENV OS="Linux" \
153-
FS_OVERRIDE=0 \
154-
FIX_VERTEX_AREA="" \
155-
FSF_OUTPUT_FORMAT="nii.gz" \
156-
FREESURFER_HOME="/opt/freesurfer"
157-
ENV SUBJECTS_DIR="$FREESURFER_HOME/subjects" \
158-
FUNCTIONALS_DIR="$FREESURFER_HOME/sessions" \
159-
MNI_DIR="$FREESURFER_HOME/mni" \
160-
LOCAL_DIR="$FREESURFER_HOME/local" \
161-
MINC_BIN_DIR="$FREESURFER_HOME/mni/bin" \
162-
MINC_LIB_DIR="$FREESURFER_HOME/mni/lib" \
163-
MNI_DATAPATH="$FREESURFER_HOME/mni/data"
164-
ENV PERL5LIB="$MINC_LIB_DIR/perl5/5.8.5" \
165-
MNI_PERL5LIB="$MINC_LIB_DIR/perl5/5.8.5" \
166-
PATH="$FREESURFER_HOME/bin:$FREESURFER_HOME/tktools:$MINC_BIN_DIR:$PATH"
167-
168-
# AFNI config
169-
ENV AFNI_IMSAVE_WARNINGS="NO"
170-
17179
# Create a shared $HOME directory
17280
RUN useradd -m -s /bin/bash -G users fmriprep
17381
WORKDIR /home/fmriprep
17482
ENV HOME="/home/fmriprep"
83+
RUN chmod -R go=u $HOME
17584

17685
COPY --link --from=templates /templateflow /home/fmriprep/.cache/templateflow
17786

178-
# FSL environment
179-
ENV LANG="C.UTF-8" \
180-
LC_ALL="C.UTF-8" \
181-
PYTHONNOUSERSITE=1 \
182-
FSLOUTPUTTYPE="NIFTI_GZ" \
183-
FSLMULTIFILEQUIT="TRUE" \
184-
FSLLOCKDIR="" \
185-
FSLMACHINELIST="" \
186-
FSLREMOTECALL="" \
187-
FSLGECUDAQ="cuda.q"
188-
18987
# Unless otherwise specified each process should only use one thread - nipype
19088
# will handle parallelization
19189
ENV MKL_NUM_THREADS=1 \

Dockerfile.base

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# fMRIPrep Docker Container Image distribution
2+
#
3+
# MIT License
4+
#
5+
# Copyright (c) The NiPreps Developers
6+
#
7+
# Permission is hereby granted, free of charge, to any person obtaining a copy
8+
# of this software and associated documentation files (the "Software"), to deal
9+
# in the Software without restriction, including without limitation the rights
10+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
# copies of the Software, and to permit persons to whom the Software is
12+
# furnished to do so, subject to the following conditions:
13+
#
14+
# The above copyright notice and this permission notice shall be included in all
15+
# copies or substantial portions of the Software.
16+
#
17+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
# SOFTWARE.
24+
25+
# Ubuntu 22.04 LTS - Jammy
26+
ARG BASE_IMAGE=ubuntu:jammy-20250730
27+
28+
#
29+
# Download stages
30+
#
31+
32+
# Utilities for downloading packages
33+
FROM ${BASE_IMAGE} AS downloader
34+
ENV DEBIAN_FRONTEND="noninteractive" \
35+
LANG="en_US.UTF-8" \
36+
LC_ALL="en_US.UTF-8"
37+
38+
# Bump the date to current to refresh curl/certificates/etc
39+
RUN echo "2025.08.20"
40+
RUN apt-get update && \
41+
apt-get install -y --no-install-recommends \
42+
binutils \
43+
bzip2 \
44+
ca-certificates \
45+
curl \
46+
unzip && \
47+
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
48+
49+
# FreeSurfer 7.3.2
50+
FROM downloader AS freesurfer
51+
COPY docker/files/freesurfer7.3.2-exclude.txt /usr/local/etc/freesurfer7.3.2-exclude.txt
52+
RUN curl -sSL https://surfer.nmr.mgh.harvard.edu/pub/dist/freesurfer/7.3.2/freesurfer-linux-ubuntu22_amd64-7.3.2.tar.gz \
53+
| tar zxv --no-same-owner -C /opt --exclude-from=/usr/local/etc/freesurfer7.3.2-exclude.txt
54+
55+
# MSM HOCR (Nov 19, 2019 release)
56+
FROM downloader AS msm
57+
RUN curl -L -H "Accept: application/octet-stream" https://api.github.com/repos/ecr05/MSM_HOCR/releases/assets/16253707 -o /usr/local/bin/msm \
58+
&& chmod +x /usr/local/bin/msm
59+
60+
#
61+
# Main stage
62+
#
63+
FROM ${BASE_IMAGE} AS base
64+
65+
# Configure apt
66+
ENV DEBIAN_FRONTEND="noninteractive" \
67+
LANG="en_US.UTF-8" \
68+
LC_ALL="en_US.UTF-8"
69+
70+
# Install downloaded files from stages
71+
COPY --link --from=freesurfer /opt/freesurfer /opt/freesurfer
72+
COPY --link --from=msm /usr/local/bin/msm /usr/local/bin/msm
73+
74+
# Some baseline tools; bc is needed for FreeSurfer, so don't drop it
75+
RUN apt-get update && \
76+
apt-get install -y --no-install-recommends \
77+
bc \
78+
ca-certificates \
79+
curl \
80+
libgomp1 \
81+
libopenblas0-openmp \
82+
lsb-release \
83+
netbase \
84+
tcsh \
85+
xvfb && \
86+
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
87+
88+
89+
# Install AFNI from Docker container
90+
# Find libraries with `ldd $BINARIES | grep afni`
91+
COPY --link --from=afni/afni_make_build:AFNI_25.2.09 \
92+
/opt/afni/install/libf2c.so \
93+
/opt/afni/install/libmri.so \
94+
/usr/local/lib/
95+
COPY --link --from=afni/afni_make_build:AFNI_25.2.09 \
96+
/opt/afni/install/3dAutomask \
97+
/opt/afni/install/3dTshift \
98+
/opt/afni/install/3dUnifize \
99+
/opt/afni/install/3dvolreg \
100+
/usr/local/bin/
101+
102+
# Changing library paths requires a re-ldconfig
103+
RUN ldconfig
104+
105+
# Simulate SetUpFreeSurfer.sh
106+
ENV OS="Linux" \
107+
FS_OVERRIDE=0 \
108+
FIX_VERTEX_AREA="" \
109+
FSF_OUTPUT_FORMAT="nii.gz" \
110+
FREESURFER_HOME="/opt/freesurfer"
111+
ENV SUBJECTS_DIR="$FREESURFER_HOME/subjects" \
112+
FUNCTIONALS_DIR="$FREESURFER_HOME/sessions" \
113+
MNI_DIR="$FREESURFER_HOME/mni" \
114+
LOCAL_DIR="$FREESURFER_HOME/local" \
115+
MINC_BIN_DIR="$FREESURFER_HOME/mni/bin" \
116+
MINC_LIB_DIR="$FREESURFER_HOME/mni/lib" \
117+
MNI_DATAPATH="$FREESURFER_HOME/mni/data"
118+
ENV PERL5LIB="$MINC_LIB_DIR/perl5/5.8.5" \
119+
MNI_PERL5LIB="$MINC_LIB_DIR/perl5/5.8.5" \
120+
PATH="$FREESURFER_HOME/bin:$FREESURFER_HOME/tktools:$MINC_BIN_DIR:$PATH"
121+
122+
# AFNI config
123+
ENV AFNI_IMSAVE_WARNINGS="NO"
124+
125+
# FSL environment
126+
ENV LANG="C.UTF-8" \
127+
LC_ALL="C.UTF-8" \
128+
PYTHONNOUSERSITE=1 \
129+
FSLOUTPUTTYPE="NIFTI_GZ" \
130+
FSLMULTIFILEQUIT="TRUE" \
131+
FSLLOCKDIR="" \
132+
FSLMACHINELIST="" \
133+
FSLREMOTECALL="" \
134+
FSLGECUDAQ="cuda.q"

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
.DEFAULT: help
33

44
tag="fmriprep"
5+
BASE_IMAGE := $(shell grep BASE_IMAGE= Dockerfile | cut -d= -f2)
56

67
help:
78
@echo "Premade recipes"
@@ -10,8 +11,11 @@ help:
1011
@echo "\tBuilds a docker image from source. Defaults to 'fmriprep' tag."
1112

1213

13-
docker-build:
14+
docker-build: docker-base
1415
docker build --rm -t $(tag) \
1516
--build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \
1617
--build-arg VCS_REF=`git rev-parse --short HEAD` \
1718
--build-arg VERSION=`hatch version` .
19+
20+
docker-base:
21+
docker pull $(BASE_IMAGE) || docker build -t $(BASE_IMAGE) -f Dockerfile.base .

0 commit comments

Comments
 (0)