From 17f5e0eb688055af8b617fd4182eebec11df561c Mon Sep 17 00:00:00 2001 From: Peter Dedene Date: Fri, 23 Oct 2026 09:31:51 +0200 Subject: [PATCH] feat: rebuild the official ruby:X.Y.Z-bookworm image on demand Replace the bullseye-slim image (Debian LTS ended 2026-08-31) with the docker-library bookworm template, version and checksum as build args, so zenjoy/ruby:X.Y.Z-bookworm is a drop-in for ruby:X.Y.Z-bookworm when a Ruby security release lands before the official image. - build.yml: manual dispatch with version + SHA256 (cross-checked against cache.ruby-lang.org index.txt), amd64 + arm64 on hosted runners, smoke test, X.Y.Z-bookworm and X.Y-bookworm tags on Docker Hub and GHCR, cosign signatures; PRs build and smoke-test without pushing - drop the weekly matrix rebuild - bump actions to current majors - fix: YJIT was never enabled for Ruby 4.x (minor >= 2 check) --- .github/workflows/build.yml | 259 ++++++++++++++----------------- .github/workflows/matrix.yml | 25 --- Dockerfile | 291 +++++++++++++---------------------- README.md | 56 ++++--- 4 files changed, 258 insertions(+), 373 deletions(-) delete mode 100644 .github/workflows/matrix.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c65c0ae..1a11b1e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,224 +1,193 @@ -name: Build and push container images for Specific Ruby Version +name: Build Ruby image + +# Break-glass build of `zenjoy/ruby:X.Y.Z-bookworm` when a Ruby (security) release is out and the +# official `ruby:X.Y.Z-bookworm` image hasn't landed yet. Run it from the Actions tab with the version +# and the tarball SHA256 from the ruby-lang.org release post. Pull requests only build (no push). on: workflow_dispatch: - workflow_call: inputs: - ruby: - description: 'Ruby version' + ruby_version: + description: 'Ruby version, e.g. 4.0.9' + required: true + type: string + ruby_sha256: + description: 'SHA256 of ruby-X.Y.Z.tar.xz (from the ruby-lang.org release post)' required: true type: string + pull_request: + paths: + - 'Dockerfile' + - '.github/workflows/build.yml' + env: - COSIGN_EXPERIMENTAL: 1 REGISTRY_IMAGE: zenjoy/ruby - DEBIAN: bullseye + DEBIAN_SUITE: bookworm + # Version pull requests build as a smoke test. + PR_RUBY_VERSION: 4.0.9 + PR_RUBY_SHA256: a8057adb39f51ad6dc3106237504c93dd792bb515837b4fe7817ae21ed7e51f4 + +concurrency: + group: build-${{ github.event_name }}-${{ inputs.ruby_version || github.ref }} + cancel-in-progress: false + +permissions: + contents: read jobs: build: - name: Build Ruby ${{ inputs.ruby }} for ${{ matrix.platform }} + name: Build Ruby ${{ inputs.ruby_version || 'PR smoke' }} (${{ matrix.platform }}) runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - os: [ubuntu-latest, ubuntu-22.04-arm-2-core] include: - - os: ubuntu-latest + - os: ubuntu-24.04 platform: amd64 - - os: ubuntu-22.04-arm-2-core + - os: ubuntu-24.04-arm platform: arm64 - permissions: - id-token: write # keyless Cosign signatures - packages: write # GHCR - contents: write # git tags - steps: - name: Checkout repo - uses: actions/checkout@v5 + uses: actions/checkout@v7 - - name: Parse ruby version - id: prepare - run: | - LATEST_PATCH=$(grep -E "^ENV RUBY_VERSION=${{ inputs.ruby }}\.[0-9]+$" Dockerfile | cut -d= -f2) - RUBY_VERSION=$(grep -Eo 'RUBY_VERSION=${{ inputs.ruby }}\.[0-9]+$' Dockerfile | cut -d '=' -f2) - echo "RUBY_VERSION=$RUBY_VERSION" - MAJOR=$(echo $RUBY_VERSION | cut -d . -f 1) - MINOR=$(echo $RUBY_VERSION | cut -d . -f 2) - PATCH=$(echo $RUBY_VERSION | cut -d . -f 3) - TODAY=$(date +%Y-%m-%d) - echo "major=$MAJOR" | tee -a $GITHUB_OUTPUT - echo "minor=$MINOR" | tee -a $GITHUB_OUTPUT - echo "patch=$PATCH" | tee -a $GITHUB_OUTPUT - echo "version=$RUBY_VERSION" | tee -a $GITHUB_OUTPUT - echo "today=${TODAY}" | tee -a $GITHUB_OUTPUT - - # Determine if YJIT should be enabled (for Ruby 3.2+) - ENABLE_YJIT="false" - if [ "$MAJOR" -ge 3 ] && [ "$MINOR" -ge 2 ]; then - ENABLE_YJIT="true" - fi - echo "enable_yjit=${ENABLE_YJIT}" | tee -a $GITHUB_OUTPUT - - - name: Generate build tag from head - id: branch + - name: Resolve and verify Ruby version + id: ruby + env: + RUBY_VERSION: ${{ inputs.ruby_version || env.PR_RUBY_VERSION }} + RUBY_SHA256: ${{ inputs.ruby_sha256 || env.PR_RUBY_SHA256 }} run: | - export GIT_REF=${GITHUB_HEAD_REF:-$GITHUB_REF_NAME} - echo "branch=$(echo ${GIT_REF,,} | sed 's/[^a-zA-Z0-9]/-/g')" | tee -a $GITHUB_OUTPUT - - - name: Generate Docker metadata with ruby version - uses: docker/metadata-action@v5 - id: metadata - with: - flavor: | - latest=${{ matrix.ruby == '3.3' }} - images: | - docker.io/${{ env.REGISTRY_IMAGE }} - ghcr.io/${{ env.REGISTRY_IMAGE }} - tags: | - type=semver,pattern={{version}},value=v${{ steps.prepare.outputs.version }} - type=semver,pattern={{major}}.{{minor}},value=v${{ steps.prepare.outputs.version }} - labels: | - org.opencontainers.image.title=ruby + set -euo pipefail + [[ "$RUBY_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::bad version '$RUBY_VERSION'"; exit 1; } + [[ "$RUBY_SHA256" =~ ^[0-9a-f]{64}$ ]] || { echo "::error::bad sha256"; exit 1; } + # Second source for the checksum: the release index on cache.ruby-lang.org. + indexed="$(curl -fsSL https://cache.ruby-lang.org/pub/ruby/index.txt | awk -v n="ruby-$RUBY_VERSION" '$1 == n && $2 ~ /\.tar\.xz$/ { print $4 }')" + [ "$indexed" = "$RUBY_SHA256" ] || { echo "::error::sha256 does not match cache.ruby-lang.org/pub/ruby/index.txt ('$indexed')"; exit 1; } + { + echo "version=$RUBY_VERSION" + echo "minor=${RUBY_VERSION%.*}" + echo "sha256=$RUBY_SHA256" + } | tee -a "$GITHUB_OUTPUT" - name: Login to Docker Hub - uses: docker/login-action@v3 + if: github.event_name != 'pull_request' + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKER_HUB_USER }} password: ${{ secrets.DOCKER_HUB_PASSWORD }} - - name: Login to GitHub Container Repository - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ github.token }} - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Build container image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 id: build with: - labels: ${{ steps.metadata.outputs.labels }} build-args: | - BUILD_AT=${{ steps.prepare.outputs.today }} - RUBY=${{ inputs.ruby }} - DEBIAN=bullseye - ENABLE_YJIT=${{ steps.prepare.outputs.enable_yjit }} + DEBIAN_SUITE=${{ env.DEBIAN_SUITE }} + RUBY_VERSION=${{ steps.ruby.outputs.version }} + RUBY_DOWNLOAD_SHA256=${{ steps.ruby.outputs.sha256 }} platforms: linux/${{ matrix.platform }} - outputs: | - type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true - cache-from: | - type=registry,ref=${{ env.REGISTRY_IMAGE }}:buildcache-${{ matrix.platform }}-${{ inputs.ruby }} - cache-to: | - type=registry,ref=${{ env.REGISTRY_IMAGE }}:buildcache-${{ matrix.platform }}-${{ inputs.ruby }},mode=max + labels: | + org.opencontainers.image.title=ruby + org.opencontainers.image.version=${{ steps.ruby.outputs.version }}-${{ env.DEBIAN_SUITE }} + org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} + org.opencontainers.image.revision=${{ github.sha }} + outputs: ${{ github.event_name == 'pull_request' && 'type=docker,name=ruby-smoke' || format('type=image,name={0},push-by-digest=true,name-canonical=true,push=true', env.REGISTRY_IMAGE) }} + cache-from: type=registry,ref=${{ env.REGISTRY_IMAGE }}:buildcache-${{ matrix.platform }}-${{ steps.ruby.outputs.minor }}-${{ env.DEBIAN_SUITE }} + cache-to: ${{ github.event_name != 'pull_request' && format('type=registry,ref={0}:buildcache-{1}-{2}-{3},mode=max', env.REGISTRY_IMAGE, matrix.platform, steps.ruby.outputs.minor, env.DEBIAN_SUITE) || '' }} + + - name: Smoke test + env: + IMAGE: ${{ github.event_name == 'pull_request' && 'ruby-smoke' || format('{0}@{1}', env.REGISTRY_IMAGE, steps.build.outputs.digest) }} + RUBY_VERSION: ${{ steps.ruby.outputs.version }} + run: | + docker run --rm -e EXPECTED="$RUBY_VERSION" "$IMAGE" ruby -e ' + abort "ruby #{RUBY_VERSION} != #{ENV["EXPECTED"]}" unless RUBY_VERSION == ENV["EXPECTED"] + abort "built without YJIT" unless defined?(RubyVM::YJIT) + require "openssl"; require "psych"; require "zlib" + puts RUBY_DESCRIPTION, OpenSSL::OPENSSL_LIBRARY_VERSION + ' - name: Export digest + if: github.event_name != 'pull_request' + env: + DIGEST: ${{ steps.build.outputs.digest }} run: | mkdir -p /tmp/digests - digest="${{ steps.build.outputs.digest }}" - touch "/tmp/digests/${digest#sha256:}" + touch "/tmp/digests/${DIGEST#sha256:}" - name: Upload digest - uses: actions/upload-artifact@v5 + if: github.event_name != 'pull_request' + uses: actions/upload-artifact@v7 with: - name: digests-${{ inputs.ruby }}-${{ env.DEBIAN }}-${{ matrix.platform }} + name: digests-${{ steps.ruby.outputs.version }}-${{ env.DEBIAN_SUITE }}-${{ matrix.platform }} path: /tmp/digests/* if-no-files-found: error retention-days: 1 merge: - name: Generating Manifest for Ruby ${{ inputs.ruby }} - runs-on: ubuntu-latest - + name: Publish Ruby ${{ inputs.ruby_version }} manifest + if: github.event_name != 'pull_request' + runs-on: ubuntu-24.04 needs: - build permissions: + contents: read id-token: write # keyless Cosign signatures packages: write # GHCR - contents: write # git tags steps: - - name: Checkout repo - uses: actions/checkout@v5 - - name: Install Cosign - uses: sigstore/cosign-installer@v4.0.0 + uses: sigstore/cosign-installer@v4.1.2 - name: Download digests - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v8 with: path: /tmp/digests - pattern: digests-${{ inputs.ruby }}-${{ env.DEBIAN }}-* + pattern: digests-${{ inputs.ruby_version }}-${{ env.DEBIAN_SUITE }}-* merge-multiple: true - - name: Parse ruby version - id: prepare - run: | - RUBY_VERSION=$(grep -Eo 'RUBY_VERSION=${{ inputs.ruby }}\.[0-9]+$' Dockerfile | cut -d '=' -f2) - MAJOR=$(echo $RUBY_VERSION | cut -d . -f 1) - MINOR=$(echo $RUBY_VERSION | cut -d . -f 2) - PATCH=$(echo $RUBY_VERSION | cut -d . -f 3) - TODAY=$(date +%Y-%m-%d) - echo "major=$MAJOR" | tee -a $GITHUB_OUTPUT - echo "minor=$MINOR" | tee -a $GITHUB_OUTPUT - echo "patch=$PATCH" | tee -a $GITHUB_OUTPUT - echo "version=$RUBY_VERSION" | tee -a $GITHUB_OUTPUT - echo "today=${TODAY}" | tee -a $GITHUB_OUTPUT - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Generate Docker metadata with ruby version - uses: docker/metadata-action@v5 - id: metadata - with: - flavor: | - latest=${{ inputs.ruby == '3.3' }} - images: | - docker.io/${{ env.REGISTRY_IMAGE }} - ghcr.io/${{ env.REGISTRY_IMAGE }} - tags: | - type=semver,pattern={{version}},value=v${{ steps.prepare.outputs.version }} - type=semver,pattern={{major}}.{{minor}},value=v${{ steps.prepare.outputs.version }} - labels: | - org.opencontainers.image.title=ruby - - name: Login to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKER_HUB_USER }} password: ${{ secrets.DOCKER_HUB_PASSWORD }} - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Repository - uses: docker/login-action@v3 + - name: Login to GitHub Container Registry + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ github.token }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + - name: Create manifest list and push + id: manifest working-directory: /tmp/digests + env: + RUBY_VERSION: ${{ inputs.ruby_version }} run: | - docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *) - - - name: Inspect image - id: inspect - run: | - docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.metadata.outputs.version }} - DIGEST=$(docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.metadata.outputs.version }} | grep Digest | awk '{print $2}') - echo "IMAGE_DIGEST=$DIGEST" >> $GITHUB_OUTPUT - echo "Image digest: $DIGEST" + set -euo pipefail + tags=() + for registry in docker.io ghcr.io; do + tags+=(-t "$registry/$REGISTRY_IMAGE:$RUBY_VERSION-$DEBIAN_SUITE") + tags+=(-t "$registry/$REGISTRY_IMAGE:${RUBY_VERSION%.*}-$DEBIAN_SUITE") + done + sources=() + for digest in *; do sources+=("$REGISTRY_IMAGE@sha256:$digest"); done + [ "${#sources[@]}" -eq 2 ] || { echo "::error::expected 2 platform digests, got ${#sources[@]}"; exit 1; } + docker buildx imagetools create "${tags[@]}" "${sources[@]}" + ref="$REGISTRY_IMAGE:$RUBY_VERSION-$DEBIAN_SUITE" + docker buildx imagetools inspect "$ref" + echo "digest=$(docker buildx imagetools inspect "$ref" --format '{{json .Manifest}}' | jq -r .digest)" | tee -a "$GITHUB_OUTPUT" - name: Sign container images - run: | - cosign sign --yes --recursive "docker.io/$IMAGE_NAME@$IMAGE_DIGEST" - cosign sign --yes --recursive "ghcr.io/$IMAGE_NAME@$IMAGE_DIGEST" env: - IMAGE_NAME: ${{ env.REGISTRY_IMAGE }} - IMAGE_DIGEST: ${{ steps.inspect.outputs.IMAGE_DIGEST }} + DIGEST: ${{ steps.manifest.outputs.digest }} + run: | + cosign sign --yes --recursive "docker.io/$REGISTRY_IMAGE@$DIGEST" + cosign sign --yes --recursive "ghcr.io/$REGISTRY_IMAGE@$DIGEST" diff --git a/.github/workflows/matrix.yml b/.github/workflows/matrix.yml deleted file mode 100644 index 6e6130a..0000000 --- a/.github/workflows/matrix.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Build and push container all images - -on: - workflow_dispatch: - schedule: - - cron: '0 1 * * 0' # weekly refresh on Sunday at 1am - push: - branches: [main] - paths: - - 'Dockerfile' - - '.github/workflows/build.yml' - - '.github/workflows/matrix.yml' - -jobs: - build: - name: Ruby ${{ matrix.ruby }} - strategy: - fail-fast: false - matrix: - ruby: ['3.1', '3.2', '3.3', '3.4', '4.0'] - - uses: zenjoy/docker-ruby/.github/workflows/build.yml@main - with: - ruby: ${{ matrix.ruby }} - secrets: inherit diff --git a/Dockerfile b/Dockerfile index 1ce6abd..3d16321 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,203 +1,124 @@ -ARG DEBIAN=bullseye -ARG RUBY=3.3 +# syntax=docker/dockerfile:1 -FROM debian:bullseye-slim AS base-bullseye +# Mirror of the official Docker Library image (docker-library/ruby, `/bookworm/Dockerfile`), +# so `zenjoy/ruby:X.Y.Z-bookworm` is a drop-in for `ruby:X.Y.Z-bookworm`. Only the version pin is +# lifted into build args, so a Ruby security release can be built before the official image lands. +# Keep the RUN steps in sync with upstream; don't diverge. +# +# https://github.com/docker-library/ruby/blob/master/Dockerfile.template -FROM base-${DEBIAN} AS ruby-2.6 -ENV RUBY_MAJOR=2.6 -ENV RUBY_VERSION=2.6.10 -ENV RUBY_DOWNLOAD_SHA256=5fd8ded51321b88fdc9c1b4b0eb1b951d2eddbc293865da0151612c2e814c1f2 +ARG DEBIAN_SUITE=bookworm -FROM base-${DEBIAN} AS ruby-2.7 -ENV RUBY_MAJOR=2.7 -ENV RUBY_VERSION=2.7.8 -ENV RUBY_DOWNLOAD_SHA256=f22f662da504d49ce2080e446e4bea7008cee11d5ec4858fc69000d0e5b1d7fb +FROM buildpack-deps:${DEBIAN_SUITE} -FROM base-${DEBIAN} AS ruby-3.0 -ENV RUBY_MAJOR=3.0 -ENV RUBY_VERSION=3.0.7 -ENV RUBY_DOWNLOAD_SHA256=1748338373c4fad80129921080d904aca326e41bd9589b498aa5ee09fd575bab +ARG RUBY_VERSION +ARG RUBY_DOWNLOAD_SHA256 -FROM base-${DEBIAN} AS ruby-3.1 -ENV RUBY_MAJOR=3.1 -ENV RUBY_VERSION=3.1.7 -ENV RUBY_DOWNLOAD_SHA256=658acc455b6bda87ac6cc1380e86552b9c1af87055e7a127589c5bf7ed80b035 - -FROM base-${DEBIAN} AS ruby-3.2 -ENV RUBY_MAJOR=3.2 -ENV RUBY_VERSION=3.2.10 -ENV RUBY_DOWNLOAD_SHA256=a64a8a910ac2f28834b2170dedea688f06cbc6431fcd65eb18cc49ddbf3826ae - -FROM base-${DEBIAN} AS ruby-3.3 -ENV RUBY_MAJOR=3.3 -ENV RUBY_VERSION=3.3.10 -ENV RUBY_DOWNLOAD_SHA256=3a06c3a709672a4ddae4e511d7e82f74799b8b3f550c8cf2d6f32089003cb84c - -FROM base-${DEBIAN} AS ruby-3.4 -ENV RUBY_MAJOR=3.4 -ENV RUBY_VERSION=3.4.8 -ENV RUBY_DOWNLOAD_SHA256=53a8ec71111449cbbd42224d8d27c493fa6ded228636731051c48604d4255d68 - -FROM base-${DEBIAN} AS ruby-4.0 -ENV RUBY_MAJOR=4.0 -ENV RUBY_VERSION=4.0.1 -ENV RUBY_DOWNLOAD_SHA256=0531fe57dfdb56bf591620d2450642ea0e0964f3512a6ebee7dc9305de69395f - -FROM ruby-${RUBY} AS release - -# skip installing gem documentation -RUN mkdir -p /usr/local/etc \ - && { \ - echo 'install: --no-document'; \ - echo 'update: --no-document'; \ - } >> /usr/local/etc/gemrc +# skip installing gem documentation with `gem install`/`gem update` +RUN set -eux; \ + mkdir -p /usr/local/etc; \ + echo 'gem: --no-document' >> /usr/local/etc/gemrc ENV LANG=C.UTF-8 -ENV RUBY_MAJOR=$RUBY_MAJOR -ENV RUBY_VERSION=$RUBY_VERSION -ENV RUBY_DOWNLOAD_SHA256=$RUBY_DOWNLOAD_SHA256 -ARG BUILD_AT -ENV BUILD_AT=$BUILD_AT - -ARG ENABLE_YJIT=false -ENV ENABLE_YJIT=$ENABLE_YJIT - -RUN set -eux; \ - apt-get update; \ - apt-get install -y --no-install-recommends \ - bzip2 \ - ca-certificates \ - libffi-dev \ - libgmp-dev \ - libssl-dev \ - libyaml-dev \ - procps \ - zlib1g-dev \ - ; \ - rm -rf /var/lib/apt/lists/* - -# skip installing gem documentation -RUN set -eux; \ - mkdir -p /usr/local/etc; \ - { \ - echo 'install: --no-document'; \ - echo 'update: --no-document'; \ - } >> /usr/local/etc/gemrc +ENV RUBY_VERSION=${RUBY_VERSION} +ENV RUBY_DOWNLOAD_URL=https://cache.ruby-lang.org/pub/ruby/${RUBY_VERSION%.*}/ruby-${RUBY_VERSION}.tar.xz +ENV RUBY_DOWNLOAD_SHA256=${RUBY_DOWNLOAD_SHA256} # some of ruby's build scripts are written in ruby # we purge system ruby later to make sure our final image uses what we just built RUN set -eux; \ - \ - savedAptMark="$(apt-mark showmanual)"; \ - apt-get update; \ - apt-get install -y --no-install-recommends \ - bison \ - dpkg-dev \ - libgdbm-dev \ - ruby \ - autoconf \ - g++ \ - gcc \ - libbz2-dev \ - libgdbm-compat-dev \ - libglib2.0-dev \ - libncurses-dev \ - libreadline-dev \ - libxml2-dev \ - libxslt-dev \ - make \ - wget \ - xz-utils \ - ; \ - rm -rf /var/lib/apt/lists/*; \ - \ - # Install Rust for YJIT if enabled and Ruby version >= 3.2 - if [ "$ENABLE_YJIT" = "true" ] && [ "${RUBY_MAJOR}" != "2.6" ] && [ "${RUBY_MAJOR}" != "2.7" ] && [ "${RUBY_MAJOR}" != "3.0" ] && [ "${RUBY_MAJOR}" != "3.1" ]; then \ - rustArch=; \ - dpkgArch="$(dpkg --print-architecture)"; \ - case "$dpkgArch" in \ - 'amd64') rustArch='x86_64-unknown-linux-gnu'; rustupUrl='https://static.rust-lang.org/rustup/archive/1.27.1/x86_64-unknown-linux-gnu/rustup-init'; rustupSha256='6aeece6993e902708983b209d04c0d1dbb14ebb405ddb87def578d41f920f56d' ;; \ - 'arm64') rustArch='aarch64-unknown-linux-gnu'; rustupUrl='https://static.rust-lang.org/rustup/archive/1.27.1/aarch64-unknown-linux-gnu/rustup-init'; rustupSha256='1cffbf51e63e634c746f741de50649bbbcbd9dbe1de363c9ecef64e278dba2b2' ;; \ - esac; \ - \ - if [ -n "$rustArch" ]; then \ - mkdir -p /tmp/rust; \ - \ - wget -O /tmp/rust/rustup-init "$rustupUrl"; \ - echo "$rustupSha256 */tmp/rust/rustup-init" | sha256sum --check --strict; \ - chmod +x /tmp/rust/rustup-init; \ - \ - export RUSTUP_HOME='/tmp/rust/rustup' CARGO_HOME='/tmp/rust/cargo'; \ - export PATH="$CARGO_HOME/bin:$PATH"; \ - /tmp/rust/rustup-init -y --no-modify-path --profile minimal --default-toolchain '1.84.0' --default-host "$rustArch"; \ - \ - rustc --version; \ - cargo --version; \ - fi; \ - fi; \ - \ - wget -O ruby.tar.xz "https://cache.ruby-lang.org/pub/ruby/${RUBY_MAJOR%-rc}/ruby-$RUBY_VERSION.tar.xz"; \ - echo "$RUBY_DOWNLOAD_SHA256 *ruby.tar.xz" | sha256sum --check --strict; \ - \ - mkdir -p /usr/src/ruby; \ - tar -xJf ruby.tar.xz -C /usr/src/ruby --strip-components=1; \ - rm ruby.tar.xz; \ - \ - cd /usr/src/ruby; \ - \ - # hack in "ENABLE_PATH_CHECK" disabling to suppress: - # warning: Insecure world writable dir - { \ - echo '#define ENABLE_PATH_CHECK 0'; \ - echo; \ - cat file.c; \ - } > file.c.new; \ - mv file.c.new file.c; \ - \ - autoconf; \ - gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)"; \ - ./configure \ - --build="$gnuArch" \ - --disable-install-doc \ - --enable-shared \ - ${rustArch:+--enable-yjit} \ - ; \ - make -j "$(nproc)"; \ - make install; \ - \ - rm -rf /tmp/rust; \ - apt-mark auto '.*' > /dev/null; \ - apt-mark manual $savedAptMark > /dev/null; \ - find /usr/local -type f -executable -not \( -name '*tkinter*' \) -exec ldd '{}' ';' \ - | awk '/=>/ { so = $(NF-1); if (index(so, "/usr/local/") == 1) { next }; gsub("^/(usr/)?", "", so); print so }' \ - | sort -u \ - | xargs -r dpkg-query --search \ - | cut -d: -f1 \ - | sort -u \ - | xargs -r apt-mark manual \ - ; \ - apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ - \ - cd /; \ - rm -r /usr/src/ruby; \ - # verify we have no "ruby" packages installed - if dpkg -l | grep -i ruby; then exit 1; fi; \ - [ "$(command -v ruby)" = '/usr/local/bin/ruby' ]; \ - # update bundler to latest version - gem update --system; \ - gem install bundler; \ - # rough smoke test - ruby --version; \ - gem --version; \ - bundle --version + [ -n "$RUBY_VERSION" ] && [ -n "$RUBY_DOWNLOAD_SHA256" ]; \ + \ + savedAptMark="$(apt-mark showmanual)"; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + dpkg-dev \ + libgdbm-dev \ + ruby \ + ; \ + \ + rustArch=; \ + dpkgArch="$(dpkg --print-architecture)"; \ + case "$dpkgArch" in \ + 'amd64') rustArch='x86_64-unknown-linux-gnu'; rustupUrl='https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-unknown-linux-gnu/rustup-init'; rustupSha256='20a06e644b0d9bd2fbdbfd52d42540bdde820ea7df86e92e533c073da0cdd43c' ;; \ + 'arm64') rustArch='aarch64-unknown-linux-gnu'; rustupUrl='https://static.rust-lang.org/rustup/archive/1.28.2/aarch64-unknown-linux-gnu/rustup-init'; rustupSha256='e3853c5a252fca15252d07cb23a1bdd9377a8c6f3efa01531109281ae47f841c' ;; \ + esac; \ + \ + if [ -n "$rustArch" ]; then \ + mkdir -p /tmp/rust; \ + \ + wget -O /tmp/rust/rustup-init "$rustupUrl"; \ + echo "$rustupSha256 */tmp/rust/rustup-init" | sha256sum --check --strict; \ + chmod +x /tmp/rust/rustup-init; \ + \ + export RUSTUP_HOME='/tmp/rust/rustup' CARGO_HOME='/tmp/rust/cargo'; \ + export PATH="$CARGO_HOME/bin:$PATH"; \ + /tmp/rust/rustup-init -y --no-modify-path --profile minimal --default-toolchain '1.91.1' --default-host "$rustArch"; \ + \ + rustc --version; \ + cargo --version; \ + fi; \ + \ + wget -O ruby.tar.xz "$RUBY_DOWNLOAD_URL"; \ + echo "$RUBY_DOWNLOAD_SHA256 *ruby.tar.xz" | sha256sum --check --strict; \ + \ + mkdir -p /usr/src/ruby; \ + tar -xJf ruby.tar.xz -C /usr/src/ruby --strip-components=1; \ + rm ruby.tar.xz; \ + \ + cd /usr/src/ruby; \ + \ + autoconf; \ + gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)"; \ +# ZJIT only exists from Ruby 4.0; upstream's 3.x templates leave the flag out + zjit=; \ + case "$RUBY_VERSION" in 3.*) ;; *) zjit="${rustArch:+--enable-zjit}" ;; esac; \ +# full RELRO: "-z now" disables lazy binding so the loader can map the GOT read-only at startup +# https://github.com/docker-library/ruby/pull/529 + export LDFLAGS='-Wl,-z,relro -Wl,-z,now'; \ + ./configure \ + --build="$gnuArch" \ + --disable-install-doc \ + --enable-shared \ + ${rustArch:+--enable-yjit} \ + $zjit \ + ; \ + make -j "$(nproc)"; \ + make install; \ + \ + rm -rf /tmp/rust; \ + apt-mark auto '.*' > /dev/null; \ + apt-mark manual $savedAptMark > /dev/null; \ + find /usr/local -type f -executable -not \( -name '*tkinter*' \) -exec ldd '{}' ';' \ + | awk '/=>/ { so = $(NF-1); if (index(so, "/usr/local/") == 1) { next }; gsub("^/(usr/)?", "", so); printf "*%s\n", so }' \ + | sort -u \ + | xargs -r dpkg-query --search \ + | awk 'sub(":$", "", $1) { print $1 }' \ + | sort -u \ + | xargs -r apt-mark manual \ + ; \ + apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ + rm -rf /var/lib/apt/lists/*; \ + \ + cd /; \ + rm -r /usr/src/ruby; \ +# verify we have no "ruby" packages installed + if dpkg -l | grep -i ruby; then exit 1; fi; \ + [ "$(command -v ruby)" = '/usr/local/bin/ruby' ]; \ +# rough smoke test + ruby --version; \ + gem --version; \ + bundle --version # don't create ".bundle" in all our apps ENV GEM_HOME=/usr/local/bundle -ENV BUNDLE_PATH="$GEM_HOME" \ - BUNDLE_SILENCE_ROOT_WARNING=1 \ - BUNDLE_APP_CONFIG="$GEM_HOME" \ - LANG=C.UTF-8 LC_ALL=C.UTF-8 +ENV BUNDLE_SILENCE_ROOT_WARNING=1 \ + BUNDLE_APP_CONFIG="$GEM_HOME" ENV PATH=$GEM_HOME/bin:$PATH +RUN set -eux; \ + mkdir "$GEM_HOME"; \ +# adjust permissions of GEM_HOME for running "gem install" as an arbitrary user + chmod 1777 "$GEM_HOME" CMD [ "irb" ] diff --git a/README.md b/README.md index a14fa02..83cafa5 100644 --- a/README.md +++ b/README.md @@ -3,38 +3,58 @@ [![Docker Hub](https://img.shields.io/badge/Docker%20Hub-zenjoy%2Fruby-lightgrey?style=flat)](https://hub.docker.com/r/zenjoy/ruby) [![License](https://img.shields.io/github/license/zenjoy/docker-ruby)](https://github.com/zenjoy/docker-ruby/blob/main/LICENSE) -The official [Debian](https://hub.docker.com/_/debian) Docker image with Ruby. +A rebuild of the official [`ruby:X.Y.Z-bookworm`](https://hub.docker.com/_/ruby) image, for when a +Ruby security release is out and the official image hasn't been published yet (that usually takes +16 to 24 hours). -Available on [Docker Hub](https://hub.docker.com/r/zenjoy/ruby) or -[GitHub Container Registry](https://ghcr.io/zenjoy/ruby) (GHCR) for AMD64 or ARM64. +The [Dockerfile](Dockerfile) is the Docker Library one +([docker-library/ruby](https://github.com/docker-library/ruby)) with the Ruby version and checksum +turned into build args. Same base (`buildpack-deps:bookworm`), same configure flags (YJIT, ZJIT on +Ruby 4.x, full RELRO), so `zenjoy/ruby:4.0.9-bookworm` can stand in for `ruby:4.0.9-bookworm`. -```sh -# Docker Hub -docker pull zenjoy/ruby:latest +Images are built for `linux/amd64` and `linux/arm64` and pushed to Docker Hub and GHCR: -# GHCR -docker pull ghcr.io/zenjoy/ruby:latest +```sh +docker pull zenjoy/ruby:4.0.9-bookworm +docker pull ghcr.io/zenjoy/ruby:4.0.9-bookworm ``` +Tags: `X.Y.Z-bookworm` and `X.Y-bookworm`. There is no `latest`. + +## When to use it + +Stay on the official image by default. When a Ruby release fixes something you can't wait for: + +1. Run the [Build Ruby image](../../actions/workflows/build.yml) workflow with the version and the + SHA256 of `ruby-X.Y.Z.tar.xz` from the ruby-lang.org release post. The workflow also checks it + against `cache.ruby-lang.org/pub/ruby/index.txt`. +2. Point the app's base image at `zenjoy/ruby:X.Y.Z-bookworm`. +3. Switch back to `ruby:X.Y.Z-bookworm` once the official image is published. + +Nothing rebuilds these images on a schedule, so they don't pick up Debian security updates after +they're built. Don't stay on one longer than needed. + +## Keeping in sync with upstream + +When docker-library changes its template (new rustup or Rust pin, configure flags, cleanup steps), +copy the change into the Dockerfile. Pull requests build the image for both platforms and run a +smoke test, without pushing. + +Older `zenjoy/ruby` tags without the `-bookworm` suffix (`3.3.10`, `4.0.1`, ...) came from the +previous bullseye-slim build and are no longer updated. + ## Container signatures -All images are automatically signed via [Cosign](https://docs.sigstore.dev/cosign/overview/) using -[keyless signatures](https://docs.sigstore.dev/cosign/keyless/). You verify the integrity of these -images as follows: +Images are signed with [Cosign](https://docs.sigstore.dev/cosign/overview/) using +[keyless signatures](https://docs.sigstore.dev/cosign/keyless/). To verify one: ```sh cosign verify \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ --certificate-identity-regexp https://github.com/zenjoy/docker-ruby/.github/workflows/ \ - zenjoy/ruby:latest + zenjoy/ruby:4.0.9-bookworm ``` -## Contributing - -Feel free to contribute and make things better by opening an -[Issue](https://github.com/zenjoy/docker-ruby/issues) or -[Pull Request](https://github.com/zenjoy/docker-ruby/pulls). - ## License View [license information](https://github.com/zenjoy/docker-ruby/blob/main/LICENSE) for the software