name: Docker on: workflow_dispatch: push: branches: [main] pull_request: release: types: [published] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: {} jobs: pre-job: runs-on: ubuntu-latest permissions: contents: read outputs: should_run_server: ${{ steps.found_paths.outputs.server == 'true' || steps.should_force.outputs.should_force == 'true' }} should_run_ml: ${{ steps.found_paths.outputs.machine-learning == 'true' || steps.should_force.outputs.should_force == 'true' }} steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: persist-credentials: false - id: found_paths uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3 with: filters: | server: - 'server/**' - 'openapi/**' - 'web/**' - 'i18n/**' machine-learning: - 'machine-learning/**' workflow: - '.github/workflows/docker.yml' - name: Check if we should force jobs to run id: should_force run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' }}" >> "$GITHUB_OUTPUT" retag_ml: name: Re-Tag ML needs: pre-job permissions: contents: read packages: write if: ${{ needs.pre-job.outputs.should_run_ml == 'false' && !github.event.pull_request.head.repo.fork }} runs-on: ubuntu-latest strategy: matrix: suffix: ['', '-cuda', '-rocm', '-openvino', '-armnn', '-rknn'] steps: - name: Login to GitHub Container Registry uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Re-tag image env: REGISTRY_NAME: 'ghcr.io' REPOSITORY: ${{ github.repository_owner }}/immich-machine-learning TAG_OLD: main${{ matrix.suffix }} TAG_PR: ${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }} TAG_COMMIT: commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }} run: | docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_PR}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}" docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_COMMIT}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}" retag_server: name: Re-Tag Server needs: pre-job permissions: contents: read packages: write if: ${{ needs.pre-job.outputs.should_run_server == 'false' && !github.event.pull_request.head.repo.fork }} runs-on: ubuntu-latest strategy: matrix: suffix: [''] steps: - name: Login to GitHub Container Registry uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Re-tag image env: REGISTRY_NAME: 'ghcr.io' REPOSITORY: ${{ github.repository_owner }}/immich-server TAG_OLD: main${{ matrix.suffix }} TAG_PR: ${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }} TAG_COMMIT: commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }} run: | docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_PR}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}" docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_COMMIT}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}" build_and_push_ml: name: Build and Push ML needs: pre-job permissions: contents: read packages: write if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }} runs-on: ${{ matrix.runner }} env: image: immich-machine-learning context: machine-learning file: machine-learning/Dockerfile GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-machine-learning strategy: # Prevent a failure in one image from stopping the other builds fail-fast: false matrix: include: - platform: linux/amd64 runner: ubuntu-latest device: cpu - platform: linux/arm64 runner: ubuntu-24.04-arm device: cpu - platform: linux/amd64 runner: ubuntu-latest device: cuda suffix: -cuda - platform: linux/amd64 runner: mich device: rocm suffix: -rocm - platform: linux/amd64 runner: ubuntu-latest device: openvino suffix: -openvino - platform: linux/arm64 runner: ubuntu-24.04-arm device: armnn suffix: -armnn - platform: linux/arm64 runner: ubuntu-24.04-arm device: rknn suffix: -rknn steps: - name: Prepare run: | platform=${{ matrix.platform }} echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: persist-credentials: false - name: Set up Docker Buildx uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - name: Login to GitHub Container Registry uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 if: ${{ !github.event.pull_request.head.repo.fork }} with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Generate cache key suffix env: REF: ${{ github.ref_name }} run: | if [[ "${{ github.event_name }}" == "pull_request" ]]; then echo "CACHE_KEY_SUFFIX=pr-${{ github.event.number }}" >> $GITHUB_ENV else SUFFIX=$(echo "${REF}" | sed 's/[^a-zA-Z0-9]/-/g') echo "CACHE_KEY_SUFFIX=${SUFFIX}" >> $GITHUB_ENV fi - name: Generate cache target id: cache-target run: | if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then # Essentially just ignore the cache output (forks can't write to registry cache) echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT else echo "cache-to=type=registry,ref=${GHCR_REPO}-build-cache:${PLATFORM_PAIR}-${{ matrix.device }}-${CACHE_KEY_SUFFIX},mode=max,compression=zstd" >> $GITHUB_OUTPUT fi - name: Generate docker image tags id: meta uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5 env: DOCKER_METADATA_PR_HEAD_SHA: 'true' - name: Build and push image id: build uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0 with: context: ${{ env.context }} file: ${{ env.file }} platforms: ${{ matrix.platforms }} labels: ${{ steps.meta.outputs.labels }} cache-to: ${{ steps.cache-target.outputs.cache-to }} cache-from: | type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-${{ env.CACHE_KEY_SUFFIX }} type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-main outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=${{ !github.event.pull_request.head.repo.fork }} build-args: | DEVICE=${{ matrix.device }} BUILD_ID=${{ github.run_id }} BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }} BUILD_SOURCE_REF=${{ github.ref_name }} BUILD_SOURCE_COMMIT=${{ github.sha }} - name: Export digest run: | # zizmor: ignore[template-injection] mkdir -p ${{ runner.temp }}/digests digest="${{ steps.build.outputs.digest }}" touch "${{ runner.temp }}/digests/${digest#sha256:}" - name: Upload digest uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: ml-digests-${{ matrix.device }}-${{ env.PLATFORM_PAIR }} path: ${{ runner.temp }}/digests/* if-no-files-found: error retention-days: 1 merge_ml: name: Merge & Push ML runs-on: ubuntu-latest permissions: contents: read actions: read packages: write if: ${{ needs.pre-job.outputs.should_run_ml == 'true' && !github.event.pull_request.head.repo.fork }} env: GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-machine-learning DOCKER_REPO: altran1502/immich-machine-learning strategy: matrix: include: - device: cpu - device: cuda suffix: -cuda - device: rocm suffix: -rocm - device: openvino suffix: -openvino - device: armnn suffix: -armnn - device: rknn suffix: -rknn needs: - build_and_push_ml steps: - name: Download digests uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: path: ${{ runner.temp }}/digests pattern: ml-digests-${{ matrix.device }}-* merge-multiple: true - name: Login to Docker Hub if: ${{ github.event_name == 'release' }} uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3 - name: Generate docker image tags id: meta uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5 env: DOCKER_METADATA_PR_HEAD_SHA: 'true' with: flavor: | # Disable latest tag latest=false suffix=${{ matrix.suffix }} images: | name=${{ env.GHCR_REPO }} name=${{ env.DOCKER_REPO }},enable=${{ github.event_name == 'release' }} tags: | # Tag with branch name type=ref,event=branch # Tag with pr-number type=ref,event=pr # Tag with long commit sha hash type=sha,format=long,prefix=commit- # Tag with git tag on release type=ref,event=tag type=raw,value=release,enable=${{ github.event_name == 'release' }} - name: Create manifest list and push working-directory: ${{ runner.temp }}/digests run: | # Process annotations declare -a ANNOTATIONS=() if [[ -n "$DOCKER_METADATA_OUTPUT_JSON" ]]; then while IFS= read -r annotation; do # Extract key and value by removing the manifest: prefix if [[ "$annotation" =~ ^manifest:(.+)=(.+)$ ]]; then key="${BASH_REMATCH[1]}" value="${BASH_REMATCH[2]}" # Use array to properly handle arguments with spaces ANNOTATIONS+=(--annotation "index:$key=$value") fi done < <(jq -r '.annotations[]' <<< "$DOCKER_METADATA_OUTPUT_JSON") fi TAGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ SOURCE_ARGS=$(printf "${GHCR_REPO}@sha256:%s " *) docker buildx imagetools create $TAGS "${ANNOTATIONS[@]}" $SOURCE_ARGS build_and_push_server: name: Build and Push Server runs-on: ${{ matrix.runner }} permissions: contents: read packages: write needs: pre-job if: ${{ needs.pre-job.outputs.should_run_server == 'true' }} env: image: immich-server context: . file: server/Dockerfile GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-server strategy: fail-fast: false matrix: include: - platform: linux/amd64 runner: ubuntu-latest - platform: linux/arm64 runner: ubuntu-24.04-arm steps: - name: Prepare run: | platform=${{ matrix.platform }} echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: persist-credentials: false - name: Set up Docker Buildx uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3 - name: Login to GitHub Container Registry uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 if: ${{ !github.event.pull_request.head.repo.fork }} with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Generate cache key suffix env: REF: ${{ github.ref_name }} run: | if [[ "${{ github.event_name }}" == "pull_request" ]]; then echo "CACHE_KEY_SUFFIX=pr-${{ github.event.number }}" >> $GITHUB_ENV else SUFFIX=$(echo "${REF}" | sed 's/[^a-zA-Z0-9]/-/g') echo "CACHE_KEY_SUFFIX=${SUFFIX}" >> $GITHUB_ENV fi - name: Generate cache target id: cache-target run: | if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then # Essentially just ignore the cache output (forks can't write to registry cache) echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT else echo "cache-to=type=registry,ref=${GHCR_REPO}-build-cache:${PLATFORM_PAIR}-${CACHE_KEY_SUFFIX},mode=max,compression=zstd" >> $GITHUB_OUTPUT fi - name: Generate docker image tags id: meta uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5 env: DOCKER_METADATA_PR_HEAD_SHA: 'true' - name: Build and push image id: build uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0 with: context: ${{ env.context }} file: ${{ env.file }} platforms: ${{ matrix.platform }} labels: ${{ steps.meta.outputs.labels }} cache-to: ${{ steps.cache-target.outputs.cache-to }} cache-from: | type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ env.CACHE_KEY_SUFFIX }} type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-main outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=${{ !github.event.pull_request.head.repo.fork }} build-args: | DEVICE=cpu BUILD_ID=${{ github.run_id }} BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }} BUILD_SOURCE_REF=${{ github.ref_name }} BUILD_SOURCE_COMMIT=${{ github.sha }} - name: Export digest run: | # zizmor: ignore[template-injection] mkdir -p ${{ runner.temp }}/digests digest="${{ steps.build.outputs.digest }}" touch "${{ runner.temp }}/digests/${digest#sha256:}" - name: Upload digest uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: server-digests-${{ env.PLATFORM_PAIR }} path: ${{ runner.temp }}/digests/* if-no-files-found: error retention-days: 1 merge_server: name: Merge & Push Server runs-on: ubuntu-latest permissions: contents: read actions: read packages: write if: ${{ needs.pre-job.outputs.should_run_server == 'true' && !github.event.pull_request.head.repo.fork }} env: GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-server DOCKER_REPO: altran1502/immich-server needs: - build_and_push_server steps: - name: Download digests uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: path: ${{ runner.temp }}/digests pattern: server-digests-* merge-multiple: true - name: Login to Docker Hub if: ${{ github.event_name == 'release' }} uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3 - name: Generate docker image tags id: meta uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5 env: DOCKER_METADATA_PR_HEAD_SHA: 'true' with: flavor: | # Disable latest tag latest=false suffix=${{ matrix.suffix }} images: | name=${{ env.GHCR_REPO }} name=${{ env.DOCKER_REPO }},enable=${{ github.event_name == 'release' }} tags: | # Tag with branch name type=ref,event=branch # Tag with pr-number type=ref,event=pr # Tag with long commit sha hash type=sha,format=long,prefix=commit- # Tag with git tag on release type=ref,event=tag type=raw,value=release,enable=${{ github.event_name == 'release' }} - name: Create manifest list and push working-directory: ${{ runner.temp }}/digests run: | # Process annotations declare -a ANNOTATIONS=() if [[ -n "$DOCKER_METADATA_OUTPUT_JSON" ]]; then while IFS= read -r annotation; do # Extract key and value by removing the manifest: prefix if [[ "$annotation" =~ ^manifest:(.+)=(.+)$ ]]; then key="${BASH_REMATCH[1]}" value="${BASH_REMATCH[2]}" # Use array to properly handle arguments with spaces ANNOTATIONS+=(--annotation "index:$key=$value") fi done < <(jq -r '.annotations[]' <<< "$DOCKER_METADATA_OUTPUT_JSON") fi TAGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ SOURCE_ARGS=$(printf "${GHCR_REPO}@sha256:%s " *) docker buildx imagetools create $TAGS "${ANNOTATIONS[@]}" $SOURCE_ARGS success-check-server: name: Docker Build & Push Server Success needs: [merge_server, retag_server] permissions: {} runs-on: ubuntu-latest if: always() steps: - name: Any jobs failed? if: ${{ contains(needs.*.result, 'failure') }} run: exit 1 - name: All jobs passed or skipped if: ${{ !(contains(needs.*.result, 'failure')) }} # zizmor: ignore[template-injection] run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}" success-check-ml: name: Docker Build & Push ML Success needs: [merge_ml, retag_ml] permissions: {} runs-on: ubuntu-latest if: always() steps: - name: Any jobs failed? if: ${{ contains(needs.*.result, 'failure') }} run: exit 1 - name: All jobs passed or skipped if: ${{ !(contains(needs.*.result, 'failure')) }} # zizmor: ignore[template-injection] run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"