From b059608170928060f40bc0c064020647cab59bc7 Mon Sep 17 00:00:00 2001 From: Gaurav Tiwari Date: Thu, 13 Aug 2026 10:01:48 +0100 Subject: [PATCH 01/14] Add reproducible cache benchmark controls --- image/base/Dockerfile | 16 +++++++++++++--- image/docker-bake.hcl | 7 +++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/image/base/Dockerfile b/image/base/Dockerfile index d58217595..0b78e2c70 100644 --- a/image/base/Dockerfile +++ b/image/base/Dockerfile @@ -228,19 +228,29 @@ RUN --mount=type=tmpfs,target=/root/.npm \ FROM discourse-build-base AS discourse-slim ARG DISCOURSE_BRANCH=main +ARG DISCOURSE_REF # Discourse specific bits RUN install -dm 0755 -o discourse -g discourse /var/www/discourse &&\ - sudo -u discourse git clone --branch $DISCOURSE_BRANCH --filter=tree:0 https://github.com/discourse/discourse.git /var/www/discourse + sudo -u discourse git clone --branch "$DISCOURSE_BRANCH" --filter=tree:0 https://github.com/discourse/discourse.git /var/www/discourse &&\ + if [ -n "$DISCOURSE_REF" ]; then \ + sudo -u discourse git -C /var/www/discourse fetch --depth 1 origin "$DISCOURSE_REF" &&\ + sudo -u discourse git -C /var/www/discourse checkout --detach FETCH_HEAD; \ + fi FROM discourse-slim AS discourse-web-only ENV RAILS_ENV=production -RUN cd /var/www/discourse &&\ +RUN --mount=type=cache,id=discourse-bundler-${DISCOURSE_BRANCH},target=/home/discourse/.bundle/cache,sharing=locked,uid=1000,gid=1000 \ + cd /var/www/discourse &&\ + echo "Bundler cache before install:" &&\ + sudo -u discourse du -sh /home/discourse/.bundle/cache &&\ sudo -u discourse bundle config --local deployment true &&\ sudo -u discourse bundle config --local path ./vendor/bundle &&\ sudo -u discourse bundle config --local without test development &&\ - sudo -u discourse bundle install --jobs $(nproc --ignore=1) &&\ + sudo -u discourse env BUNDLE_GLOBAL_GEM_CACHE=true BUNDLE_USER_CACHE=/home/discourse/.bundle/cache bundle install --jobs $(nproc --ignore=1) &&\ + echo "Bundler cache after install:" &&\ + sudo -u discourse du -sh /home/discourse/.bundle/cache &&\ find /var/www/discourse/vendor/bundle -name cache -not -path '*/gems/*' -type d -exec rm -rf {} + &&\ find /var/www/discourse/vendor/bundle -name tmp -type d -exec rm -rf {} + diff --git a/image/docker-bake.hcl b/image/docker-bake.hcl index bfb76b526..b15a5146d 100644 --- a/image/docker-bake.hcl +++ b/image/docker-bake.hcl @@ -22,6 +22,10 @@ variable "DEV_IMAGE" { default = "local_discourse/discourse_dev" } +variable "DISCOURSE_REF" { + default = "" +} + group "base" { targets = ["base-slim", "base-web-only", "base-release"] } @@ -60,6 +64,7 @@ target "base-slim" { platforms = ["linux/${arch}"] args = { "DISCOURSE_BRANCH" = "${branch}" + "DISCOURSE_REF" = branch == "main" ? DISCOURSE_REF : "" } } @@ -75,6 +80,7 @@ target "base-web-only" { platforms = ["linux/${arch}"] args = { "DISCOURSE_BRANCH" = "${branch}" + "DISCOURSE_REF" = branch == "main" ? DISCOURSE_REF : "" } } @@ -90,6 +96,7 @@ target "base-release" { platforms = ["linux/${arch}"] args = { "DISCOURSE_BRANCH" = "${branch}" + "DISCOURSE_REF" = branch == "main" ? DISCOURSE_REF : "" } } From 4b0bdd2a8717c17df2ac1dc7740e792a0175bf72 Mon Sep 17 00:00:00 2001 From: Gaurav Tiwari Date: Thu, 13 Aug 2026 10:03:28 +0100 Subject: [PATCH 02/14] Report the pinned Discourse source --- image/base/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/image/base/Dockerfile b/image/base/Dockerfile index 0b78e2c70..e2aef721f 100644 --- a/image/base/Dockerfile +++ b/image/base/Dockerfile @@ -236,7 +236,8 @@ RUN install -dm 0755 -o discourse -g discourse /var/www/discourse &&\ if [ -n "$DISCOURSE_REF" ]; then \ sudo -u discourse git -C /var/www/discourse fetch --depth 1 origin "$DISCOURSE_REF" &&\ sudo -u discourse git -C /var/www/discourse checkout --detach FETCH_HEAD; \ - fi + fi &&\ + sudo -u discourse git -C /var/www/discourse rev-parse HEAD FROM discourse-slim AS discourse-web-only ENV RAILS_ENV=production From 899754ce85f9d0fd7dbb4497585b9af9cdb0dd83 Mon Sep 17 00:00:00 2001 From: Gaurav Tiwari Date: Thu, 13 Aug 2026 10:10:17 +0100 Subject: [PATCH 03/14] Add a native compilation benchmark revision --- image/base/Dockerfile | 3 +++ image/docker-bake.hcl | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/image/base/Dockerfile b/image/base/Dockerfile index e2aef721f..0986a7e0e 100644 --- a/image/base/Dockerfile +++ b/image/base/Dockerfile @@ -19,6 +19,9 @@ DEBIAN_FRONTEND=noninteractive apt-get -y install wget \ libfreetype-dev \ libbrotli-dev +ARG CCACHE_BENCHMARK_REV +ENV CCACHE_BENCHMARK_REV=${CCACHE_BENCHMARK_REV} + FROM builder AS libheif-builder ADD install-libheif /tmp/install-libheif RUN /tmp/install-libheif diff --git a/image/docker-bake.hcl b/image/docker-bake.hcl index b15a5146d..3c63e23c0 100644 --- a/image/docker-bake.hcl +++ b/image/docker-bake.hcl @@ -26,6 +26,10 @@ variable "DISCOURSE_REF" { default = "" } +variable "CCACHE_BENCHMARK_REV" { + default = "" +} + group "base" { targets = ["base-slim", "base-web-only", "base-release"] } @@ -63,6 +67,7 @@ target "base-slim" { target = "discourse-slim" platforms = ["linux/${arch}"] args = { + "CCACHE_BENCHMARK_REV" = CCACHE_BENCHMARK_REV "DISCOURSE_BRANCH" = "${branch}" "DISCOURSE_REF" = branch == "main" ? DISCOURSE_REF : "" } @@ -79,6 +84,7 @@ target "base-web-only" { target = "discourse-web-only" platforms = ["linux/${arch}"] args = { + "CCACHE_BENCHMARK_REV" = CCACHE_BENCHMARK_REV "DISCOURSE_BRANCH" = "${branch}" "DISCOURSE_REF" = branch == "main" ? DISCOURSE_REF : "" } @@ -95,6 +101,7 @@ target "base-release" { target = "discourse-release" platforms = ["linux/${arch}"] args = { + "CCACHE_BENCHMARK_REV" = CCACHE_BENCHMARK_REV "DISCOURSE_BRANCH" = "${branch}" "DISCOURSE_REF" = branch == "main" ? DISCOURSE_REF : "" } From bbf7ed5e1e8ad1eae32928dbc69f36ee5f4c30b9 Mon Sep 17 00:00:00 2001 From: Gaurav Tiwari Date: Thu, 13 Aug 2026 10:14:05 +0100 Subject: [PATCH 04/14] Keep the Bundler benchmark focused --- image/base/Dockerfile | 3 --- image/docker-bake.hcl | 7 ------- 2 files changed, 10 deletions(-) diff --git a/image/base/Dockerfile b/image/base/Dockerfile index 0986a7e0e..e2aef721f 100644 --- a/image/base/Dockerfile +++ b/image/base/Dockerfile @@ -19,9 +19,6 @@ DEBIAN_FRONTEND=noninteractive apt-get -y install wget \ libfreetype-dev \ libbrotli-dev -ARG CCACHE_BENCHMARK_REV -ENV CCACHE_BENCHMARK_REV=${CCACHE_BENCHMARK_REV} - FROM builder AS libheif-builder ADD install-libheif /tmp/install-libheif RUN /tmp/install-libheif diff --git a/image/docker-bake.hcl b/image/docker-bake.hcl index 3c63e23c0..b15a5146d 100644 --- a/image/docker-bake.hcl +++ b/image/docker-bake.hcl @@ -26,10 +26,6 @@ variable "DISCOURSE_REF" { default = "" } -variable "CCACHE_BENCHMARK_REV" { - default = "" -} - group "base" { targets = ["base-slim", "base-web-only", "base-release"] } @@ -67,7 +63,6 @@ target "base-slim" { target = "discourse-slim" platforms = ["linux/${arch}"] args = { - "CCACHE_BENCHMARK_REV" = CCACHE_BENCHMARK_REV "DISCOURSE_BRANCH" = "${branch}" "DISCOURSE_REF" = branch == "main" ? DISCOURSE_REF : "" } @@ -84,7 +79,6 @@ target "base-web-only" { target = "discourse-web-only" platforms = ["linux/${arch}"] args = { - "CCACHE_BENCHMARK_REV" = CCACHE_BENCHMARK_REV "DISCOURSE_BRANCH" = "${branch}" "DISCOURSE_REF" = branch == "main" ? DISCOURSE_REF : "" } @@ -101,7 +95,6 @@ target "base-release" { target = "discourse-release" platforms = ["linux/${arch}"] args = { - "CCACHE_BENCHMARK_REV" = CCACHE_BENCHMARK_REV "DISCOURSE_BRANCH" = "${branch}" "DISCOURSE_REF" = branch == "main" ? DISCOURSE_REF : "" } From 5604a903dcc2edcb718b46b273ea242946f5cdeb Mon Sep 17 00:00:00 2001 From: Jeff Wong Date: Fri, 12 Jun 2026 21:51:21 -0700 Subject: [PATCH 05/14] add caching and push docker bake config allow for push and cache to be done by including bake config do local caching, for github actions cache. introduce datestamp for cache key use datestamps for cache invalidation --- .github/workflows/build.yml | 88 +++++++++++++++++++++++-------- image/base/Dockerfile | 25 ++++++--- image/discourse_dev/Dockerfile | 6 +-- image/discourse_test/Dockerfile | 15 +++--- image/docker-bake.cache-write.hcl | 67 +++++++++++++++++++++++ image/docker-bake.cache.hcl | 88 +++++++++++++++++++++++++++++++ image/docker-bake.hcl | 79 ++++++++------------------- image/docker-bake.push.hcl | 56 ++++++++++++++++++++ 8 files changed, 327 insertions(+), 97 deletions(-) create mode 100644 image/docker-bake.cache-write.hcl create mode 100644 image/docker-bake.cache.hcl create mode 100644 image/docker-bake.push.hcl diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f14b3d8b5..749592b4d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,13 +22,19 @@ jobs: runs-on: ubuntu-latest outputs: timestamp: ${{ steps.timestamp.outputs.timestamp }} + datestamp: ${{ steps.timestamp.outputs.datestamp }} steps: - id: timestamp run: | timestamp=$(date +%Y%m%d-%H%M) + datestamp=$(echo $timestamp | sed -e 's/-.*$//') echo "timestamp=$timestamp" + echo "datestamp=$datestamp" echo "timestamp=$timestamp" >> $GITHUB_OUTPUT + echo "datestamp=$datestamp" >> $GITHUB_OUTPUT + # run all targets individually for cache, otherwise this may break caching + # possibley related to https://github.com/docker/buildx/issues/414 base: # `cdck-linux-8-core` for amd64 builds # `ubuntu-24.04-8core-arm` for arm64 builds @@ -43,53 +49,66 @@ jobs: working-directory: image env: TIMESTAMP: ${{ needs.timestamp.outputs.timestamp }} + DATESTAMP: ${{ needs.timestamp.outputs.datestamp }} ARCH: ${{ matrix.arch }} + CACHE_IMAGE: ghcr.io/${{ github.repository }} steps: - uses: actions/checkout@v4 with: fetch-depth: 1 - uses: docker/setup-buildx-action@v3 + - name: Login to cache registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Login to Docker Hub if: ${{ github.event_name == 'schedule' || github.event_name == 'push' }} uses: docker/login-action@v3 with: username: ${{ vars.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} - - name: Build runtime deps + - name: Build deps run: | - docker buildx bake base-runtime-deps --no-cache --load + docker buildx bake base-runtime-deps -f docker-bake.hcl -f docker-bake.cache.hcl --load - name: Build slim image run: | - docker buildx bake base-slim --load + docker buildx bake base-slim-main -f docker-bake.hcl -f docker-bake.cache.hcl + docker buildx bake base-slim-stable -f docker-bake.hcl -f docker-bake.cache.hcl - name: Build web only image run: | - docker buildx bake base-web-only --load + docker buildx bake base-web-only-main -f docker-bake.hcl -f docker-bake.cache.hcl --load + docker buildx bake base-web-only-stable -f docker-bake.hcl -f docker-bake.cache.hcl --load - name: Build release image run: | - docker buildx bake base-release --load + docker buildx bake base-release-main -f docker-bake.hcl -f docker-bake.cache.hcl --load + docker buildx bake base-release-stable -f docker-bake.hcl -f docker-bake.cache.hcl --load - name: Build test images run: | - docker buildx bake test-release-${{ matrix.arch }} --load + docker buildx bake test-release -f docker-bake.hcl -f docker-bake.cache.hcl --load - name: run specs for `main` branch if: vars.SKIP_TESTS != '1' run: | - docker run --rm -e RUBY_ONLY=1 -e USE_TURBO=1 -e SKIP_PLUGINS=1 -e SKIP_LINT=1 -e DISCOURSE_TURBO_RSPEC_RETRY_AND_LOG_FLAKY_TESTS=1 ${TEST_IMAGE}:release-${{ matrix.arch }} + docker run --rm -e RUBY_ONLY=1 -e USE_TURBO=1 -e SKIP_PLUGINS=1 -e SKIP_LINT=1 -e DISCOURSE_TURBO_RSPEC_RETRY_AND_LOG_FLAKY_TESTS=1 ${TEST_IMAGE}:release - name: Build and push test image if: ${{ github.event_name == 'schedule' || github.event_name == 'push' }} run: | - docker buildx bake test --set="*.tags=${TEST_IMAGE}" --set="*.output=type=registry,push-by-digest=true" --metadata-file=/tmp/test.json + docker buildx bake test-slim -f docker-bake.hcl -f docker-bake.push.hcl -f docker-bake.cache.hcl -f docker-bake.cache-write.hcl --metadata-file=/tmp/test-slim.json + docker buildx bake test-slim-browsers -f docker-bake.hcl -f docker-bake.push.hcl -f docker-bake.cache.hcl -f docker-bake.cache-write.hcl --metadata-file=/tmp/test-slim-browsers.json + docker buildx bake test-release -f docker-bake.hcl -f docker-bake.push.hcl -f docker-bake.cache.hcl -f docker-bake.cache-write.hcl --metadata-file=/tmp/test-release.json - name: Build and push dev image if: ${{ github.event_name == 'schedule' || github.event_name == 'push' }} run: | - docker buildx bake dev --set="*.tags=${DEV_IMAGE}" --set="*.output=type=registry,push-by-digest=true" --allow=fs.read=../templates --metadata-file=/tmp/dev.json + docker buildx bake --allow=fs.read=../templates dev -f docker-bake.hcl -f docker-bake.push.hcl -f docker-bake.cache.hcl -f docker-bake.cache-write.hcl --metadata-file=/tmp/dev.json - name: Print summary run: | @@ -97,57 +116,69 @@ jobs: - name: Print `docker history` summary for main branch image run: | - docker history ${BASE_IMAGE}:release-main-${{ matrix.arch }} + docker history ${BASE_IMAGE}:release-main - name: push to dockerhub if: ${{ github.event_name == 'schedule' || github.event_name == 'push' }} run: | - docker buildx bake base --set="*.tags=${BASE_IMAGE}" --set="*.output=type=registry,push-by-digest=true" --metadata-file=/tmp/base.json + docker buildx bake base-runtime-deps -f docker-bake.hcl -f docker-bake.push.hcl -f docker-bake.cache.hcl -f docker-bake.cache-write.hcl --metadata-file=/tmp/base-runtime-deps.json + docker buildx bake base-slim-main -f docker-bake.hcl -f docker-bake.push.hcl -f docker-bake.cache.hcl -f docker-bake.cache-write.hcl --metadata-file=/tmp/base-slim-main.json + docker buildx bake base-slim-stable -f docker-bake.hcl -f docker-bake.push.hcl -f docker-bake.cache.hcl -f docker-bake.cache-write.hcl --metadata-file=/tmp/base-slim-stable.json + docker buildx bake base-web-only-main -f docker-bake.hcl -f docker-bake.push.hcl -f docker-bake.cache.hcl -f docker-bake.cache-write.hcl --metadata-file=/tmp/base-web-only-main.json + docker buildx bake base-web-only-stable -f docker-bake.hcl -f docker-bake.push.hcl -f docker-bake.cache.hcl -f docker-bake.cache-write.hcl --metadata-file=/tmp/base-web-only-stable.json + docker buildx bake base-release-main -f docker-bake.hcl -f docker-bake.push.hcl -f docker-bake.cache.hcl -f docker-bake.cache-write.hcl --metadata-file=/tmp/base-release-main.json + docker buildx bake base-release-stable -f docker-bake.hcl -f docker-bake.push.hcl -f docker-bake.cache.hcl -f docker-bake.cache-write.hcl --metadata-file=/tmp/base-release-stable.json - name: save image digests if: ${{ github.event_name == 'schedule' || github.event_name == 'push' }} id: metadata run: | - test_slim_${{ matrix.arch }}=$(jq -r '."test-slim-${{ matrix.arch }}"["containerimage.digest"]' /tmp/test.json) + test_slim_${{ matrix.arch }}=$(jq -r '."test-slim"["containerimage.digest"]' /tmp/test-slim.json) echo "test_slim_${{ matrix.arch }}=$test_slim_${{ matrix.arch }}" echo "test_slim_${{ matrix.arch }}=$test_slim_${{ matrix.arch }}" >> $GITHUB_OUTPUT - test_slim_browsers_${{ matrix.arch }}=$(jq -r '."test-slim-browsers-${{ matrix.arch }}"["containerimage.digest"]' /tmp/test.json) + test_slim_browsers_${{ matrix.arch }}=$(jq -r '."test-slim-browsers"["containerimage.digest"]' /tmp/test-slim-browsers.json) echo "test_slim_browsers_${{ matrix.arch }}=$test_slim_browsers_${{ matrix.arch }}" echo "test_slim_browsers_${{ matrix.arch }}=$test_slim_browsers_${{ matrix.arch }}" >> $GITHUB_OUTPUT - test_release_${{ matrix.arch }}=$(jq -r '."test-release-${{ matrix.arch }}"["containerimage.digest"]' /tmp/test.json) + test_release_${{ matrix.arch }}=$(jq -r '."test-release"["containerimage.digest"]' /tmp/test-release.json) echo "test_release_${{ matrix.arch }}=$test_release_${{ matrix.arch }}" echo "test_release_${{ matrix.arch }}=$test_release_${{ matrix.arch }}" >> $GITHUB_OUTPUT - dev_${{ matrix.arch }}=$(jq -r '."dev-${{ matrix.arch }}"["containerimage.digest"]' /tmp/dev.json) + dev_${{ matrix.arch }}=$(jq -r '."dev"["containerimage.digest"]' /tmp/dev.json) echo "dev_${{ matrix.arch }}=$dev_${{ matrix.arch }}" echo "dev_${{ matrix.arch }}=$dev_${{ matrix.arch }}" >> $GITHUB_OUTPUT - base_slim_main_${{ matrix.arch }}=$(jq -r '."base-slim-main-${{ matrix.arch }}"["containerimage.digest"]' /tmp/base.json) + base_runtime_deps_${{ matrix.arch }}=$(jq -r '."base-runtime-deps"["containerimage.digest"]' /tmp/base-runtime-deps.json) + echo "base_runtime_deps_${{ matrix.arch }}=$base_runtime_deps_${{ matrix.arch }}" + echo "base_runtime_deps_${{ matrix.arch }}=$base_runtime_deps_${{ matrix.arch }}" >> $GITHUB_OUTPUT + + base_slim_main_${{ matrix.arch }}=$(jq -r '."base-slim-main"["containerimage.digest"]' /tmp/base-slim-main.json) echo "base_slim_main_${{ matrix.arch }}=$base_slim_main_${{ matrix.arch }}" echo "base_slim_main_${{ matrix.arch }}=$base_slim_main_${{ matrix.arch }}" >> $GITHUB_OUTPUT - base_slim_stable_${{ matrix.arch }}=$(jq -r '."base-slim-stable-${{ matrix.arch }}"["containerimage.digest"]' /tmp/base.json) + base_slim_stable_${{ matrix.arch }}=$(jq -r '."base-slim-stable"["containerimage.digest"]' /tmp/base-slim-stable.json) echo "base_slim_stable_${{ matrix.arch }}=$base_slim_stable_${{ matrix.arch }}" echo "base_slim_stable_${{ matrix.arch }}=$base_slim_stable_${{ matrix.arch }}" >> $GITHUB_OUTPUT - base_web_only_main_${{ matrix.arch }}=$(jq -r '."base-web-only-main-${{ matrix.arch }}"["containerimage.digest"]' /tmp/base.json) + base_web_only_main_${{ matrix.arch }}=$(jq -r '."base-web-only-main"["containerimage.digest"]' /tmp/base-web-only-main.json) echo "base_web_only_main_${{ matrix.arch }}=$base_web_only_main_${{ matrix.arch }}" echo "base_web_only_main_${{ matrix.arch }}=$base_web_only_main_${{ matrix.arch }}" >> $GITHUB_OUTPUT - base_web_only_stable_${{ matrix.arch }}=$(jq -r '."base-web-only-stable-${{ matrix.arch }}"["containerimage.digest"]' /tmp/base.json) + base_web_only_stable_${{ matrix.arch }}=$(jq -r '."base-web-only-stable"["containerimage.digest"]' /tmp/base-web-only-stable.json) echo "base_web_only_stable_${{ matrix.arch }}=$base_web_only_stable_${{ matrix.arch }}" echo "base_web_only_stable_${{ matrix.arch }}=$base_web_only_stable_${{ matrix.arch }}" >> $GITHUB_OUTPUT - base_release_main_${{ matrix.arch }}=$(jq -r '."base-release-main-${{ matrix.arch }}"["containerimage.digest"]' /tmp/base.json) + base_release_main_${{ matrix.arch }}=$(jq -r '."base-release-main"["containerimage.digest"]' /tmp/base-release-main.json) echo "base_release_main_${{ matrix.arch }}=$base_release_main_${{ matrix.arch }}" echo "base_release_main_${{ matrix.arch }}=$base_release_main_${{ matrix.arch }}" >> $GITHUB_OUTPUT - base_release_stable_${{ matrix.arch }}=$(jq -r '."base-release-stable-${{ matrix.arch }}"["containerimage.digest"]' /tmp/base.json) + base_release_stable_${{ matrix.arch }}=$(jq -r '."base-release-stable"["containerimage.digest"]' /tmp/base-release-stable.json) echo "base_release_stable_${{ matrix.arch }}=$base_release_stable_${{ matrix.arch }}" echo "base_release_stable_${{ matrix.arch }}=$base_release_stable_${{ matrix.arch }}" >> $GITHUB_OUTPUT outputs: + base_runtime_deps_amd64: ${{ steps.metadata.outputs.base_runtime_deps_amd64 }} + base_runtime_deps_arm64: ${{ steps.metadata.outputs.base_runtime_deps_arm64 }} base_slim_main_amd64: ${{ steps.metadata.outputs.base_slim_main_amd64 }} base_slim_main_arm64: ${{ steps.metadata.outputs.base_slim_main_arm64 }} base_slim_stable_amd64: ${{ steps.metadata.outputs.base_slim_stable_amd64 }} @@ -195,11 +226,12 @@ jobs: - name: build and push to dockerhub run: | - docker buildx bake setup-wizard --set="*.tags=${SETUP_WIZARD_IMAGE}" --set="*.output=type=registry,push-by-digest=true" --metadata-file=/tmp/setup-wizard.json + docker buildx bake setup-wizard -f docker-bake.hcl -f docker-bake.push.hcl --metadata-file=/tmp/setup-wizard.json + - name: save image digests id: metadata run: | - ${{ matrix.arch }}=$(jq -r '."setup-wizard-${{ matrix.arch }}"["containerimage.digest"]' /tmp/setup-wizard.json) + ${{ matrix.arch }}=$(jq -r '."setup-wizard"["containerimage.digest"]' /tmp/setup-wizard.json) echo "${{ matrix.arch }}=$${{ matrix.arch }}" echo "${{ matrix.arch }}=$${{ matrix.arch }}" >> $GITHUB_OUTPUT outputs: @@ -232,6 +264,16 @@ jobs: ${{ needs.base.outputs.base_slim_main_amd64 }} \ ${{ needs.base.outputs.base_slim_main_arm64 }} + # runtime-deps timestamped + docker buildx imagetools create -t ${BASE_IMAGE}:2.0.${{ env.TIMESTAMP }}-runtime-deps \ + ${{ needs.base.outputs.base_runtime_deps_amd64 }} \ + ${{ needs.base.outputs.base_runtime_deps_arm64 }} + + # runtime-deps release + docker buildx imagetools create -t ${BASE_IMAGE}:runtime-deps \ + ${{ needs.base.outputs.base_runtime_deps_amd64 }} \ + ${{ needs.base.outputs.base_runtime_deps_arm64 }} + # Web-Only `main` timestamped docker buildx imagetools create -t ${BASE_IMAGE}:2.0.${{ env.TIMESTAMP }}-web-only \ ${{ needs.base.outputs.base_web_only_main_amd64 }} \ diff --git a/image/base/Dockerfile b/image/base/Dockerfile index e2aef721f..2dc39707d 100644 --- a/image/base/Dockerfile +++ b/image/base/Dockerfile @@ -4,8 +4,10 @@ ARG DEBIAN_RELEASE=trixie ARG RUBY_VERSION=3.4.10 ARG FROM_DOCKER_IMAGE_TAG=${RUBY_VERSION}-${DEBIAN_RELEASE}-slim +ARG DATESTAMP=0 FROM discourse/ruby:${FROM_DOCKER_IMAGE_TAG} AS builder +ARG DATESTAMP ARG DEBIAN_RELEASE ENV DEBIAN_RELEASE=${DEBIAN_RELEASE} RUN echo "deb http://deb.debian.org/debian ${DEBIAN_RELEASE}-backports main" > "/etc/apt/sources.list.d/${DEBIAN_RELEASE}-backports.list" @@ -24,7 +26,7 @@ ADD install-libheif /tmp/install-libheif RUN /tmp/install-libheif FROM libheif-builder AS imagemagick_builder -ADD install-imagemagick /tmp/install-imagemagick +COPY install-imagemagick /tmp/install-imagemagick RUN /tmp/install-imagemagick FROM libheif-builder AS jpegli-builder @@ -33,26 +35,34 @@ RUN /tmp/install-jpegli FROM libheif-builder AS vips-builder COPY --from=jpegli-builder /usr/local/lib/jpegli /usr/local/lib/jpegli -ADD install-vips /tmp/install-vips +COPY install-vips /tmp/install-vips RUN /tmp/install-vips +FROM builder AS nginx-builder +# From https://nginx.org/en/pgp_keys.html +COPY nginx_public_keys.key /tmp/nginx_public_keys.key +COPY install-nginx /tmp/install-nginx +RUN gpg --import /tmp/nginx_public_keys.key &&\ + rm /tmp/nginx_public_keys.key &&\ + /tmp/install-nginx + FROM builder AS thpoff-builder # This tool allows us to disable huge page support for our current process # since the flag is preserved through forks and execs it can be used on any # process -ADD thpoff.c /src/thpoff.c +COPY thpoff.c /src/thpoff.c RUN gcc -o /usr/local/sbin/thpoff /src/thpoff.c && rm /src/thpoff.c FROM builder AS jemalloc-builder -ADD install-jemalloc /tmp/install-jemalloc +COPY install-jemalloc /tmp/install-jemalloc RUN /tmp/install-jemalloc FROM builder AS oxipng-builder -ADD install-oxipng /tmp/install-oxipng +COPY install-oxipng /tmp/install-oxipng RUN /tmp/install-oxipng FROM discourse/ruby:${FROM_DOCKER_IMAGE_TAG} AS discourse-runtime-base - +ARG DATESTAMP ARG DEBIAN_RELEASE ARG PG_MAJOR=18 ARG PG_MAJOR_OLD=15 @@ -180,6 +190,7 @@ COPY --from=thpoff-builder /usr/local/sbin/thpoff /usr/local/sbin COPY --from=jemalloc-builder /usr/lib/libjemalloc.so /usr/lib COPY --from=oxipng-builder /usr/local/bin/jhead /usr/local/bin COPY --from=oxipng-builder /usr/local/bin/oxipng /usr/local/bin +COPY install-redis /tmp/install-redis # version check: https://rubygems.org/gems/pups RUN cd /tmp &&\ @@ -200,7 +211,7 @@ COPY sbin/ /sbin FROM discourse-runtime-base AS discourse-build-base # From https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key # fingerprint: 6F71 F525 2828 41EE DAF8 51B4 2F59 B5F9 9B1B E0B4 -ADD nodesource-repo.gpg.key /usr/share/keyrings/nodesource.asc +COPY nodesource-repo.gpg.key /usr/share/keyrings/nodesource.asc RUN --mount=type=tmpfs,target=/var/log \ --mount=type=tmpfs,target=/var/cache/apt \ --mount=type=tmpfs,target=/var/lib/apt \ diff --git a/image/discourse_dev/Dockerfile b/image/discourse_dev/Dockerfile index 6f4989008..509a62cd6 100644 --- a/image/discourse_dev/Dockerfile +++ b/image/discourse_dev/Dockerfile @@ -27,7 +27,7 @@ RUN apt-get -y update && DEBIAN_FRONTEND=noninteractive apt-get -y install redis RUN rm -rf /var/www/* # Give discourse user no-passwd sudo permissions (for bundle install) -ADD sudoers.discourse /etc/sudoers.d/discourse +COPY sudoers.discourse /etc/sudoers.d/discourse RUN sudo -u discourse bundle config set --global path /home/discourse/.bundle/gems @@ -49,7 +49,7 @@ RUN sed -e 's/\(db_name: discourse\)/\1_development/' /pups/postgres.template.ym RUN LANG=en_US.UTF-8 /pups/bin/pups /pups/postgres.yml # add dev databases -ADD postgres_dev.template.yml /pups/postgres_dev.yml +COPY postgres_dev.template.yml /pups/postgres_dev.yml RUN /pups/bin/pups /pups/postgres_dev.yml # move default postgres_data out of the way @@ -57,7 +57,7 @@ RUN mv /shared/postgres_data /shared/postgres_data_orig # re-instantiate data on boot if needed (this will allow it to persist across # invocations when used with a mounted volume) -ADD ensure-database /etc/runit/1.d/ensure-database +COPY ensure-database /etc/runit/1.d/ensure-database # Install Mailpit, retaining the old binary name for discourse/bin/docker compatibility COPY --from=mailpit --chmod=0755 /mailpit /usr/local/bin/mailpit diff --git a/image/discourse_test/Dockerfile b/image/discourse_test/Dockerfile index ca821cdc7..1d4b23ee2 100644 --- a/image/discourse_test/Dockerfile +++ b/image/discourse_test/Dockerfile @@ -29,12 +29,12 @@ FROM base AS with_browsers ENV TESTEM_DEFAULT_BROWSER=Chrome # From https://dl.google.com/linux/linux_signing_key.pub # fingerprint: EB4C 1BFD 4F04 2F6D DDCC EC91 7721 F63B D38B 4796 -ADD google-linux-signing-key.pub /usr/share/keyrings/google-chrome.asc -ADD install-chrome /tmp/install-chrome +COPY google-linux-signing-key.pub /usr/share/keyrings/google-chrome.asc +COPY install-chrome /tmp/install-chrome # From https://archive.mozilla.org/pub/firefox/releases//KEY # fingerprint: 14F2 6682 D091 6CDD 81E3 7B6D 61B7 B526 D98F 0353 -ADD mozilla-release-key.asc /tmp/mozilla-release-key.asc -ADD install-firefox /tmp/install-firefox +COPY mozilla-release-key.asc /tmp/mozilla-release-key.asc +COPY install-firefox /tmp/install-firefox RUN /tmp/install-chrome &&\ apt update &&\ apt install -y libxss1 firefox-esr &&\ @@ -43,8 +43,11 @@ RUN /tmp/install-chrome &&\ FROM with_browsers AS release -RUN cd /var/www/discourse &&\ - sudo -u discourse bundle install --jobs $(nproc --ignore=1) &&\ +RUN --mount=type=cache,id=discourse-test-bundler,target=/home/discourse/.bundle/cache,sharing=locked,uid=1000,gid=1000 \ + cd /var/www/discourse &&\ + sudo -u discourse bundle config --local deployment true &&\ + sudo -u discourse bundle config --local path ./vendor/bundle &&\ + sudo -u discourse env BUNDLE_GLOBAL_GEM_CACHE=true BUNDLE_USER_CACHE=/home/discourse/.bundle/cache bundle install --jobs $(nproc --ignore=1) &&\ sudo -E -u discourse -H /bin/bash -c 'CI=1 pnpm install' RUN cd /var/www/discourse && sudo -E -u discourse -H bundle exec rake plugin:install_all_official &&\ diff --git a/image/docker-bake.cache-write.hcl b/image/docker-bake.cache-write.hcl new file mode 100644 index 000000000..4916c6306 --- /dev/null +++ b/image/docker-bake.cache-write.hcl @@ -0,0 +1,67 @@ +target "base-runtime-deps" { + cache-to = [{ + type = "registry" + ref = "${CACHE_IMAGE}:cache-runtime-deps-${ARCH}" + mode = "max" + }] +} +target "base-build-deps" { + cache-to = [{ + type = "registry" + ref = "${CACHE_IMAGE}:cache-build-deps-${ARCH}" + mode = "max" + }] +} +target "base-slim" { + name = "base-slim-${branch}" + matrix = { + branch = ["main", "stable"] + } + cache-to = [{ + type = "registry" + ref = "${CACHE_IMAGE}:cache-slim-${branch}-${ARCH}" + mode = "max" + }] +} +target "base-web-only" { + name = "base-web-only-${branch}" + matrix = { + branch = ["main", "stable"] + } + cache-to = [{ + type = "registry" + ref = "${CACHE_IMAGE}:cache-web-only-${branch}-${ARCH}" + mode = "max" + }] +} +target "base-release" { + name = "base-release-${branch}" + matrix = { + branch = ["main", "stable"] + } + cache-to = [{ + type = "registry" + ref = "${CACHE_IMAGE}:cache-release-${branch}-${ARCH}" + mode = "max" + }] +} + +target "test" { + name = "test-${tag}" + matrix = { + tag = ["slim", "slim-browsers", "release"] + } + cache-to = [{ + type = "registry" + ref = "${CACHE_IMAGE}:cache-test-${tag}-${ARCH}" + mode = "max" + }] +} + +target "dev" { + cache-to = [{ + type = "registry" + ref = "${CACHE_IMAGE}:cache-dev-${ARCH}" + mode = "max" + }] +} diff --git a/image/docker-bake.cache.hcl b/image/docker-bake.cache.hcl new file mode 100644 index 000000000..4335a64c2 --- /dev/null +++ b/image/docker-bake.cache.hcl @@ -0,0 +1,88 @@ +variable "CACHE_IMAGE" { + default = "" + validation { + condition = CACHE_IMAGE != "" + error_message = "The variable 'CACHE_IMAGE' must not be empty." + } +} + +variable "ARCH" { + deafault = "" + validation { + condition = ARCH != "" + error_message = "The variable 'ARCH' must not be empty." + } +} + +target "base-runtime-deps" { + cache-from = [{ + type = "registry" + ref = "${CACHE_IMAGE}:cache-runtime-deps-${ARCH}" + }] +} +target "base-build-deps" { + cache-from = [{ + type = "registry" + ref = "${CACHE_IMAGE}:cache-build-deps-${ARCH}" + }] +} +target "base-slim" { + name = "base-slim-${branch}" + matrix = { + branch = ["main", "stable"] + } + cache-from = [{ + type = "registry" + ref = "${CACHE_IMAGE}:cache-slim-${branch}-${ARCH}" + }] +} +target "base-web-only" { + name = "base-web-only-${branch}" + matrix = { + branch = ["main", "stable"] + } + cache-from = [{ + type = "registry" + ref = "${CACHE_IMAGE}:cache-web-only-${branch}-${ARCH}" + }] +} +target "base-release" { + name = "base-release-${branch}" + matrix = { + branch = ["main", "stable"] + } + cache-from = [{ + type = "registry" + ref = "${CACHE_IMAGE}:cache-release-${branch}-${ARCH}" + }] +} + +target "test" { + name = "test-${tag}" + matrix = { + tag = ["slim", "slim-browsers", "release"] + } + cache-from = [ + { + type = "registry" + ref = "${CACHE_IMAGE}:cache-test-${tag}-${ARCH}" + }, + { + type = "registry" + ref = "${CACHE_IMAGE}:cache-slim-main-${ARCH}" + } + ] +} + +target "dev" { + cache-from = [ + { + type = "registry" + ref = "${CACHE_IMAGE}:cache-dev-${ARCH}" + }, + { + type = "registry" + ref = "${CACHE_IMAGE}:cache-slim-main-${ARCH}" + } + ] +} diff --git a/image/docker-bake.hcl b/image/docker-bake.hcl index b15a5146d..1e7ec56ac 100644 --- a/image/docker-bake.hcl +++ b/image/docker-bake.hcl @@ -1,11 +1,3 @@ -variable "ARCH" { - default = "amd64,arm64" -} - -variable "ARCH_ARRAY" { - default = split(",", ARCH) -} - variable "BASE_IMAGE" { default = "local_discourse/base" } @@ -26,42 +18,33 @@ variable "DISCOURSE_REF" { default = "" } -group "base" { - targets = ["base-slim", "base-web-only", "base-release"] +variable "DATESTAMP" { + default = "0" } target "base-runtime-deps" { - name = "base-runtime-deps-${arch}" - matrix = { - arch = ARCH_ARRAY - } context = "./base" - tags = ["${BASE_IMAGE}:runtime-deps-${arch}"] + tags = ["${BASE_IMAGE}:runtime-deps"] target = "discourse-runtime-base" - platforms = ["linux/${arch}"] + args = { + "DATESTAMP" = DATESTAMP + } } target "base-build-deps" { - name = "base-build-deps-${arch}" - matrix = { - arch = ARCH_ARRAY - } context = "./base" - tags = ["${BASE_IMAGE}:build-deps-${arch}"] + tags = ["${BASE_IMAGE}:build-deps"] target = "discourse-build-base" - platforms = ["linux/${arch}"] } target "base-slim" { - name = "base-slim-${branch}-${arch}" + name = "base-slim-${branch}" matrix = { - arch = ARCH_ARRAY branch = ["main", "stable"] } context = "./base" - tags = ["${BASE_IMAGE}:slim-${branch}-${arch}"] + tags = ["${BASE_IMAGE}:slim-${branch}"] target = "discourse-slim" - platforms = ["linux/${arch}"] args = { "DISCOURSE_BRANCH" = "${branch}" "DISCOURSE_REF" = branch == "main" ? DISCOURSE_REF : "" @@ -69,15 +52,13 @@ target "base-slim" { } target "base-web-only" { - name = "base-web-only-${branch}-${arch}" + name = "base-web-only-${branch}" matrix = { - arch = ARCH_ARRAY branch = ["main", "stable"] } context = "./base" - tags = ["${BASE_IMAGE}:web-only-${branch}-${arch}"] + tags = ["${BASE_IMAGE}:web-only-${branch}"] target = "discourse-web-only" - platforms = ["linux/${arch}"] args = { "DISCOURSE_BRANCH" = "${branch}" "DISCOURSE_REF" = branch == "main" ? DISCOURSE_REF : "" @@ -85,40 +66,32 @@ target "base-web-only" { } target "base-release" { - name = "base-release-${branch}-${arch}" + name = "base-release-${branch}" matrix = { - arch = ARCH_ARRAY branch = ["main", "stable"] } context = "./base" - tags = ["${BASE_IMAGE}:release-${branch}-${arch}"] + tags = ["${BASE_IMAGE}:release-${branch}"] target = "discourse-release" - platforms = ["linux/${arch}"] args = { "DISCOURSE_BRANCH" = "${branch}" "DISCOURSE_REF" = branch == "main" ? DISCOURSE_REF : "" } } -# depends on raw arch image, canary build for test images when building base images target "test" { - name = "test-${build_target.tag}-${arch}" + name = "test-${build_target.tag}" matrix = { - arch = ARCH_ARRAY - branch = ["main"] build_target = [ { - from_tag = "slim" target_name = "base" tag = "slim" }, { - from_tag = "slim" target_name = "with_browsers" tag = "slim-browsers" }, { - from_tag = "release" target_name = "release" tag = "release" } @@ -126,40 +99,30 @@ target "test" { } target = build_target.target_name context = "./discourse_test" - platforms = ["linux/${arch}"] - tags = ["${TEST_IMAGE}:${build_target.tag}-${arch}"] + tags = ["${TEST_IMAGE}:${build_target.tag}"] args = { "from_tag" = "from" + "SOURCE_DATE_EPOCH" = 0 } contexts = { - from = "target:base-${build_target.from_tag}-${branch}-${arch}" + from = "target:base-slim-main" } } target "dev" { - name = "dev-${arch}" - matrix = { - arch = ARCH_ARRAY - branch = ["main"] - } context = "./discourse_dev" - tags = ["${DEV_IMAGE}:release-${arch}"] - platforms = ["linux/${arch}"] + tags = ["${DEV_IMAGE}:release"] args = { "from_tag" = "from" + "SOURCE_DATE_EPOCH" = 0 } contexts = { - from = "target:base-slim-${branch}-${arch}" + from = "target:base-slim-main" templates = "../templates" } } target "setup-wizard" { - name = "setup-wizard-${arch}" - matrix = { - arch = ARCH_ARRAY - } context = "./setup_wizard" - tags = ["${SETUP_WIZARD_IMAGE}:release-${arch}"] - platforms = ["linux/${arch}"] + tags = ["${SETUP_WIZARD_IMAGE}:release"] } diff --git a/image/docker-bake.push.hcl b/image/docker-bake.push.hcl new file mode 100644 index 000000000..170796901 --- /dev/null +++ b/image/docker-bake.push.hcl @@ -0,0 +1,56 @@ +# push targets, push by digests +target "_common-push" { + output = [{ + type = "registry", + push-by-digest = true + }] +} + +target "base-runtime-deps" { + inherits = ["_common-push"] + tags = [BASE_IMAGE] +} +target "base-build-deps" { + inherits = ["_common-push"] + tags = [BASE_IMAGE] +} +target "base-slim" { + name = "base-slim-${branch}" + matrix = { + branch = ["main", "stable"] + } + inherits = ["_common-push"] + tags = [BASE_IMAGE] +} +target "base-web-only" { + name = "base-web-only-${branch}" + matrix = { + branch = ["main", "stable"] + } + inherits = ["_common-push"] + tags = [BASE_IMAGE] +} +target "base-release" { + name = "base-release-${branch}" + matrix = { + branch = ["main", "stable"] + } + inherits = ["_common-push"] + tags = [BASE_IMAGE] +} +target "dev" { + inherits = ["dev", "_common-push"] + tags = [DEV_IMAGE] +} +target "test" { + name = "test-${tag}" + matrix = { + tag = ["slim", "slim-browsers", "release"] + } + inherits = ["_common-push"] + tags = [TEST_IMAGE] +} +target "setup-wizard" { + inherits = ["_common-push"] + tags = [SETUP_WIZARD_IMAGE] +} From 5cc4f095b4efdb7bd105577e021390f6c2dfa147 Mon Sep 17 00:00:00 2001 From: Gaurav Tiwari Date: Thu, 13 Aug 2026 10:23:12 +0100 Subject: [PATCH 06/14] Wire package caches into the upstream cache build --- image/base/Dockerfile | 7 ++++--- image/discourse_test/Dockerfile | 9 ++++++++- image/docker-bake.cache.hcl | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/image/base/Dockerfile b/image/base/Dockerfile index 2dc39707d..4a59dddd2 100644 --- a/image/base/Dockerfile +++ b/image/base/Dockerfile @@ -22,7 +22,7 @@ DEBIAN_FRONTEND=noninteractive apt-get -y install wget \ libbrotli-dev FROM builder AS libheif-builder -ADD install-libheif /tmp/install-libheif +COPY install-libheif /tmp/install-libheif RUN /tmp/install-libheif FROM libheif-builder AS imagemagick_builder @@ -30,7 +30,7 @@ COPY install-imagemagick /tmp/install-imagemagick RUN /tmp/install-imagemagick FROM libheif-builder AS jpegli-builder -ADD install-jpegli /tmp/install-jpegli +COPY install-jpegli /tmp/install-jpegli RUN /tmp/install-jpegli FROM libheif-builder AS vips-builder @@ -266,7 +266,8 @@ RUN --mount=type=cache,id=discourse-bundler-${DISCOURSE_BRANCH},target=/home/dis find /var/www/discourse/vendor/bundle -name cache -not -path '*/gems/*' -type d -exec rm -rf {} + &&\ find /var/www/discourse/vendor/bundle -name tmp -type d -exec rm -rf {} + -RUN cd /var/www/discourse &&\ +RUN --mount=type=cache,id=discourse-pnpm-${DISCOURSE_BRANCH},target=/home/discourse/.local/share/pnpm/store,sharing=locked,uid=1000,gid=1000 \ + cd /var/www/discourse &&\ sudo -u discourse /bin/bash -c 'pnpm install --frozen-lockfile' FROM discourse-web-only AS discourse-release diff --git a/image/discourse_test/Dockerfile b/image/discourse_test/Dockerfile index 1d4b23ee2..e61ec5e56 100644 --- a/image/discourse_test/Dockerfile +++ b/image/discourse_test/Dockerfile @@ -43,11 +43,18 @@ RUN /tmp/install-chrome &&\ FROM with_browsers AS release -RUN --mount=type=cache,id=discourse-test-bundler,target=/home/discourse/.bundle/cache,sharing=locked,uid=1000,gid=1000 \ +RUN --mount=type=cache,id=discourse-bundler-main,target=/home/discourse/.bundle/cache,sharing=locked,uid=1000,gid=1000 \ cd /var/www/discourse &&\ + echo "Test Bundler cache before install:" &&\ + sudo -u discourse du -sh /home/discourse/.bundle/cache &&\ sudo -u discourse bundle config --local deployment true &&\ sudo -u discourse bundle config --local path ./vendor/bundle &&\ sudo -u discourse env BUNDLE_GLOBAL_GEM_CACHE=true BUNDLE_USER_CACHE=/home/discourse/.bundle/cache bundle install --jobs $(nproc --ignore=1) &&\ + echo "Test Bundler cache after install:" &&\ + sudo -u discourse du -sh /home/discourse/.bundle/cache + +RUN --mount=type=cache,id=discourse-pnpm-main,target=/home/discourse/.local/share/pnpm/store,sharing=locked,uid=1000,gid=1000 \ + cd /var/www/discourse &&\ sudo -E -u discourse -H /bin/bash -c 'CI=1 pnpm install' RUN cd /var/www/discourse && sudo -E -u discourse -H bundle exec rake plugin:install_all_official &&\ diff --git a/image/docker-bake.cache.hcl b/image/docker-bake.cache.hcl index 4335a64c2..e039e227f 100644 --- a/image/docker-bake.cache.hcl +++ b/image/docker-bake.cache.hcl @@ -7,7 +7,7 @@ variable "CACHE_IMAGE" { } variable "ARCH" { - deafault = "" + default = "" validation { condition = ARCH != "" error_message = "The variable 'ARCH' must not be empty." From f551d332451ed9794ce27a112dbbca3025a6eafc Mon Sep 17 00:00:00 2001 From: Gaurav Tiwari Date: Thu, 13 Aug 2026 10:39:48 +0100 Subject: [PATCH 07/14] Remove the stale Redis installer copy --- image/base/Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/image/base/Dockerfile b/image/base/Dockerfile index 4a59dddd2..23f0b6cce 100644 --- a/image/base/Dockerfile +++ b/image/base/Dockerfile @@ -190,7 +190,6 @@ COPY --from=thpoff-builder /usr/local/sbin/thpoff /usr/local/sbin COPY --from=jemalloc-builder /usr/lib/libjemalloc.so /usr/lib COPY --from=oxipng-builder /usr/local/bin/jhead /usr/local/bin COPY --from=oxipng-builder /usr/local/bin/oxipng /usr/local/bin -COPY install-redis /tmp/install-redis # version check: https://rubygems.org/gems/pups RUN cd /tmp &&\ From 9f2852fa33445014b8fb4497a7d08e0a9bfe681c Mon Sep 17 00:00:00 2001 From: Gaurav Tiwari Date: Thu, 13 Aug 2026 11:17:23 +0100 Subject: [PATCH 08/14] Cache complete package-manager state --- image/base/Dockerfile | 22 ++++++++++++++-------- image/discourse_test/Dockerfile | 20 +++++++++++++------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/image/base/Dockerfile b/image/base/Dockerfile index 23f0b6cce..2a5e6438e 100644 --- a/image/base/Dockerfile +++ b/image/base/Dockerfile @@ -252,20 +252,26 @@ RUN install -dm 0755 -o discourse -g discourse /var/www/discourse &&\ FROM discourse-slim AS discourse-web-only ENV RAILS_ENV=production -RUN --mount=type=cache,id=discourse-bundler-${DISCOURSE_BRANCH},target=/home/discourse/.bundle/cache,sharing=locked,uid=1000,gid=1000 \ +RUN --mount=type=cache,id=discourse-bundle-${DISCOURSE_BRANCH},target=/home/discourse/.cache/bundle,sharing=locked,uid=1000,gid=1000 \ cd /var/www/discourse &&\ - echo "Bundler cache before install:" &&\ - sudo -u discourse du -sh /home/discourse/.bundle/cache &&\ + echo "Installed bundle cache before install:" &&\ + sudo -u discourse du -sh /home/discourse/.cache/bundle &&\ sudo -u discourse bundle config --local deployment true &&\ - sudo -u discourse bundle config --local path ./vendor/bundle &&\ + sudo -u discourse bundle config --local path /home/discourse/.cache/bundle &&\ sudo -u discourse bundle config --local without test development &&\ - sudo -u discourse env BUNDLE_GLOBAL_GEM_CACHE=true BUNDLE_USER_CACHE=/home/discourse/.bundle/cache bundle install --jobs $(nproc --ignore=1) &&\ - echo "Bundler cache after install:" &&\ - sudo -u discourse du -sh /home/discourse/.bundle/cache &&\ + sudo -u discourse bundle install --jobs $(nproc --ignore=1) &&\ + echo "Installed bundle cache after install:" &&\ + sudo -u discourse du -sh /home/discourse/.cache/bundle &&\ + rm -rf /var/www/discourse/vendor/bundle &&\ + install -dm 0755 -o discourse -g discourse /var/www/discourse/vendor/bundle &&\ + cp -a /home/discourse/.cache/bundle/. /var/www/discourse/vendor/bundle/ &&\ + sudo -u discourse bundle config --local path ./vendor/bundle &&\ + sudo -u discourse bundle check &&\ find /var/www/discourse/vendor/bundle -name cache -not -path '*/gems/*' -type d -exec rm -rf {} + &&\ find /var/www/discourse/vendor/bundle -name tmp -type d -exec rm -rf {} + -RUN --mount=type=cache,id=discourse-pnpm-${DISCOURSE_BRANCH},target=/home/discourse/.local/share/pnpm/store,sharing=locked,uid=1000,gid=1000 \ +RUN install -dm 0755 -o discourse -g discourse /home/discourse/.local/share/pnpm +RUN --mount=type=cache,id=discourse-pnpm-${DISCOURSE_BRANCH},target=/home/discourse/.local/share/pnpm,sharing=locked,uid=1000,gid=1000 \ cd /var/www/discourse &&\ sudo -u discourse /bin/bash -c 'pnpm install --frozen-lockfile' diff --git a/image/discourse_test/Dockerfile b/image/discourse_test/Dockerfile index e61ec5e56..ec1b6eb08 100644 --- a/image/discourse_test/Dockerfile +++ b/image/discourse_test/Dockerfile @@ -43,17 +43,23 @@ RUN /tmp/install-chrome &&\ FROM with_browsers AS release -RUN --mount=type=cache,id=discourse-bundler-main,target=/home/discourse/.bundle/cache,sharing=locked,uid=1000,gid=1000 \ +RUN --mount=type=cache,id=discourse-bundle-test-main,target=/home/discourse/.cache/bundle,sharing=locked,uid=1000,gid=1000 \ cd /var/www/discourse &&\ - echo "Test Bundler cache before install:" &&\ - sudo -u discourse du -sh /home/discourse/.bundle/cache &&\ + echo "Test installed bundle cache before install:" &&\ + sudo -u discourse du -sh /home/discourse/.cache/bundle &&\ sudo -u discourse bundle config --local deployment true &&\ + sudo -u discourse bundle config --local path /home/discourse/.cache/bundle &&\ + sudo -u discourse bundle install --jobs $(nproc --ignore=1) &&\ + echo "Test installed bundle cache after install:" &&\ + sudo -u discourse du -sh /home/discourse/.cache/bundle &&\ + rm -rf /var/www/discourse/vendor/bundle &&\ + install -dm 0755 -o discourse -g discourse /var/www/discourse/vendor/bundle &&\ + cp -a /home/discourse/.cache/bundle/. /var/www/discourse/vendor/bundle/ &&\ sudo -u discourse bundle config --local path ./vendor/bundle &&\ - sudo -u discourse env BUNDLE_GLOBAL_GEM_CACHE=true BUNDLE_USER_CACHE=/home/discourse/.bundle/cache bundle install --jobs $(nproc --ignore=1) &&\ - echo "Test Bundler cache after install:" &&\ - sudo -u discourse du -sh /home/discourse/.bundle/cache + sudo -u discourse bundle check -RUN --mount=type=cache,id=discourse-pnpm-main,target=/home/discourse/.local/share/pnpm/store,sharing=locked,uid=1000,gid=1000 \ +RUN install -dm 0755 -o discourse -g discourse /home/discourse/.local/share/pnpm +RUN --mount=type=cache,id=discourse-pnpm-main,target=/home/discourse/.local/share/pnpm,sharing=locked,uid=1000,gid=1000 \ cd /var/www/discourse &&\ sudo -E -u discourse -H /bin/bash -c 'CI=1 pnpm install' From f0152c829ccb9eb1e9cab6f06c5658d3d6912fbf Mon Sep 17 00:00:00 2001 From: Gaurav Tiwari Date: Thu, 13 Aug 2026 11:19:53 +0100 Subject: [PATCH 09/14] Verify materialized bundles with a final install --- image/base/Dockerfile | 1 + image/discourse_test/Dockerfile | 1 + 2 files changed, 2 insertions(+) diff --git a/image/base/Dockerfile b/image/base/Dockerfile index 2a5e6438e..d8b4109b5 100644 --- a/image/base/Dockerfile +++ b/image/base/Dockerfile @@ -266,6 +266,7 @@ RUN --mount=type=cache,id=discourse-bundle-${DISCOURSE_BRANCH},target=/home/disc install -dm 0755 -o discourse -g discourse /var/www/discourse/vendor/bundle &&\ cp -a /home/discourse/.cache/bundle/. /var/www/discourse/vendor/bundle/ &&\ sudo -u discourse bundle config --local path ./vendor/bundle &&\ + sudo -u discourse bundle install --jobs $(nproc --ignore=1) &&\ sudo -u discourse bundle check &&\ find /var/www/discourse/vendor/bundle -name cache -not -path '*/gems/*' -type d -exec rm -rf {} + &&\ find /var/www/discourse/vendor/bundle -name tmp -type d -exec rm -rf {} + diff --git a/image/discourse_test/Dockerfile b/image/discourse_test/Dockerfile index ec1b6eb08..3041e52d2 100644 --- a/image/discourse_test/Dockerfile +++ b/image/discourse_test/Dockerfile @@ -56,6 +56,7 @@ RUN --mount=type=cache,id=discourse-bundle-test-main,target=/home/discourse/.cac install -dm 0755 -o discourse -g discourse /var/www/discourse/vendor/bundle &&\ cp -a /home/discourse/.cache/bundle/. /var/www/discourse/vendor/bundle/ &&\ sudo -u discourse bundle config --local path ./vendor/bundle &&\ + sudo -u discourse bundle install --jobs $(nproc --ignore=1) &&\ sudo -u discourse bundle check RUN install -dm 0755 -o discourse -g discourse /home/discourse/.local/share/pnpm From ea18a068f167b981d4d97242c8927826421ad422 Mon Sep 17 00:00:00 2001 From: Gaurav Tiwari Date: Thu, 13 Aug 2026 11:57:25 +0100 Subject: [PATCH 10/14] Run cache comparison in the Discourse fork --- .boringcache.toml | 19 ++ .../actions/discourse-cache-build/action.yml | 248 ++++++++++++++++++ .github/boringcache/README.md | 28 ++ .../workflows/boringcache-container-cache.yml | 181 +++++++++++++ image/base/Dockerfile | 46 +++- image/discourse_test/Dockerfile | 1 + scripts/configure-benchmark-cache-scope.py | 79 ++++++ 7 files changed, 594 insertions(+), 8 deletions(-) create mode 100644 .boringcache.toml create mode 100644 .github/actions/discourse-cache-build/action.yml create mode 100644 .github/boringcache/README.md create mode 100644 .github/workflows/boringcache-container-cache.yml create mode 100644 scripts/configure-benchmark-cache-scope.py diff --git a/.boringcache.toml b/.boringcache.toml new file mode 100644 index 000000000..6edb36e59 --- /dev/null +++ b/.boringcache.toml @@ -0,0 +1,19 @@ +workspace = "discourse/discourse-docker" + +[proxy] +metadata-hints = ["project=discourse-docker"] + +[adapters.docker] +tag = "discourse-docker-layer-ccache-mount" +cache-mode = "max" +no-platform = true +no-git = true +fail-on-cache-error = true +metadata-hints = ["upstream=discourse", "lane=layer-ccache-mount"] + +[adapters.ccache] +tag = "discourse-docker-ccache" +no-platform = true +no-git = true +fail-on-cache-error = true +metadata-hints = ["upstream=discourse", "lane=layer-ccache-mount", "tool=ccache"] diff --git a/.github/actions/discourse-cache-build/action.yml b/.github/actions/discourse-cache-build/action.yml new file mode 100644 index 000000000..ed8ea2327 --- /dev/null +++ b/.github/actions/discourse-cache-build/action.yml @@ -0,0 +1,248 @@ +name: Run one Discourse cache comparison phase +description: Build Discourse's upstream image-factory graph with one isolated cache lane. +inputs: + strategy: {required: true} + phase: {required: true} + source-sha: {required: true} + source-datestamp: {required: true} + architecture: {required: true} +runs: + using: composite + steps: + - name: Verify immutable benchmark inputs + shell: bash + env: + SOURCE_SHA: ${{ inputs.source-sha }} + SOURCE_DATESTAMP: ${{ inputs.source-datestamp }} + run: | + set -euo pipefail + [[ "$SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]] || { echo "source-sha must be a full commit SHA" >&2; exit 1; } + [[ "$SOURCE_DATESTAMP" =~ ^[0-9]{8}$ ]] || { echo "source-datestamp must use YYYYMMDD" >&2; exit 1; } + + - name: Select isolated cache scopes + id: scope + shell: bash + env: + STRATEGY: ${{ inputs.strategy }} + ARCHITECTURE: ${{ inputs.architecture }} + run: | + set -euo pipefail + # Seed and rebuild share a workflow run and therefore the same cohort. + # A new dispatch receives fresh cache scopes; reruns keep their seed. + cohort="discourse-r${GITHUB_RUN_ID}-${STRATEGY}-${ARCHITECTURE}" + echo "docker_tag=${cohort}-docker" >> "$GITHUB_OUTPUT" + echo "ccache_tag=${cohort}-ccache" >> "$GITHUB_OUTPUT" + + - name: Configure the isolated BoringCache plan + if: startsWith(inputs.strategy, 'boringcache') + shell: bash + run: | + python3 ./scripts/configure-benchmark-cache-scope.py .boringcache.toml \ + --docker-tag "${{ steps.scope.outputs.docker_tag }}" \ + --ccache-tag "${{ steps.scope.outputs.ccache_tag }}" + + - name: Set up Buildx for GitHub Actions cache + if: inputs.strategy == 'gha' + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + + - name: Set up BoringCache for the upstream Bake graph + if: startsWith(inputs.strategy, 'boringcache') + uses: boringcache/one@e24257b122813ad11d53b9ed024b474ca4946ad2 # v1.19.1 + with: + cli-version: v1.19.1 + trust-policy: restore + setup: none + mode: docker + working-directory: image + docker-command: setup + diagnostics: summary + verify: none + fail-on-cache-error: true + metadata-hints: benchmark=discourse-docker,phase=${{ inputs.phase }},architecture=${{ inputs.architecture }},variant=${{ inputs.strategy }} + env: + BORINGCACHE_RESTORE_TOKEN: ${{ env.BORINGCACHE_RESTORE_TOKEN }} + BORINGCACHE_SAVE_TOKEN: "" + + - name: Start image-factory timer + id: timer + shell: bash + run: echo "started_at=$(date +%s)" >> "$GITHUB_OUTPUT" + + - name: Build with GitHub Actions cache + if: inputs.strategy == 'gha' + shell: bash + working-directory: image + env: + ARCH: ${{ inputs.architecture }} + DATESTAMP: ${{ inputs.source-datestamp }} + DISCOURSE_REF: ${{ inputs.source-sha }} + CACHE_SCOPE: ${{ steps.scope.outputs.docker_tag }} + run: | + set -euo pipefail + targets=( + base-runtime-deps + base-slim-main + base-slim-stable + base-web-only-main + base-web-only-stable + base-release-main + base-release-stable + test-release + ) + for target in "${targets[@]}"; do + scope="${CACHE_SCOPE}-${target}" + args=( + "$target" + --load + "--set=${target}.cache-from=type=gha,scope=${scope}" + "--set=${target}.cache-to=type=gha,scope=${scope},mode=max" + ) + if [[ "$target" == "test-release" ]]; then + args+=("--set=base-slim-main.cache-from=type=gha,scope=${CACHE_SCOPE}-base-slim-main") + fi + docker buildx bake "${args[@]}" + done + + - name: Build with BoringCache layer cache + if: inputs.strategy == 'boringcache' + shell: bash + working-directory: image + env: + ARCH: ${{ inputs.architecture }} + DATESTAMP: ${{ inputs.source-datestamp }} + DISCOURSE_REF: ${{ inputs.source-sha }} + RUST_LOG: warn + run: | + set -euo pipefail + targets=( + base-runtime-deps + base-slim-main + base-slim-stable + base-web-only-main + base-web-only-stable + base-release-main + base-release-stable + test-release + ) + for target in "${targets[@]}"; do + boringcache docker \ + --fail-on-cache-error \ + -- docker buildx bake "$target" --load + done + + - name: Build with BoringCache layer, ccache, and mountcache + if: inputs.strategy == 'boringcache-optimized' + shell: bash + working-directory: image + env: + ARCH: ${{ inputs.architecture }} + DATESTAMP: ${{ inputs.source-datestamp }} + DISCOURSE_REF: ${{ inputs.source-sha }} + RUST_LOG: warn + run: | + set -euo pipefail + targets=( + base-runtime-deps + base-slim-main + base-slim-stable + base-web-only-main + base-web-only-stable + base-release-main + base-release-stable + test-release + ) + for target in "${targets[@]}"; do + boringcache docker \ + --fail-on-cache-error \ + --mount-cache \ + --tool-cache ccache \ + -- docker buildx bake "$target" --load + done + + - name: Capture image-factory timing + id: timing + shell: bash + env: + STARTED_AT: ${{ steps.timer.outputs.started_at }} + run: echo "seconds=$(( $(date +%s) - STARTED_AT ))" >> "$GITHUB_OUTPUT" + + - name: Write phase evidence + shell: bash + env: + STRATEGY: ${{ inputs.strategy }} + PHASE: ${{ inputs.phase }} + ARCHITECTURE: ${{ inputs.architecture }} + SOURCE_SHA: ${{ inputs.source-sha }} + BUILD_SECONDS: ${{ steps.timing.outputs.seconds }} + DOCKER_TAG: ${{ steps.scope.outputs.docker_tag }} + CCACHE_TAG: ${{ steps.scope.outputs.ccache_tag }} + run: | + set -euo pipefail + mkdir -p benchmark-results + jq -n \ + --arg strategy "$STRATEGY" \ + --arg phase "$PHASE" \ + --arg architecture "$ARCHITECTURE" \ + --arg source_sha "$SOURCE_SHA" \ + --arg docker_tag "$DOCKER_TAG" \ + --arg ccache_tag "$CCACHE_TAG" \ + --argjson build_seconds "$BUILD_SECONDS" \ + '{ + schema_version: 1, + strategy: $strategy, + phase: $phase, + architecture: $architecture, + source_sha: $source_sha, + build_seconds: $build_seconds, + docker_cache_tag: $docker_tag, + ccache_tag: (if $strategy == "boringcache-optimized" then $ccache_tag else null end), + github: { + repository: env.GITHUB_REPOSITORY, + run_id: env.GITHUB_RUN_ID, + run_attempt: env.GITHUB_RUN_ATTEMPT + } + }' > "benchmark-results/${PHASE}-${STRATEGY}-${ARCHITECTURE}.json" + + - name: Run upstream image specs + shell: bash + env: + SOURCE_SHA: ${{ inputs.source-sha }} + run: | + timeout --foreground --signal=TERM --kill-after=1m 45m \ + docker run --rm \ + -e CI=true \ + -e RUBY_ONLY=1 \ + -e USE_TURBO=1 \ + -e SKIP_PLUGINS=1 \ + -e SKIP_LINT=1 \ + -e DISCOURSE_TURBO_RSPEC_RETRY_AND_LOG_FLAKY_TESTS=1 \ + -e COMMIT_HASH="$SOURCE_SHA" \ + local_discourse/discourse_test:release + + - name: Publish phase summary + if: always() + shell: bash + env: + STRATEGY: ${{ inputs.strategy }} + PHASE: ${{ inputs.phase }} + ARCHITECTURE: ${{ inputs.architecture }} + SOURCE_SHA: ${{ inputs.source-sha }} + BUILD_SECONDS: ${{ steps.timing.outputs.seconds }} + DOCKER_TAG: ${{ steps.scope.outputs.docker_tag }} + CCACHE_TAG: ${{ steps.scope.outputs.ccache_tag }} + run: | + { + echo "### Discourse image factory" + echo + echo "| Field | Value |" + echo "| --- | --- |" + echo "| Lane | $STRATEGY |" + echo "| Phase | $PHASE |" + echo "| Architecture | $ARCHITECTURE |" + echo "| Discourse source | \`$SOURCE_SHA\` |" + echo "| Timed build | ${BUILD_SECONDS}s |" + echo "| Layer cache cohort | \`$DOCKER_TAG\` |" + if [[ "$STRATEGY" == "boringcache-optimized" ]]; then + echo "| ccache cohort | \`$CCACHE_TAG\` |" + fi + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/boringcache/README.md b/.github/boringcache/README.md new file mode 100644 index 000000000..1eb5cc0b0 --- /dev/null +++ b/.github/boringcache/README.md @@ -0,0 +1,28 @@ +# Discourse container cache comparison + +This branch runs Discourse's own image factory in `boringcache/discourse_docker`. +It keeps the upstream target-by-target Bake order and uses native GitHub-hosted +runners: `ubuntu-24.04` for AMD64 and `ubuntu-24.04-arm` for ARM64. + +Every architecture builds the same Dockerfiles through three isolated lanes: + +1. GitHub Actions layer cache. +2. BoringCache layer cache. +3. BoringCache layer cache plus ccache tool cache and BuildKit mountcache. + +The shared Dockerfile owns the ccache 4.13.6 compiler launchers. Only the third +lane injects BoringCache's remote ccache settings. The Bundler mount contains +the complete installed bundle, not only downloaded gem archives. Each build +materializes it into `vendor/bundle`, runs a normal `bundle install` there to +repair or install anything missing, and then uses `bundle check` as a final +verification. The pnpm mounts cover its complete writable home. + +The seed uses Discourse commit +`eedf0ac2344c37d66a2c9ab05dc8a83bf3efd9bb`. The rebuild uses its immediate +child, `763655f6faf47b088afee1a59e2d97cec5886c97`. Their dependency manifests +are identical, while the date transition exercises Discourse's native base +image refresh and the changed source ref exercises Bundler and pnpm mounts. + +The benchmark keeps the Mozilla signing-key correction from +`a68d4b8707fd653697e8b6b27b336d093dbed5e4` and runs the upstream image specs +outside the timed build with `CI=true`. diff --git a/.github/workflows/boringcache-container-cache.yml b/.github/workflows/boringcache-container-cache.yml new file mode 100644 index 000000000..9df33d5b6 --- /dev/null +++ b/.github/workflows/boringcache-container-cache.yml @@ -0,0 +1,181 @@ +name: Container cache comparison +run-name: Discourse image factory | seed and rebuild | ${{ github.ref_name }} + +on: + push: + branches: + - agent/benchmark-cache-controls + workflow_dispatch: + +permissions: + actions: write + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +env: + BORINGCACHE_RESTORE_TOKEN: ${{ secrets.BORINGCACHE_RESTORE_TOKEN }} + BORINGCACHE_SAVE_TOKEN: ${{ secrets.BORINGCACHE_SAVE_TOKEN }} + BUILDKIT_PROGRESS: plain + DOCKER_BUILD_RECORD_UPLOAD: "false" + DOCKER_BUILD_SUMMARY: "false" + SEED_SOURCE_SHA: eedf0ac2344c37d66a2c9ab05dc8a83bf3efd9bb + SEED_DATESTAMP: "20260811" + REBUILD_SOURCE_SHA: 763655f6faf47b088afee1a59e2d97cec5886c97 + REBUILD_DATESTAMP: "20260812" + +jobs: + validate: + name: Verify benchmark contract + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout the fork with its history + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Checkout the exact Discourse rebuild source + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + repository: discourse/discourse + ref: ${{ env.REBUILD_SOURCE_SHA }} + path: benchmark-source + fetch-depth: 2 + persist-credentials: false + + - name: Verify source window and cache wiring + run: | + set -euo pipefail + git -C benchmark-source cat-file -e "${SEED_SOURCE_SHA}^{commit}" + git -C benchmark-source merge-base --is-ancestor "$SEED_SOURCE_SHA" "$REBUILD_SOURCE_SHA" + [[ "$(git -C benchmark-source rev-list --count "${SEED_SOURCE_SHA}..${REBUILD_SOURCE_SHA}")" == 1 ]] + git -C benchmark-source diff --quiet "$SEED_SOURCE_SHA" "$REBUILD_SOURCE_SHA" -- \ + Gemfile Gemfile.lock package.json pnpm-lock.yaml + + mozilla_fix=a68d4b8707fd653697e8b6b27b336d093dbed5e4 + git cat-file -e "${mozilla_fix}^{commit}" + git merge-base --is-ancestor "$mozilla_fix" HEAD + + grep -Fq 'target=/home/discourse/.cache/bundle,sharing=locked,uid=1000,gid=1000' image/base/Dockerfile + grep -Fq 'cp -a /home/discourse/.cache/bundle/. /var/www/discourse/vendor/bundle/' image/base/Dockerfile + grep -Fq 'bundle config --local path ./vendor/bundle' image/base/Dockerfile + # shellcheck disable=SC2016 + [[ "$(grep -Fc 'sudo -u discourse bundle install --jobs $(nproc --ignore=1)' image/base/Dockerfile)" -eq 2 ]] + grep -Fq 'target=/home/discourse/.local/share/pnpm,sharing=locked,uid=1000,gid=1000' image/base/Dockerfile + # shellcheck disable=SC2016 + grep -Fq 'ccache version ${CCACHE_VERSION}' image/base/Dockerfile + + seed: + name: Seed | ${{ matrix.lane.label }} | ${{ matrix.architecture.name }} + needs: validate + runs-on: ${{ matrix.architecture.runner }} + timeout-minutes: 180 + strategy: &comparison-matrix + fail-fast: false + matrix: + lane: + - {label: GitHub Actions, strategy: gha} + - {label: BoringCache layer, strategy: boringcache} + - {label: BoringCache layer + ccache + mountcache, strategy: boringcache-optimized} + architecture: + - {name: amd64, runner: ubuntu-24.04} + - {name: arm64, runner: ubuntu-24.04-arm} + steps: + - name: Checkout fork benchmark branch + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: Build seed source + uses: ./.github/actions/discourse-cache-build + with: + strategy: ${{ matrix.lane.strategy }} + phase: seed + source-sha: ${{ env.SEED_SOURCE_SHA }} + source-datestamp: ${{ env.SEED_DATESTAMP }} + architecture: ${{ matrix.architecture.name }} + + - name: Upload seed evidence + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.2.0 + with: + name: phase-seed-${{ matrix.lane.strategy }}-${{ matrix.architecture.name }} + path: benchmark-results/ + if-no-files-found: warn + retention-days: 30 + + rebuild: + name: Rebuild | ${{ matrix.lane.label }} | ${{ matrix.architecture.name }} + if: ${{ always() && !contains(fromJSON('["cancelled", "skipped"]'), needs.seed.result) }} + needs: seed + runs-on: ${{ matrix.architecture.runner }} + timeout-minutes: 180 + strategy: *comparison-matrix + steps: + - name: Checkout fork benchmark branch + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: Build rebuild source + uses: ./.github/actions/discourse-cache-build + with: + strategy: ${{ matrix.lane.strategy }} + phase: rebuild + source-sha: ${{ env.REBUILD_SOURCE_SHA }} + source-datestamp: ${{ env.REBUILD_DATESTAMP }} + architecture: ${{ matrix.architecture.name }} + + - name: Upload rebuild evidence + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.2.0 + with: + name: phase-rebuild-${{ matrix.lane.strategy }}-${{ matrix.architecture.name }} + path: benchmark-results/ + if-no-files-found: warn + retention-days: 30 + + report: + name: Comparison report + if: ${{ always() && needs.rebuild.result != 'cancelled' }} + needs: rebuild + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Download phase evidence + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: phase-* + path: phase-evidence + + - name: Build comparison report + run: | + set -euo pipefail + mkdir -p benchmark-results + find phase-evidence -name '*.json' -print0 | sort -z | xargs -0 jq -s 'sort_by(.architecture, .strategy, .phase)' > benchmark-results/comparison.json + { + echo "## Discourse container cache comparison" + echo + echo "| Architecture | Lane | Seed | Rebuild | Change |" + echo "| --- | --- | ---: | ---: | ---: |" + jq -r ' + group_by([.architecture, .strategy])[] | + (map(select(.phase == "seed"))[0]) as $seed | + (map(select(.phase == "rebuild"))[0]) as $rebuild | + if $seed and $rebuild then + "| \($seed.architecture) | \($seed.strategy) | \($seed.build_seconds)s | \($rebuild.build_seconds)s | \((100 * ($seed.build_seconds - $rebuild.build_seconds) / $seed.build_seconds) | floor)% faster |" + else empty end + ' benchmark-results/comparison.json + } | tee -a "$GITHUB_STEP_SUMMARY" + + - name: Upload comparison + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.2.0 + with: + name: discourse-container-cache-comparison + path: benchmark-results/ + if-no-files-found: error + retention-days: 30 diff --git a/image/base/Dockerfile b/image/base/Dockerfile index d8b4109b5..df129d96c 100644 --- a/image/base/Dockerfile +++ b/image/base/Dockerfile @@ -15,28 +15,51 @@ RUN apt update &&\ DEBIAN_FRONTEND=noninteractive apt-get -y install wget \ autoconf build-essential \ git \ + ccache \ cmake \ gnupg \ libpcre2-dev \ libfreetype-dev \ libbrotli-dev +# BoringCache v1.19.1 targets ccache 4.13.6's @-attribute syntax. Keep +# Debian's compiler launchers, but use the same released ccache binary on both +# native runner architectures. +ARG CCACHE_VERSION=4.13.6 +RUN set -eux; \ + architecture="$(dpkg --print-architecture)"; \ + case "$architecture" in \ + amd64) release_arch=x86_64; checksum=567b1b648411819590f918f045218c92da14418bdec3b30db94a3b4f5d77cf13 ;; \ + arm64) release_arch=aarch64; checksum=fae67fb810e1f0d390409af6603355483572229e19183e68574cd0f851a6fb98 ;; \ + *) echo "Unsupported ccache architecture: $architecture" >&2; exit 1 ;; \ + esac; \ + archive="ccache-${CCACHE_VERSION}-linux-${release_arch}-glibc.tar.gz"; \ + wget --quiet --tries=8 "https://github.com/ccache/ccache/releases/download/v${CCACHE_VERSION}/${archive}" -O "/tmp/${archive}"; \ + echo "${checksum} /tmp/${archive}" | sha256sum --check; \ + tar xzf "/tmp/${archive}" -C /tmp; \ + install -m 0755 "/tmp/ccache-${CCACHE_VERSION}-linux-${release_arch}-glibc/ccache" /usr/bin/ccache; \ + ccache --version | grep -F "ccache version ${CCACHE_VERSION}"; \ + rm -rf "/tmp/${archive}" "/tmp/ccache-${CCACHE_VERSION}-linux-${release_arch}-glibc" + +ENV PATH="/usr/lib/ccache:${PATH}" \ + CCACHE_COMPILERCHECK=content + FROM builder AS libheif-builder COPY install-libheif /tmp/install-libheif -RUN /tmp/install-libheif +RUN /tmp/install-libheif && ccache --show-stats FROM libheif-builder AS imagemagick_builder COPY install-imagemagick /tmp/install-imagemagick -RUN /tmp/install-imagemagick +RUN /tmp/install-imagemagick && ccache --show-stats FROM libheif-builder AS jpegli-builder COPY install-jpegli /tmp/install-jpegli -RUN /tmp/install-jpegli +RUN /tmp/install-jpegli && ccache --show-stats FROM libheif-builder AS vips-builder COPY --from=jpegli-builder /usr/local/lib/jpegli /usr/local/lib/jpegli COPY install-vips /tmp/install-vips -RUN /tmp/install-vips +RUN /tmp/install-vips && ccache --show-stats FROM builder AS nginx-builder # From https://nginx.org/en/pgp_keys.html @@ -44,22 +67,23 @@ COPY nginx_public_keys.key /tmp/nginx_public_keys.key COPY install-nginx /tmp/install-nginx RUN gpg --import /tmp/nginx_public_keys.key &&\ rm /tmp/nginx_public_keys.key &&\ - /tmp/install-nginx + /tmp/install-nginx &&\ + ccache --show-stats FROM builder AS thpoff-builder # This tool allows us to disable huge page support for our current process # since the flag is preserved through forks and execs it can be used on any # process COPY thpoff.c /src/thpoff.c -RUN gcc -o /usr/local/sbin/thpoff /src/thpoff.c && rm /src/thpoff.c +RUN gcc -o /usr/local/sbin/thpoff /src/thpoff.c && rm /src/thpoff.c && ccache --show-stats FROM builder AS jemalloc-builder COPY install-jemalloc /tmp/install-jemalloc -RUN /tmp/install-jemalloc +RUN /tmp/install-jemalloc && ccache --show-stats FROM builder AS oxipng-builder COPY install-oxipng /tmp/install-oxipng -RUN /tmp/install-oxipng +RUN /tmp/install-oxipng && ccache --show-stats FROM discourse/ruby:${FROM_DOCKER_IMAGE_TAG} AS discourse-runtime-base ARG DATESTAMP @@ -208,6 +232,11 @@ COPY etc/ /etc COPY sbin/ /sbin FROM discourse-runtime-base AS discourse-build-base +COPY --from=builder /usr/bin/ccache /usr/bin/ccache +COPY --from=builder /usr/lib/ccache /usr/lib/ccache +ENV PATH="/usr/lib/ccache:${PATH}" \ + CCACHE_COMPILERCHECK=content + # From https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key # fingerprint: 6F71 F525 2828 41EE DAF8 51B4 2F59 B5F9 9B1B E0B4 COPY nodesource-repo.gpg.key /usr/share/keyrings/nodesource.asc @@ -260,6 +289,7 @@ RUN --mount=type=cache,id=discourse-bundle-${DISCOURSE_BRANCH},target=/home/disc sudo -u discourse bundle config --local path /home/discourse/.cache/bundle &&\ sudo -u discourse bundle config --local without test development &&\ sudo -u discourse bundle install --jobs $(nproc --ignore=1) &&\ + ccache --show-stats &&\ echo "Installed bundle cache after install:" &&\ sudo -u discourse du -sh /home/discourse/.cache/bundle &&\ rm -rf /var/www/discourse/vendor/bundle &&\ diff --git a/image/discourse_test/Dockerfile b/image/discourse_test/Dockerfile index 3041e52d2..7248489e7 100644 --- a/image/discourse_test/Dockerfile +++ b/image/discourse_test/Dockerfile @@ -50,6 +50,7 @@ RUN --mount=type=cache,id=discourse-bundle-test-main,target=/home/discourse/.cac sudo -u discourse bundle config --local deployment true &&\ sudo -u discourse bundle config --local path /home/discourse/.cache/bundle &&\ sudo -u discourse bundle install --jobs $(nproc --ignore=1) &&\ + ccache --show-stats &&\ echo "Test installed bundle cache after install:" &&\ sudo -u discourse du -sh /home/discourse/.cache/bundle &&\ rm -rf /var/www/discourse/vendor/bundle &&\ diff --git a/scripts/configure-benchmark-cache-scope.py b/scripts/configure-benchmark-cache-scope.py new file mode 100644 index 000000000..ee37bfdf0 --- /dev/null +++ b/scripts/configure-benchmark-cache-scope.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Write one benchmark run's isolated adapter tags into the repo plan.""" + +from __future__ import annotations + +import argparse +import re +from pathlib import Path +from typing import Optional + + +CACHE_TAG = re.compile(r"[A-Za-z0-9_][A-Za-z0-9._-]{0,127}") + + +def replace_tag(source: str, adapter: str, tag: str) -> str: + lines = source.splitlines(keepends=True) + section = f"[adapters.{adapter}]" + in_section = False + replacements = 0 + + for index, line in enumerate(lines): + stripped = line.strip() + if stripped.startswith("[") and stripped.endswith("]"): + in_section = stripped == section + continue + if in_section and stripped.startswith("tag = "): + ending = "\n" if line.endswith("\n") else "" + lines[index] = f'tag = "{tag}"{ending}' + replacements += 1 + + if replacements != 1: + raise SystemExit(f"expected one tag in {section}, found {replacements}") + return "".join(lines) + + +def read_tag(source: str, adapter: str) -> Optional[str]: + section = f"[adapters.{adapter}]" + in_section = False + for line in source.splitlines(): + stripped = line.strip() + if stripped.startswith("[") and stripped.endswith("]"): + in_section = stripped == section + continue + if in_section and stripped.startswith("tag = "): + return stripped.removeprefix("tag = ").strip('"') + return None + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("plan", type=Path) + parser.add_argument("--docker-tag", required=True) + parser.add_argument("--ccache-tag", required=True) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + for label, tag in (("Docker", args.docker_tag), ("ccache", args.ccache_tag)): + if CACHE_TAG.fullmatch(tag) is None: + raise SystemExit(f"invalid {label} cache tag: {tag}") + if args.docker_tag == args.ccache_tag: + raise SystemExit("Docker and ccache cache tags must differ") + + source = args.plan.read_text() + rendered = replace_tag(source, "docker", args.docker_tag) + rendered = replace_tag(rendered, "ccache", args.ccache_tag) + args.plan.write_text(rendered) + + written = args.plan.read_text() + actual = (read_tag(written, "docker"), read_tag(written, "ccache")) + expected = (args.docker_tag, args.ccache_tag) + if actual != expected: + raise SystemExit(f"cache plan contains the wrong tags: {actual}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From a1fb0a71e49ee6c6212d694b51cb27e69d40167b Mon Sep 17 00:00:00 2001 From: Gaurav Tiwari Date: Thu, 13 Aug 2026 12:04:22 +0100 Subject: [PATCH 11/14] Separate setup and build proxy ports --- .github/actions/discourse-cache-build/action.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/discourse-cache-build/action.yml b/.github/actions/discourse-cache-build/action.yml index ed8ea2327..f18e36060 100644 --- a/.github/actions/discourse-cache-build/action.yml +++ b/.github/actions/discourse-cache-build/action.yml @@ -55,6 +55,7 @@ runs: mode: docker working-directory: image docker-command: setup + proxy-port: "22244" diagnostics: summary verify: none fail-on-cache-error: true From 53338986d7f9b9de9c852254df0e3f13432b201e Mon Sep 17 00:00:00 2001 From: Gaurav Tiwari Date: Thu, 13 Aug 2026 13:30:40 +0100 Subject: [PATCH 12/14] Fix cache mount hydration ownership --- .github/boringcache/README.md | 4 +++- .github/workflows/boringcache-container-cache.yml | 6 ++++-- image/base/Dockerfile | 6 ++++-- image/discourse_test/Dockerfile | 6 ++++-- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.github/boringcache/README.md b/.github/boringcache/README.md index 1eb5cc0b0..b345d24a9 100644 --- a/.github/boringcache/README.md +++ b/.github/boringcache/README.md @@ -15,7 +15,9 @@ lane injects BoringCache's remote ccache settings. The Bundler mount contains the complete installed bundle, not only downloaded gem archives. Each build materializes it into `vendor/bundle`, runs a normal `bundle install` there to repair or install anything missing, and then uses `bundle check` as a final -verification. The pnpm mounts cover its complete writable home. +verification. The pnpm mounts cover its complete writable home. Ownership is +set inside each mounted `RUN`; putting `uid`/`gid` on the mount creates BuildKit +initialization entries that prevent an empty mount from being hydrated. The seed uses Discourse commit `eedf0ac2344c37d66a2c9ab05dc8a83bf3efd9bb`. The rebuild uses its immediate diff --git a/.github/workflows/boringcache-container-cache.yml b/.github/workflows/boringcache-container-cache.yml index 9df33d5b6..c065b0e45 100644 --- a/.github/workflows/boringcache-container-cache.yml +++ b/.github/workflows/boringcache-container-cache.yml @@ -60,12 +60,14 @@ jobs: git cat-file -e "${mozilla_fix}^{commit}" git merge-base --is-ancestor "$mozilla_fix" HEAD - grep -Fq 'target=/home/discourse/.cache/bundle,sharing=locked,uid=1000,gid=1000' image/base/Dockerfile + grep -Fq 'target=/home/discourse/.cache/bundle,sharing=locked' image/base/Dockerfile + grep -Fq 'chown discourse:discourse /home/discourse/.cache/bundle' image/base/Dockerfile grep -Fq 'cp -a /home/discourse/.cache/bundle/. /var/www/discourse/vendor/bundle/' image/base/Dockerfile grep -Fq 'bundle config --local path ./vendor/bundle' image/base/Dockerfile # shellcheck disable=SC2016 [[ "$(grep -Fc 'sudo -u discourse bundle install --jobs $(nproc --ignore=1)' image/base/Dockerfile)" -eq 2 ]] - grep -Fq 'target=/home/discourse/.local/share/pnpm,sharing=locked,uid=1000,gid=1000' image/base/Dockerfile + grep -Fq 'target=/home/discourse/.local/share/pnpm,sharing=locked' image/base/Dockerfile + grep -Fq 'chown discourse:discourse /home/discourse/.local/share/pnpm' image/base/Dockerfile # shellcheck disable=SC2016 grep -Fq 'ccache version ${CCACHE_VERSION}' image/base/Dockerfile diff --git a/image/base/Dockerfile b/image/base/Dockerfile index df129d96c..59c53a98a 100644 --- a/image/base/Dockerfile +++ b/image/base/Dockerfile @@ -281,7 +281,8 @@ RUN install -dm 0755 -o discourse -g discourse /var/www/discourse &&\ FROM discourse-slim AS discourse-web-only ENV RAILS_ENV=production -RUN --mount=type=cache,id=discourse-bundle-${DISCOURSE_BRANCH},target=/home/discourse/.cache/bundle,sharing=locked,uid=1000,gid=1000 \ +RUN --mount=type=cache,id=discourse-bundle-${DISCOURSE_BRANCH},target=/home/discourse/.cache/bundle,sharing=locked \ + chown discourse:discourse /home/discourse/.cache/bundle &&\ cd /var/www/discourse &&\ echo "Installed bundle cache before install:" &&\ sudo -u discourse du -sh /home/discourse/.cache/bundle &&\ @@ -302,7 +303,8 @@ RUN --mount=type=cache,id=discourse-bundle-${DISCOURSE_BRANCH},target=/home/disc find /var/www/discourse/vendor/bundle -name tmp -type d -exec rm -rf {} + RUN install -dm 0755 -o discourse -g discourse /home/discourse/.local/share/pnpm -RUN --mount=type=cache,id=discourse-pnpm-${DISCOURSE_BRANCH},target=/home/discourse/.local/share/pnpm,sharing=locked,uid=1000,gid=1000 \ +RUN --mount=type=cache,id=discourse-pnpm-${DISCOURSE_BRANCH},target=/home/discourse/.local/share/pnpm,sharing=locked \ + chown discourse:discourse /home/discourse/.local/share/pnpm &&\ cd /var/www/discourse &&\ sudo -u discourse /bin/bash -c 'pnpm install --frozen-lockfile' diff --git a/image/discourse_test/Dockerfile b/image/discourse_test/Dockerfile index 7248489e7..d80588d7a 100644 --- a/image/discourse_test/Dockerfile +++ b/image/discourse_test/Dockerfile @@ -43,7 +43,8 @@ RUN /tmp/install-chrome &&\ FROM with_browsers AS release -RUN --mount=type=cache,id=discourse-bundle-test-main,target=/home/discourse/.cache/bundle,sharing=locked,uid=1000,gid=1000 \ +RUN --mount=type=cache,id=discourse-bundle-test-main,target=/home/discourse/.cache/bundle,sharing=locked \ + chown discourse:discourse /home/discourse/.cache/bundle &&\ cd /var/www/discourse &&\ echo "Test installed bundle cache before install:" &&\ sudo -u discourse du -sh /home/discourse/.cache/bundle &&\ @@ -61,7 +62,8 @@ RUN --mount=type=cache,id=discourse-bundle-test-main,target=/home/discourse/.cac sudo -u discourse bundle check RUN install -dm 0755 -o discourse -g discourse /home/discourse/.local/share/pnpm -RUN --mount=type=cache,id=discourse-pnpm-main,target=/home/discourse/.local/share/pnpm,sharing=locked,uid=1000,gid=1000 \ +RUN --mount=type=cache,id=discourse-pnpm-main,target=/home/discourse/.local/share/pnpm,sharing=locked \ + chown discourse:discourse /home/discourse/.local/share/pnpm &&\ cd /var/www/discourse &&\ sudo -E -u discourse -H /bin/bash -c 'CI=1 pnpm install' From 3d76129f42c13156b1428c053bb58e7fc1314fab Mon Sep 17 00:00:00 2001 From: Gaurav Tiwari Date: Thu, 13 Aug 2026 15:38:09 +0100 Subject: [PATCH 13/14] Benchmark tool and mount caches without layers --- .../actions/discourse-cache-build/action.yml | 52 +++++++++++++++++-- .github/boringcache/README.md | 10 +++- .../workflows/boringcache-container-cache.yml | 4 ++ scripts/configure-benchmark-cache-scope.py | 27 ++++++++++ 4 files changed, 87 insertions(+), 6 deletions(-) diff --git a/.github/actions/discourse-cache-build/action.yml b/.github/actions/discourse-cache-build/action.yml index f18e36060..6e850bd01 100644 --- a/.github/actions/discourse-cache-build/action.yml +++ b/.github/actions/discourse-cache-build/action.yml @@ -32,6 +32,14 @@ runs: cohort="discourse-r${GITHUB_RUN_ID}-${STRATEGY}-${ARCHITECTURE}" echo "docker_tag=${cohort}-docker" >> "$GITHUB_OUTPUT" echo "ccache_tag=${cohort}-ccache" >> "$GITHUB_OUTPUT" + case "$STRATEGY" in + boringcache) lane_hint=layer ;; + boringcache-optimized) lane_hint=layer-ccache-mount ;; + boringcache-no-layer) lane_hint=no-layer-ccache-mount ;; + gha) lane_hint=gha ;; + *) echo "unknown cache strategy: $STRATEGY" >&2; exit 1 ;; + esac + echo "lane_hint=${lane_hint}" >> "$GITHUB_OUTPUT" - name: Configure the isolated BoringCache plan if: startsWith(inputs.strategy, 'boringcache') @@ -39,7 +47,8 @@ runs: run: | python3 ./scripts/configure-benchmark-cache-scope.py .boringcache.toml \ --docker-tag "${{ steps.scope.outputs.docker_tag }}" \ - --ccache-tag "${{ steps.scope.outputs.ccache_tag }}" + --ccache-tag "${{ steps.scope.outputs.ccache_tag }}" \ + --lane "${{ steps.scope.outputs.lane_hint }}" - name: Set up Buildx for GitHub Actions cache if: inputs.strategy == 'gha' @@ -160,6 +169,35 @@ runs: -- docker buildx bake "$target" --load done + - name: Build with BoringCache ccache and mountcache without layer reuse + if: inputs.strategy == 'boringcache-no-layer' + shell: bash + working-directory: image + env: + ARCH: ${{ inputs.architecture }} + DATESTAMP: ${{ inputs.source-datestamp }} + DISCOURSE_REF: ${{ inputs.source-sha }} + RUST_LOG: warn + run: | + set -euo pipefail + targets=( + base-runtime-deps + base-slim-main + base-slim-stable + base-web-only-main + base-web-only-stable + base-release-main + base-release-stable + test-release + ) + for target in "${targets[@]}"; do + boringcache docker \ + --fail-on-cache-error \ + --mount-cache \ + --tool-cache ccache \ + -- docker buildx bake "$target" --no-cache --load + done + - name: Capture image-factory timing id: timing shell: bash @@ -195,8 +233,9 @@ runs: architecture: $architecture, source_sha: $source_sha, build_seconds: $build_seconds, + docker_layer_reuse: ($strategy != "boringcache-no-layer"), docker_cache_tag: $docker_tag, - ccache_tag: (if $strategy == "boringcache-optimized" then $ccache_tag else null end), + ccache_tag: (if ($strategy == "boringcache-optimized" or $strategy == "boringcache-no-layer") then $ccache_tag else null end), github: { repository: env.GITHUB_REPOSITORY, run_id: env.GITHUB_RUN_ID, @@ -242,8 +281,13 @@ runs: echo "| Architecture | $ARCHITECTURE |" echo "| Discourse source | \`$SOURCE_SHA\` |" echo "| Timed build | ${BUILD_SECONDS}s |" - echo "| Layer cache cohort | \`$DOCKER_TAG\` |" - if [[ "$STRATEGY" == "boringcache-optimized" ]]; then + if [[ "$STRATEGY" == "boringcache-no-layer" ]]; then + echo "| Docker layer reuse | Disabled (\`--no-cache\`) |" + else + echo "| Docker layer reuse | Enabled |" + fi + echo "| Docker cache cohort | \`$DOCKER_TAG\` |" + if [[ "$STRATEGY" == "boringcache-optimized" || "$STRATEGY" == "boringcache-no-layer" ]]; then echo "| ccache cohort | \`$CCACHE_TAG\` |" fi } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/boringcache/README.md b/.github/boringcache/README.md index b345d24a9..e9b6c4833 100644 --- a/.github/boringcache/README.md +++ b/.github/boringcache/README.md @@ -4,14 +4,20 @@ This branch runs Discourse's own image factory in `boringcache/discourse_docker` It keeps the upstream target-by-target Bake order and uses native GitHub-hosted runners: `ubuntu-24.04` for AMD64 and `ubuntu-24.04-arm` for ARM64. -Every architecture builds the same Dockerfiles through three isolated lanes: +Every architecture builds the same Dockerfiles through four isolated lanes: 1. GitHub Actions layer cache. 2. BoringCache layer cache. 3. BoringCache layer cache plus ccache tool cache and BuildKit mountcache. +4. BoringCache ccache tool cache and BuildKit mountcache with ordinary Docker + layer reuse disabled. The shared Dockerfile owns the ccache 4.13.6 compiler launchers. Only the third -lane injects BoringCache's remote ccache settings. The Bundler mount contains +and fourth lanes inject BoringCache's remote ccache settings. The fourth lane +passes Bake's native `--no-cache` flag for every target, so every Dockerfile +instruction executes while ccache and cache-mount contents can still be +restored. Each lane uses separate per-run, per-architecture tags and cannot +warm another lane. The Bundler mount contains the complete installed bundle, not only downloaded gem archives. Each build materializes it into `vendor/bundle`, runs a normal `bundle install` there to repair or install anything missing, and then uses `bundle check` as a final diff --git a/.github/workflows/boringcache-container-cache.yml b/.github/workflows/boringcache-container-cache.yml index c065b0e45..1104c299e 100644 --- a/.github/workflows/boringcache-container-cache.yml +++ b/.github/workflows/boringcache-container-cache.yml @@ -70,6 +70,9 @@ jobs: grep -Fq 'chown discourse:discourse /home/discourse/.local/share/pnpm' image/base/Dockerfile # shellcheck disable=SC2016 grep -Fq 'ccache version ${CCACHE_VERSION}' image/base/Dockerfile + # shellcheck disable=SC2016 + grep -Fq -- '-- docker buildx bake "$target" --no-cache --load' \ + .github/actions/discourse-cache-build/action.yml seed: name: Seed | ${{ matrix.lane.label }} | ${{ matrix.architecture.name }} @@ -83,6 +86,7 @@ jobs: - {label: GitHub Actions, strategy: gha} - {label: BoringCache layer, strategy: boringcache} - {label: BoringCache layer + ccache + mountcache, strategy: boringcache-optimized} + - {label: BoringCache no layer + ccache + mountcache, strategy: boringcache-no-layer} architecture: - {name: amd64, runner: ubuntu-24.04} - {name: arm64, runner: ubuntu-24.04-arm} diff --git a/scripts/configure-benchmark-cache-scope.py b/scripts/configure-benchmark-cache-scope.py index ee37bfdf0..ff01bb815 100644 --- a/scripts/configure-benchmark-cache-scope.py +++ b/scripts/configure-benchmark-cache-scope.py @@ -10,6 +10,7 @@ CACHE_TAG = re.compile(r"[A-Za-z0-9_][A-Za-z0-9._-]{0,127}") +LANE = re.compile(r"[A-Za-z0-9_][A-Za-z0-9._-]{0,63}") def replace_tag(source: str, adapter: str, tag: str) -> str: @@ -46,11 +47,33 @@ def read_tag(source: str, adapter: str) -> Optional[str]: return None +def replace_lane(source: str, adapter: str, lane: str) -> str: + lines = source.splitlines(keepends=True) + section = f"[adapters.{adapter}]" + in_section = False + replacements = 0 + + for index, line in enumerate(lines): + stripped = line.strip() + if stripped.startswith("[") and stripped.endswith("]"): + in_section = stripped == section + continue + if in_section and stripped.startswith("metadata-hints = "): + rendered, count = re.subn(r'"lane=[^"]+"', f'"lane={lane}"', line) + lines[index] = rendered + replacements += count + + if replacements != 1: + raise SystemExit(f"expected one lane hint in {section}, found {replacements}") + return "".join(lines) + + def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("plan", type=Path) parser.add_argument("--docker-tag", required=True) parser.add_argument("--ccache-tag", required=True) + parser.add_argument("--lane", required=True) return parser.parse_args() @@ -61,10 +84,14 @@ def main() -> int: raise SystemExit(f"invalid {label} cache tag: {tag}") if args.docker_tag == args.ccache_tag: raise SystemExit("Docker and ccache cache tags must differ") + if LANE.fullmatch(args.lane) is None: + raise SystemExit(f"invalid cache lane: {args.lane}") source = args.plan.read_text() rendered = replace_tag(source, "docker", args.docker_tag) rendered = replace_tag(rendered, "ccache", args.ccache_tag) + rendered = replace_lane(rendered, "docker", args.lane) + rendered = replace_lane(rendered, "ccache", args.lane) args.plan.write_text(rendered) written = args.plan.read_text() From b3837f27bf7c4e743679a1d6659377ea46ae7355 Mon Sep 17 00:00:00 2001 From: Gaurav Tiwari Date: Thu, 13 Aug 2026 15:49:33 +0100 Subject: [PATCH 14/14] Cache the pnpm store used inside BuildKit --- .github/boringcache/README.md | 10 +++++++--- .github/workflows/boringcache-container-cache.yml | 6 ++++-- image/base/Dockerfile | 14 ++++++++++---- image/discourse_test/Dockerfile | 14 ++++++++++---- 4 files changed, 31 insertions(+), 13 deletions(-) diff --git a/.github/boringcache/README.md b/.github/boringcache/README.md index e9b6c4833..a6da412cc 100644 --- a/.github/boringcache/README.md +++ b/.github/boringcache/README.md @@ -21,9 +21,13 @@ warm another lane. The Bundler mount contains the complete installed bundle, not only downloaded gem archives. Each build materializes it into `vendor/bundle`, runs a normal `bundle install` there to repair or install anything missing, and then uses `bundle check` as a final -verification. The pnpm mounts cover its complete writable home. Ownership is -set inside each mounted `RUN`; putting `uid`/`gid` on the mount creates BuildKit -initialization entries that prevent an empty mount from being hydrated. +verification. pnpm's store is explicitly fixed to and mounted at +`/var/www/discourse/.pnpm-store`; when its default home store is a separate +BuildKit filesystem, pnpm otherwise relocates the real store beside the +project and leaves the apparent home mount empty. The build logs the resolved +store path and its before/after size. Ownership is set inside each mounted +`RUN`; putting `uid`/`gid` on the mount creates BuildKit initialization entries +that prevent an empty mount from being hydrated. The seed uses Discourse commit `eedf0ac2344c37d66a2c9ab05dc8a83bf3efd9bb`. The rebuild uses its immediate diff --git a/.github/workflows/boringcache-container-cache.yml b/.github/workflows/boringcache-container-cache.yml index 1104c299e..c3f398690 100644 --- a/.github/workflows/boringcache-container-cache.yml +++ b/.github/workflows/boringcache-container-cache.yml @@ -66,8 +66,10 @@ jobs: grep -Fq 'bundle config --local path ./vendor/bundle' image/base/Dockerfile # shellcheck disable=SC2016 [[ "$(grep -Fc 'sudo -u discourse bundle install --jobs $(nproc --ignore=1)' image/base/Dockerfile)" -eq 2 ]] - grep -Fq 'target=/home/discourse/.local/share/pnpm,sharing=locked' image/base/Dockerfile - grep -Fq 'chown discourse:discourse /home/discourse/.local/share/pnpm' image/base/Dockerfile + grep -Fq 'target=/var/www/discourse/.pnpm-store,sharing=locked' image/base/Dockerfile + grep -Fq 'pnpm store path --store-dir=/var/www/discourse/.pnpm-store' image/base/Dockerfile + grep -Fq 'pnpm install --store-dir=/var/www/discourse/.pnpm-store --frozen-lockfile' image/base/Dockerfile + grep -Fq 'target=/var/www/discourse/.pnpm-store,sharing=locked' image/discourse_test/Dockerfile # shellcheck disable=SC2016 grep -Fq 'ccache version ${CCACHE_VERSION}' image/base/Dockerfile # shellcheck disable=SC2016 diff --git a/image/base/Dockerfile b/image/base/Dockerfile index 59c53a98a..5d8d6b9d1 100644 --- a/image/base/Dockerfile +++ b/image/base/Dockerfile @@ -302,11 +302,17 @@ RUN --mount=type=cache,id=discourse-bundle-${DISCOURSE_BRANCH},target=/home/disc find /var/www/discourse/vendor/bundle -name cache -not -path '*/gems/*' -type d -exec rm -rf {} + &&\ find /var/www/discourse/vendor/bundle -name tmp -type d -exec rm -rf {} + -RUN install -dm 0755 -o discourse -g discourse /home/discourse/.local/share/pnpm -RUN --mount=type=cache,id=discourse-pnpm-${DISCOURSE_BRANCH},target=/home/discourse/.local/share/pnpm,sharing=locked \ - chown discourse:discourse /home/discourse/.local/share/pnpm &&\ +RUN install -dm 0755 -o discourse -g discourse /var/www/discourse/.pnpm-store +RUN --mount=type=cache,id=discourse-pnpm-${DISCOURSE_BRANCH},target=/var/www/discourse/.pnpm-store,sharing=locked \ + chown discourse:discourse /var/www/discourse/.pnpm-store &&\ cd /var/www/discourse &&\ - sudo -u discourse /bin/bash -c 'pnpm install --frozen-lockfile' + pnpm_store="$(sudo -u discourse pnpm store path --store-dir=/var/www/discourse/.pnpm-store)" &&\ + case "$pnpm_store" in /var/www/discourse/.pnpm-store/*) ;; *) echo "Unexpected pnpm store: $pnpm_store" >&2; exit 1 ;; esac &&\ + echo "pnpm store before install: $pnpm_store" &&\ + du -sh /var/www/discourse/.pnpm-store &&\ + sudo -u discourse /bin/bash -c 'pnpm install --store-dir=/var/www/discourse/.pnpm-store --frozen-lockfile' &&\ + echo "pnpm store after install: $pnpm_store" &&\ + du -sh /var/www/discourse/.pnpm-store FROM discourse-web-only AS discourse-release RUN --mount=type=tmpfs,target=/var/log \ diff --git a/image/discourse_test/Dockerfile b/image/discourse_test/Dockerfile index d80588d7a..049f615eb 100644 --- a/image/discourse_test/Dockerfile +++ b/image/discourse_test/Dockerfile @@ -61,11 +61,17 @@ RUN --mount=type=cache,id=discourse-bundle-test-main,target=/home/discourse/.cac sudo -u discourse bundle install --jobs $(nproc --ignore=1) &&\ sudo -u discourse bundle check -RUN install -dm 0755 -o discourse -g discourse /home/discourse/.local/share/pnpm -RUN --mount=type=cache,id=discourse-pnpm-main,target=/home/discourse/.local/share/pnpm,sharing=locked \ - chown discourse:discourse /home/discourse/.local/share/pnpm &&\ +RUN install -dm 0755 -o discourse -g discourse /var/www/discourse/.pnpm-store +RUN --mount=type=cache,id=discourse-pnpm-main,target=/var/www/discourse/.pnpm-store,sharing=locked \ + chown discourse:discourse /var/www/discourse/.pnpm-store &&\ cd /var/www/discourse &&\ - sudo -E -u discourse -H /bin/bash -c 'CI=1 pnpm install' + pnpm_store="$(sudo -E -u discourse -H pnpm store path --store-dir=/var/www/discourse/.pnpm-store)" &&\ + case "$pnpm_store" in /var/www/discourse/.pnpm-store/*) ;; *) echo "Unexpected pnpm store: $pnpm_store" >&2; exit 1 ;; esac &&\ + echo "Test pnpm store before install: $pnpm_store" &&\ + du -sh /var/www/discourse/.pnpm-store &&\ + sudo -E -u discourse -H /bin/bash -c 'CI=1 pnpm install --store-dir=/var/www/discourse/.pnpm-store' &&\ + echo "Test pnpm store after install: $pnpm_store" &&\ + du -sh /var/www/discourse/.pnpm-store RUN cd /var/www/discourse && sudo -E -u discourse -H bundle exec rake plugin:install_all_official &&\ LOAD_PLUGINS=1 sudo -E -u discourse -H bundle exec rake plugin:install_all_gems &&\