Skip to content

Recompress slim layers to gzip before pushing #115

Recompress slim layers to gzip before pushing

Recompress slim layers to gzip before pushing #115

Workflow file for this run

name: Docker Image
on:
# When a release is published
release:
types: [published]
# Push excluding tags and Markdown-only changes
push:
branches:
- main
tags-ignore:
- '*.*'
paths-ignore:
- '**/*.md'
# Validate pull requests without publishing
pull_request:
branches:
- main
paths-ignore:
- '**/*.md'
# Manual trigger
workflow_dispatch:
permissions:
contents: read
security-events: write
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.head.sha || github.ref }}
cancel-in-progress: true
env:
IMAGE_NAME: ${{ vars.DOCKERHUB_NAMESPACE || github.repository_owner }}/postgresql
jobs:
build_arch_images:
name: Build ${{ matrix.arch }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
platform: linux/amd64
- arch: arm64
platform: linux/arm64
outputs:
version: ${{ steps.meta.outputs.version }}
is_versioned: ${{ steps.meta.outputs.is_versioned }}
steps:
- name: Free up disk space
shell: bash
run: |
echo "Disk space before cleanup:"
df -h
# Remove large directories
sudo rm -rf /usr/share/dotnet \
/usr/local/lib/android \
/opt/ghc \
/usr/local/.ghcup \
/opt/hostedtoolcache/CodeQL || true
# Remove large packages
sudo apt-get remove -y '^aspnetcore-.*' '^dotnet-.*' '^llvm-.*' 'php.*' \
'^mongodb-.*' '^mysql-.*' azure-cli google-chrome-stable firefox \
powershell mono-devel libgl1-mesa-dri google-cloud-sdk google-cloud-cli || true
sudo apt-get autoremove -y
sudo apt-get clean
# Remove Docker images
sudo docker image prune --all --force || true
# Remove swap storage
sudo swapoff -a || true
sudo rm -f /mnt/swapfile || true
echo "Disk space after cleanup:"
df -h
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
with:
platforms: all
- name: Set up Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Log in to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets._TEMP_DOCKERHUB_USER }}
password: ${{ secrets._TEMP_DOCKERHUB_PASSWORD }}
- name: Compute image metadata
id: meta
shell: bash
run: |
VERSION=""
IS_VERSIONED="false"
if [[ "${{ github.event_name }}" == "release" ]]; then
VERSION="${{ github.event.release.tag_name }}"
IS_VERSIONED="true"
elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
if [[ "${{ github.ref_type }}" == "tag" ]]; then
VERSION="${{ github.ref_name }}"
IS_VERSIONED="true"
elif [[ "${{ github.ref }}" != "refs/heads/main" ]]; then
echo "workflow_dispatch must be run on main or on a tag"
exit 1
fi
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "is_versioned=$IS_VERSIONED" >> "$GITHUB_OUTPUT"
- name: Build base image (PR — load only)
if: github.event_name == 'pull_request'
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: ${{ matrix.platform }}
provenance: false
sbom: false
outputs: type=docker,name=local-ci-image:latest
build-args: |
GIT_COMMIT=${{ github.sha }}
cache-from: type=gha,scope=postgresql-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=postgresql-${{ matrix.arch }}
no-cache-filters: |
trimmed
trimmed-all
- name: Build base image (load + push by digest)
id: build_base
if: github.event_name != 'pull_request'
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: ${{ matrix.platform }}
provenance: false
sbom: false
outputs: |
type=docker,name=local-ci-image:latest
type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
build-args: |
GIT_COMMIT=${{ github.sha }}
cache-from: type=gha,scope=postgresql-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=postgresql-${{ matrix.arch }}
no-cache-filters: |
trimmed
trimmed-all
- name: Scan base Docker image
uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0
id: anchore-scan
with:
image: local-ci-image:latest
fail-build: false
severity-cutoff: critical
- name: Upload Anchore scan SARIF report
if: ${{ !cancelled() && github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || github.ref == 'refs/heads/main') }}
uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
with:
sarif_file: ${{ steps.anchore-scan.outputs.sarif }}
category: grype-${{ matrix.arch }}
- name: Install slim toolkit
shell: bash
run: |
curl -sL https://raw.githubusercontent.com/slimtoolkit/slim/master/scripts/install-slim.sh | sudo -E bash -
- name: Slim the image
shell: bash
run: |
chmod +x ./slim-image.sh
./slim-image.sh local-ci-image:latest local-ci-image-slim:latest "${{ matrix.arch }}"
# The slim image lives in the host docker daemon (slim-toolkit's output).
# Buildx can't push it by digest: the `docker` driver reads docker-daemon
# images but doesn't support push-by-digest, while the `docker-container`
# driver supports push-by-digest but can't reach the docker daemon. We
# use regctl instead — it can push from a local OCI layout to a registry
# `repo@sha256:…` URL, leaving no tag in the registry.
#
# Cosign is installed first so regctl-installer can verify the binary's
# sigstore signature (the action's verification is opportunistic — it
# silently skips if cosign isn't on PATH).
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Install regctl
if: github.event_name != 'pull_request'
uses: regclient/actions/regctl-installer@1b705e32d40851370799ea5814e83d0a5f6a70dc # v0.1.0
with:
release: v0.11.3
- name: Push slim image by digest
id: push_slim
if: github.event_name != 'pull_request'
shell: bash
env:
DOCKERHUB_USER: ${{ secrets._TEMP_DOCKERHUB_USER }}
DOCKERHUB_PASSWORD: ${{ secrets._TEMP_DOCKERHUB_PASSWORD }}
run: |
set -euo pipefail
cleanup() {
rm -f /tmp/slim.tar
rm -rf /tmp/slim-oci
}
trap cleanup EXIT
# regctl maintains its own credential store, separate from `docker login`.
echo "${DOCKERHUB_PASSWORD}" \
| regctl registry login docker.io --user "${DOCKERHUB_USER}" --pass-stdin
# Defense-in-depth: assert the credential persisted. Silent fallback
# to anonymous would let the push fail with an opaque permission error.
if ! regctl registry config docker.io | grep -q '"user":'; then
echo "regctl login did not persist user credential for docker.io" >&2
exit 1
fi
# docker save → OCI layout: regctl needs a storage backend it understands;
# the docker daemon isn't one, so we stage the image on disk first.
docker save local-ci-image-slim:latest -o /tmp/slim.tar
regctl image import "ocidir:///tmp/slim-oci:slim" /tmp/slim.tar
rm /tmp/slim.tar
# docker save writes uncompressed layer tarballs (.tar, not .tar.gzip).
# Pushed as-is the slim image lands in the registry larger than the
# un-slimmed base. Recompress layers to gzip in place so the registry
# blob sizes reflect the slim toolkit's actual size win.
regctl image mod "ocidir:///tmp/slim-oci:slim" \
--layer-compress gzip --replace
# Compute the manifest digest locally so we can address the registry push
# by digest URL — no tag is created on Docker Hub. Must happen AFTER the
# mod step: recompressing layers changes layer digests → manifest digest.
DIGEST="$(regctl manifest digest "ocidir:///tmp/slim-oci:slim")"
echo "Slim digest (${{ matrix.arch }}): ${DIGEST}"
# Retry on transient registry errors (5xx, connection resets). A failed
# push otherwise costs a full ~15-minute matrix re-run.
for attempt in 1 2 3; do
if regctl image copy "ocidir:///tmp/slim-oci:slim" "${IMAGE_NAME}@${DIGEST}"; then
break
fi
if [ "${attempt}" -eq 3 ]; then
echo "regctl image copy failed after 3 attempts" >&2
exit 1
fi
sleep_for=$((attempt * 5))
echo "regctl image copy attempt ${attempt} failed, retrying in ${sleep_for}s..." >&2
sleep "${sleep_for}"
done
echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT"
- name: Save digests
if: github.event_name != 'pull_request'
shell: bash
run: |
mkdir -p /tmp/digests
echo "${{ steps.build_base.outputs.digest }}" > "/tmp/digests/base-${{ matrix.arch }}"
echo "${{ steps.push_slim.outputs.digest }}" > "/tmp/digests/slim-${{ matrix.arch }}"
- name: Upload digests
if: github.event_name != 'pull_request'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: digests-${{ matrix.arch }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
create_manifests:
name: Create manifests
needs: build_arch_images
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Set up Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Log in to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets._TEMP_DOCKERHUB_USER }}
password: ${{ secrets._TEMP_DOCKERHUB_PASSWORD }}
- name: Download digests
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Create manifest tags
shell: bash
run: |
set -euo pipefail
VERSION="${{ needs.build_arch_images.outputs.version }}"
IS_VERSIONED="${{ needs.build_arch_images.outputs.is_versioned }}"
BASE_AMD64="${IMAGE_NAME}@$(cat /tmp/digests/base-amd64)"
BASE_ARM64="${IMAGE_NAME}@$(cat /tmp/digests/base-arm64)"
SLIM_AMD64="${IMAGE_NAME}@$(cat /tmp/digests/slim-amd64)"
SLIM_ARM64="${IMAGE_NAME}@$(cat /tmp/digests/slim-arm64)"
if [[ "$IS_VERSIONED" == "true" ]]; then
docker buildx imagetools create \
-t "${IMAGE_NAME}:${VERSION}" \
-t "${IMAGE_NAME}:latest" \
"$BASE_AMD64" "$BASE_ARM64"
docker buildx imagetools create \
-t "${IMAGE_NAME}:${VERSION}-slim" \
-t "${IMAGE_NAME}:latest-slim" \
"$SLIM_AMD64" "$SLIM_ARM64"
else
docker buildx imagetools create \
-t "${IMAGE_NAME}:develop" \
"$BASE_AMD64" "$BASE_ARM64"
docker buildx imagetools create \
-t "${IMAGE_NAME}:develop-slim" \
"$SLIM_AMD64" "$SLIM_ARM64"
fi