diff --git a/.circleci/config.yml b/.circleci/config.yml index 4dd943f79..ee78c5639 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -175,8 +175,25 @@ jobs: - run: *docker_auth - run: *setup_docker_registry - run: - name: Create named builder - command: docker buildx create --use --name=builder --driver=docker-container + name: Get base image information + command: | + export BASE_IMAGE=$( grep BASE_IMAGE= Dockerfile | cut -d= -f2 ) + export BASE_IMAGE_NAME=${BASE_IMAGE%:*} + export BASE_TAG=${BASE_IMAGE#*:} + echo "BASE_IMAGE=$BASE_IMAGE" >> $BASH_ENV + echo "BASE_IMAGE_NAME=$BASE_IMAGE_NAME" >> $BASH_ENV + echo "BASE_TAG=$BASE_TAG" >> $BASH_ENV + env + - run: + name: Build base image, if needed + command: | + if ! docker manifest inspect $BASE_IMAGE; then + docker buildx build --load \ + --cache-from $BASE_IMAGE_NAME:latest \ + -t $BASE_IMAGE \ + --platform linux/amd64 \ + -f Dockerfile.base . + fi - run: name: Build Docker image (production environment) no_output_timeout: 60m @@ -192,7 +209,7 @@ jobs: echo "them to your fork with ``git push origin --tags``" fi # Build docker image - docker buildx build --load --builder builder \ + docker buildx build --load \ --cache-from localhost:5000/fmriprep \ --cache-from nipreps/fmriprep:latest \ -t nipreps/fmriprep:latest \ @@ -204,7 +221,7 @@ jobs: name: Build Docker image (test environment) no_output_timeout: 60m command: | - docker buildx build --load --builder builder \ + docker buildx build --load \ --cache-from localhost:5000/fmriprep \ --cache-from nipreps/fmriprep:latest \ -t nipreps/fmriprep:test \ diff --git a/.dockerignore b/.dockerignore index 839c5bbce..be234e82f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -14,3 +14,4 @@ fmriprep.egg-info .eggs .pixi +.tox diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 12bdc6c95..7e9742044 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -32,6 +32,8 @@ jobs: - name: Setup Docker buildx uses: docker/setup-buildx-action@v3 + with: + driver: docker - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' @@ -41,6 +43,37 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Get base image tag, check + id: base + run: | + export BASE_IMAGE=$( grep BASE_IMAGE= Dockerfile | cut -d= -f2 ) + export BASE_IMAGE_NAME=${BASE_IMAGE%:*} + export BASE_TAG=${BASE_IMAGE#*:} + echo "image=$BASE_IMAGE" >> $GITHUB_OUTPUT + echo "name=$BASE_IMAGE_NAME" >> $GITHUB_OUTPUT + echo "tag=$BASE_TAG" >> $GITHUB_OUTPUT + env + if docker manifest inspect $BASE_IMAGE; then + echo "build=false" >> $GITHUB_OUTPUT + else + echo "build=true" >> $GITHUB_OUTPUT + fi + + - name: Build base image + if: steps.base.outputs.build == 'true' + uses: docker/build-push-action@v6 + with: + file: Dockerfile.base + load: true + push: ${{ github.event_name != 'pull_request' }} + tags: | + ${{ steps.base.outputs.image }} + ${{ steps.base.outputs.name }}:latest + platforms: linux/amd64 + cache-from: | + type=registry,ref=${{ steps.base.outputs.name }}:latest + cache-to: type=inline + - name: Extract Docker metadata id: meta uses: docker/metadata-action@v5 diff --git a/Dockerfile b/Dockerfile index b3f578a53..683a80832 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,8 +22,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -# Ubuntu 22.04 LTS - Jammy -ARG BASE_IMAGE=ubuntu:jammy-20250730 +ARG BASE_IMAGE=ghcr.io/nipreps/fmriprep-base:20250915 # # Build pixi environment @@ -71,121 +70,20 @@ RUN uv pip install --system templateflow COPY scripts/fetch_templates.py fetch_templates.py RUN python fetch_templates.py -# -# Download stages -# - -# Utilities for downloading packages -FROM ${BASE_IMAGE} AS downloader -ENV DEBIAN_FRONTEND="noninteractive" \ - LANG="en_US.UTF-8" \ - LC_ALL="en_US.UTF-8" - -# Bump the date to current to refresh curl/certificates/etc -RUN echo "2025.08.20" -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - binutils \ - bzip2 \ - ca-certificates \ - curl \ - unzip && \ - apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* - -# FreeSurfer 7.3.2 -FROM downloader AS freesurfer -COPY docker/files/freesurfer7.3.2-exclude.txt /usr/local/etc/freesurfer7.3.2-exclude.txt -RUN curl -sSL https://surfer.nmr.mgh.harvard.edu/pub/dist/freesurfer/7.3.2/freesurfer-linux-ubuntu22_amd64-7.3.2.tar.gz \ - | tar zxv --no-same-owner -C /opt --exclude-from=/usr/local/etc/freesurfer7.3.2-exclude.txt - -# MSM HOCR (Nov 19, 2019 release) -FROM downloader AS msm -RUN curl -L -H "Accept: application/octet-stream" https://api.github.com/repos/ecr05/MSM_HOCR/releases/assets/16253707 -o /usr/local/bin/msm \ - && chmod +x /usr/local/bin/msm # # Main stage # FROM ${BASE_IMAGE} AS base -# Configure apt -ENV DEBIAN_FRONTEND="noninteractive" \ - LANG="en_US.UTF-8" \ - LC_ALL="en_US.UTF-8" - -# Some baseline tools; bc is needed for FreeSurfer, so don't drop it -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - bc \ - ca-certificates \ - curl \ - libgomp1 \ - libopenblas0-openmp \ - lsb-release \ - netbase \ - tcsh \ - xvfb && \ - apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* - - -# Install downloaded files from stages -COPY --link --from=freesurfer /opt/freesurfer /opt/freesurfer -COPY --link --from=msm /usr/local/bin/msm /usr/local/bin/msm - -# Install AFNI from Docker container -# Find libraries with `ldd $BINARIES | grep afni` -COPY --link --from=afni/afni_make_build:AFNI_25.2.09 \ - /opt/afni/install/libf2c.so \ - /opt/afni/install/libmri.so \ - /usr/local/lib/ -COPY --link --from=afni/afni_make_build:AFNI_25.2.09 \ - /opt/afni/install/3dAutomask \ - /opt/afni/install/3dTshift \ - /opt/afni/install/3dUnifize \ - /opt/afni/install/3dvolreg \ - /usr/local/bin/ - -# Changing library paths requires a re-ldconfig -RUN ldconfig - -# Simulate SetUpFreeSurfer.sh -ENV OS="Linux" \ - FS_OVERRIDE=0 \ - FIX_VERTEX_AREA="" \ - FSF_OUTPUT_FORMAT="nii.gz" \ - FREESURFER_HOME="/opt/freesurfer" -ENV SUBJECTS_DIR="$FREESURFER_HOME/subjects" \ - FUNCTIONALS_DIR="$FREESURFER_HOME/sessions" \ - MNI_DIR="$FREESURFER_HOME/mni" \ - LOCAL_DIR="$FREESURFER_HOME/local" \ - MINC_BIN_DIR="$FREESURFER_HOME/mni/bin" \ - MINC_LIB_DIR="$FREESURFER_HOME/mni/lib" \ - MNI_DATAPATH="$FREESURFER_HOME/mni/data" -ENV PERL5LIB="$MINC_LIB_DIR/perl5/5.8.5" \ - MNI_PERL5LIB="$MINC_LIB_DIR/perl5/5.8.5" \ - PATH="$FREESURFER_HOME/bin:$FREESURFER_HOME/tktools:$MINC_BIN_DIR:$PATH" - -# AFNI config -ENV AFNI_IMSAVE_WARNINGS="NO" - # Create a shared $HOME directory RUN useradd -m -s /bin/bash -G users fmriprep WORKDIR /home/fmriprep ENV HOME="/home/fmriprep" +RUN chmod -R go=u $HOME COPY --link --from=templates /templateflow /home/fmriprep/.cache/templateflow -# FSL environment -ENV LANG="C.UTF-8" \ - LC_ALL="C.UTF-8" \ - PYTHONNOUSERSITE=1 \ - FSLOUTPUTTYPE="NIFTI_GZ" \ - FSLMULTIFILEQUIT="TRUE" \ - FSLLOCKDIR="" \ - FSLMACHINELIST="" \ - FSLREMOTECALL="" \ - FSLGECUDAQ="cuda.q" - # Unless otherwise specified each process should only use one thread - nipype # will handle parallelization ENV MKL_NUM_THREADS=1 \ diff --git a/Dockerfile.base b/Dockerfile.base new file mode 100644 index 000000000..be54804ed --- /dev/null +++ b/Dockerfile.base @@ -0,0 +1,134 @@ +# fMRIPrep Docker Container Image distribution +# +# MIT License +# +# Copyright (c) The NiPreps Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +# Ubuntu 22.04 LTS - Jammy +ARG BASE_IMAGE=ubuntu:jammy-20250730 + +# +# Download stages +# + +# Utilities for downloading packages +FROM ${BASE_IMAGE} AS downloader +ENV DEBIAN_FRONTEND="noninteractive" \ + LANG="en_US.UTF-8" \ + LC_ALL="en_US.UTF-8" + +# Bump the date to current to refresh curl/certificates/etc +RUN echo "2025.08.20" +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + binutils \ + bzip2 \ + ca-certificates \ + curl \ + unzip && \ + apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +# FreeSurfer 7.3.2 +FROM downloader AS freesurfer +COPY docker/files/freesurfer7.3.2-exclude.txt /usr/local/etc/freesurfer7.3.2-exclude.txt +RUN curl -sSL https://surfer.nmr.mgh.harvard.edu/pub/dist/freesurfer/7.3.2/freesurfer-linux-ubuntu22_amd64-7.3.2.tar.gz \ + | tar zxv --no-same-owner -C /opt --exclude-from=/usr/local/etc/freesurfer7.3.2-exclude.txt + +# MSM HOCR (Nov 19, 2019 release) +FROM downloader AS msm +RUN curl -L -H "Accept: application/octet-stream" https://api.github.com/repos/ecr05/MSM_HOCR/releases/assets/16253707 -o /usr/local/bin/msm \ + && chmod +x /usr/local/bin/msm + +# +# Main stage +# +FROM ${BASE_IMAGE} AS base + +# Configure apt +ENV DEBIAN_FRONTEND="noninteractive" \ + LANG="en_US.UTF-8" \ + LC_ALL="en_US.UTF-8" + +# Install downloaded files from stages +COPY --link --from=freesurfer /opt/freesurfer /opt/freesurfer +COPY --link --from=msm /usr/local/bin/msm /usr/local/bin/msm + +# Some baseline tools; bc is needed for FreeSurfer, so don't drop it +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + bc \ + ca-certificates \ + curl \ + libgomp1 \ + libopenblas0-openmp \ + lsb-release \ + netbase \ + tcsh \ + xvfb && \ + apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + + +# Install AFNI from Docker container +# Find libraries with `ldd $BINARIES | grep afni` +COPY --link --from=afni/afni_make_build:AFNI_25.2.09 \ + /opt/afni/install/libf2c.so \ + /opt/afni/install/libmri.so \ + /usr/local/lib/ +COPY --link --from=afni/afni_make_build:AFNI_25.2.09 \ + /opt/afni/install/3dAutomask \ + /opt/afni/install/3dTshift \ + /opt/afni/install/3dUnifize \ + /opt/afni/install/3dvolreg \ + /usr/local/bin/ + +# Changing library paths requires a re-ldconfig +RUN ldconfig + +# Simulate SetUpFreeSurfer.sh +ENV OS="Linux" \ + FS_OVERRIDE=0 \ + FIX_VERTEX_AREA="" \ + FSF_OUTPUT_FORMAT="nii.gz" \ + FREESURFER_HOME="/opt/freesurfer" +ENV SUBJECTS_DIR="$FREESURFER_HOME/subjects" \ + FUNCTIONALS_DIR="$FREESURFER_HOME/sessions" \ + MNI_DIR="$FREESURFER_HOME/mni" \ + LOCAL_DIR="$FREESURFER_HOME/local" \ + MINC_BIN_DIR="$FREESURFER_HOME/mni/bin" \ + MINC_LIB_DIR="$FREESURFER_HOME/mni/lib" \ + MNI_DATAPATH="$FREESURFER_HOME/mni/data" +ENV PERL5LIB="$MINC_LIB_DIR/perl5/5.8.5" \ + MNI_PERL5LIB="$MINC_LIB_DIR/perl5/5.8.5" \ + PATH="$FREESURFER_HOME/bin:$FREESURFER_HOME/tktools:$MINC_BIN_DIR:$PATH" + +# AFNI config +ENV AFNI_IMSAVE_WARNINGS="NO" + +# FSL environment +ENV LANG="C.UTF-8" \ + LC_ALL="C.UTF-8" \ + PYTHONNOUSERSITE=1 \ + FSLOUTPUTTYPE="NIFTI_GZ" \ + FSLMULTIFILEQUIT="TRUE" \ + FSLLOCKDIR="" \ + FSLMACHINELIST="" \ + FSLREMOTECALL="" \ + FSLGECUDAQ="cuda.q" diff --git a/Makefile b/Makefile index dbda857ef..96793191e 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ .DEFAULT: help tag="fmriprep" +BASE_IMAGE := $(shell grep BASE_IMAGE= Dockerfile | cut -d= -f2) help: @echo "Premade recipes" @@ -10,8 +11,11 @@ help: @echo "\tBuilds a docker image from source. Defaults to 'fmriprep' tag." -docker-build: +docker-build: docker-base docker build --rm -t $(tag) \ --build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \ --build-arg VCS_REF=`git rev-parse --short HEAD` \ --build-arg VERSION=`hatch version` . + +docker-base: + docker pull $(BASE_IMAGE) || docker build -t $(BASE_IMAGE) -f Dockerfile.base .