Build Docker Image workflow (open-webui/open-terminal)
The Build Docker Image workflow from open-webui/open-terminal, explained and optimized by Latchkey.
CI health: C - fair
Point runs-on at Latchkey and get run de-duplication, job timeouts, SHA-pinned actions, self-healing for flaky steps, and up to 58% lower cost, applied automatically.
What it does
This is the Build Docker Image workflow from the open-webui/open-terminal repository, a real project running GitHub Actions. It is shown here with attribution under its MIT license.
Below, Latchkey shows a faster, safer version produced by its optimization engine.
The workflow
name: Build Docker Image
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
variant:
- name: default
file: Dockerfile
suffix: ""
short_tag: "latest"
- name: slim
file: Dockerfile.slim
suffix: "-slim"
short_tag: "slim"
- name: alpine
file: Dockerfile.alpine
suffix: "-alpine"
short_tag: "alpine"
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
if: github.event_name != 'pull_request'
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@v5
id: meta
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- uses: docker/build-push-action@v5
id: build
with:
context: .
file: ${{ matrix.variant.file }}
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
cache-from: type=gha,scope=${{ matrix.variant.name }}-${{ matrix.platform }}
cache-to: type=gha,scope=${{ matrix.variant.name }}-${{ matrix.platform }},mode=max
- name: Export digest
if: github.event_name != 'pull_request'
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- uses: actions/upload-artifact@v4
if: github.event_name != 'pull_request'
with:
name: digests-${{ matrix.variant.name }}-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
needs: build
strategy:
matrix:
variant:
- name: default
suffix: ""
short_tag: "latest"
- name: slim
suffix: "-slim"
short_tag: "slim"
- name: alpine
suffix: "-alpine"
short_tag: "alpine"
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get version from pyproject.toml
id: version
run: |
VERSION=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
MAJOR_MINOR=$(echo "$VERSION" | cut -d. -f1,2)
echo "major_minor=$MAJOR_MINOR" >> "$GITHUB_OUTPUT"
if git rev-parse "refs/tags/v$VERSION" >/dev/null 2>&1; then
TAG_SHA=$(git rev-list -n 1 "v$VERSION")
if [ "$TAG_SHA" = "${{ github.sha }}" ]; then
echo "is_new_version=true" >> "$GITHUB_OUTPUT"
else
echo "is_new_version=false" >> "$GITHUB_OUTPUT"
fi
else
echo "is_new_version=true" >> "$GITHUB_OUTPUT"
fi
- uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-${{ matrix.variant.name }}-*
merge-multiple: true
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@v5
id: meta
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest${{ matrix.variant.suffix }},enable={{is_default_branch}}
type=raw,value=${{ matrix.variant.short_tag }},enable={{is_default_branch}}
type=raw,value=${{ steps.version.outputs.version }}${{ matrix.variant.suffix }},enable=${{ steps.version.outputs.is_new_version }}
type=raw,value=${{ steps.version.outputs.major_minor }}${{ matrix.variant.suffix }},enable=${{ steps.version.outputs.is_new_version }}
type=sha,suffix=${{ matrix.variant.suffix }}
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
The same workflow, on Latchkey
Removes redundant runs and caps runaway jobs. Added and changed lines are highlighted.
name: Build Docker Image on: push: branches: [main] pull_request: branches: [main] env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: timeout-minutes: 30 runs-on: latchkey-small strategy: fail-fast: false matrix: platform: - linux/amd64 - linux/arm64 variant: - name: default file: Dockerfile suffix: "" short_tag: "latest" - name: slim file: Dockerfile.slim suffix: "-slim" short_tag: "slim" - name: alpine file: Dockerfile.alpine suffix: "-alpine" short_tag: "alpine" permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - uses: docker/setup-qemu-action@v3 - uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 if: github.event_name != 'pull_request' with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - uses: docker/metadata-action@v5 id: meta with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - uses: docker/build-push-action@v5 id: build with: context: . file: ${{ matrix.variant.file }} platforms: ${{ matrix.platform }} labels: ${{ steps.meta.outputs.labels }} outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }} cache-from: type=gha,scope=${{ matrix.variant.name }}-${{ matrix.platform }} cache-to: type=gha,scope=${{ matrix.variant.name }}-${{ matrix.platform }},mode=max - name: Export digest if: github.event_name != 'pull_request' run: | mkdir -p /tmp/digests digest="${{ steps.build.outputs.digest }}" touch "/tmp/digests/${digest#sha256:}" - uses: actions/upload-artifact@v4 if: github.event_name != 'pull_request' with: name: digests-${{ matrix.variant.name }}-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }} path: /tmp/digests/* if-no-files-found: error retention-days: 1 merge: timeout-minutes: 30 runs-on: latchkey-small if: github.event_name != 'pull_request' needs: build strategy: matrix: variant: - name: default suffix: "" short_tag: "latest" - name: slim suffix: "-slim" short_tag: "slim" - name: alpine suffix: "-alpine" short_tag: "alpine" permissions: contents: read packages: write steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Get version from pyproject.toml id: version run: | VERSION=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])") echo "version=$VERSION" >> "$GITHUB_OUTPUT" MAJOR_MINOR=$(echo "$VERSION" | cut -d. -f1,2) echo "major_minor=$MAJOR_MINOR" >> "$GITHUB_OUTPUT" if git rev-parse "refs/tags/v$VERSION" >/dev/null 2>&1; then TAG_SHA=$(git rev-list -n 1 "v$VERSION") if [ "$TAG_SHA" = "${{ github.sha }}" ]; then echo "is_new_version=true" >> "$GITHUB_OUTPUT" else echo "is_new_version=false" >> "$GITHUB_OUTPUT" fi else echo "is_new_version=true" >> "$GITHUB_OUTPUT" fi - uses: actions/download-artifact@v4 with: path: /tmp/digests pattern: digests-${{ matrix.variant.name }}-* merge-multiple: true - uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - uses: docker/metadata-action@v5 id: meta with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=raw,value=latest${{ matrix.variant.suffix }},enable={{is_default_branch}} type=raw,value=${{ matrix.variant.short_tag }},enable={{is_default_branch}} type=raw,value=${{ steps.version.outputs.version }}${{ matrix.variant.suffix }},enable=${{ steps.version.outputs.is_new_version }} type=raw,value=${{ steps.version.outputs.major_minor }}${{ matrix.variant.suffix }},enable=${{ steps.version.outputs.is_new_version }} type=sha,suffix=${{ matrix.variant.suffix }} - name: Create manifest list and push working-directory: /tmp/digests run: | docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ $(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *) - name: Inspect image run: | docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
What changed
- Run on Latchkey managed runners with one line (
runs-on), which apply the fixes below automatically and self-heal transient failures. This example useslatchkey-small; pick the runner size that fits the job. - Cancel superseded runs when a branch or PR gets a newer push.
- Add a job timeout so a hung step cannot burn hours of runner time.
5 third-party actions are referenced by a movable tag. Pin them to the commit SHA (Latchkey resolves and applies this automatically) so a repointed tag cannot change what runs.
What Latchkey heals here
This workflow has steps that commonly fail on transient issues (network, registries, flaky browsers). On Latchkey managed runners they are detected, retried, and self-healed instead of failing your build:
- Container pulls and builds
This workflow runs 2 jobs (9 with the matrix expanded) per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.