Skip to content
Sengi

Fast Docker builds in GitHub Actions: the buildx cache, measured on three real projects

How do I make Docker builds fast in GitHub Actions?

Docker builds are slow in GitHub Actions because every job starts on a fresh runner with no layer cache, so each build reruns every RUN step, on 2 vCPU. Keep the layers between runs with docker/build-push-action and cache-to: type=gha,mode=max, and build on faster hardware. On Sengi that is one line, runs-on: sengi-standard-4-ubuntu-2404, and the type=gha cache is served by Sengi’s own cache with no workflow change. Measured on three open-source projects: a Rust server’s image built cold in 3m37s on Sengi’s 4 vCPU runner against 11m06s on GitHub’s ubuntu-latest, and in 1m04s warm after a one-line change against 3m40s, at $0.006 a warm build against $0.022.

Last checked October 27, 2026. Every time on this page was measured October 27, 2026, with one benchmark workflow on Sengi’s 2, 4 and 8 vCPU runners and GitHub’s ubuntu-latest in a private repository. Prices are list prices per minute. Every number is in one JSON file.

Why are Docker builds slow in GitHub Actions?

  • Every layer starts cold. A GitHub-hosted job runs on a new VM, and the Docker build cache on its disk is gone when the job ends. Without an exported cache, each build reinstalls dependencies and recompiles from scratch.
  • No persistent cache unless you add one. The default docker driver cannot export a cache at all. You need a Buildx builder and a cache backend, such as type=gha, for one run to reuse another’s layers.
  • The first miss reruns everything after it. A layer is reused only if it and every layer before it are unchanged, so a COPY . . above the dependency install throws the install away on every commit.
  • 2 vCPU does the rest. On a private repository ubuntu-latest is 2 vCPU and 8 GB, and every layer the cache cannot skip runs on it: compilers, bundlers and package installs.

The fast Docker build workflow for GitHub Actions

One job, three actions: a Buildx builder, the build, and the type=gha layer cache in both directions. Save it as .github/workflows/docker.yml:

name: Docker image
on:
  push:
    branches: [main]
  pull_request:
jobs:
  build:
    runs-on: sengi-standard-4-ubuntu-2404
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v7
      - uses: docker/setup-buildx-action@v4
      - uses: docker/build-push-action@v7
        with:
          context: .
          push: false
          tags: my-app:ci
          cache-from: type=gha,scope=my-app
          cache-to: type=gha,scope=my-app,mode=max
  • runs-on: sengi-standard-4-ubuntu-2404 runs the build on a 4 vCPU Sengi runner at $0.006 a minute, the price of GitHub’s 2 vCPU runner. It is the only line that differs from a GitHub-hosted workflow.
  • cache-to: type=gha,mode=max saves every layer of every stage, including builder stages that never reach the final image. On Sengi the layers go to Sengi’s own cache: in every Sengi job behind these numbers the runner’s ACTIONS_CACHE_URL pointed at Sengi’s cache service on a private address inside Sengi’s network, not at GitHub’s cache, and the cache is included in the runner price.
  • scope=my-app keeps each image’s layers apart when one repository builds several images.
  • push: false builds and caches without publishing. Set push: true with docker/login-action to publish to your registry.

The cache only helps if the Dockerfile copies the lockfile and installs dependencies before it copies the source. The three projects below all do; slow Docker builds in GitHub Actions shows the ordering, and Docker layer caching on Sengi covers scopes and branches.

Docker build benchmark: cold and warm, Sengi and GitHub

Three public projects, each pinned to a commit, each built from its own Dockerfile:

  • Atuin server (atuinsh/atuin at 876aa50): Rust sync server: a cargo-chef dependency layer, then cargo build --release.
  • Umami (umami-software/umami at 559e3c6): Next.js analytics app: a pnpm install layer, then next build.
  • Saleor (saleor/saleor at c982006): Django commerce API: uv sync of native-extension wheels, then collectstatic.

A cold build has no cache at all, like the first run of a new workflow. A warm build runs on a fresh builder after a one-line change to a source file, restores every earlier layer from the type=gha cache and saves the new ones, like the next pull request. Times are the build step’s wall time, the median of the runs listed; cost is that time at the runner’s list price per minute.

Project and runnerRunsCold buildWarm buildCold, per buildWarm, per build
Atuin server, Sengi 2 vCPU36m13s1m44s$0.019$0.005
Atuin server, Sengi 4 vCPU33m37s1m04s$0.022$0.006
Atuin server, Sengi 8 vCPU31m56s50 s$0.023$0.010
Atuin server, GitHub ubuntu-latest (2 vCPU)111m06s3m40s$0.067$0.022
Umami, Sengi 2 vCPU32m14s2m48s$0.007$0.008
Umami, Sengi 4 vCPU31m52s1m41s$0.011$0.010
Umami, Sengi 8 vCPU31m15s1m38s$0.015$0.020
Umami, GitHub ubuntu-latest (2 vCPU)23m10s4m02s$0.019$0.024
Saleor, Sengi 2 vCPU356 s29 s$0.003$0.001
Saleor, Sengi 4 vCPU347 s31 s$0.005$0.003
Saleor, Sengi 8 vCPU334 s27 s$0.007$0.005
Saleor, GitHub ubuntu-latest (2 vCPU)31m20s39 s$0.008$0.004

The Rust build is where hardware and the cache both pay: Sengi’s 4 vCPU runner built Atuin’s image cold in 3m37s against 11m06s on ubuntu-latest, 3.1× quicker for the same $0.006 a minute, and the 8 vCPU runner in 1m56s. On the same 2 vCPU, Sengi built it in 6m13s at half GitHub’s price per minute. After a one-line change the dependency layer came back from the cache and the warm build took 1m04s on Sengi’s 4 vCPU runner against 3m40s on ubuntu-latest. Umami’s Next.js image built cold in 1m15s on Sengi’s 8 vCPU runner against 3m10s on ubuntu-latest.

What does the layer cache skip, and what does it not?

  • It skips everything above your change. Atuin’s cargo chef cook layer compiles every dependency; with it cached, the warm build compiles only the server’s own crates.
  • It cannot skip the step your change invalidates. Umami’s change lands before next build, so the warm build reruns the whole Next.js build: 1m41s warm against 1m52s cold on Sengi’s 4 vCPU runner. Faster cores are what shorten that step: the same warm build took 4m02s on ubuntu-latest.
  • Saving the cache takes time too. Writing a cache for the first time uploads every layer with mode=max, and each warm build saves its new layers again. Umami’s first cache write took 2m56s on Sengi’s 4 vCPU runner and 4m04s on ubuntu-latest. The JSON file lists the cache write time for every build.
  • Cache mounts stay behind. RUN --mount=type=cache directories are not exported to type=gha, so a fresh runner starts them empty.

Which runner size should build Docker images?

  • 2 vCPU, sengi-standard-2-ubuntu-2404, $0.003 a minute. Small images and images that mostly download: half GitHub’s price for the same size.
  • 4 vCPU, sengi-standard-4-ubuntu-2404, $0.006 a minute. The default for Docker builds: twice the cores of ubuntu-latest at its price per minute, with 16 GB and a 150 GiB volume on local NVMe.
  • 8 vCPU, sengi-standard-8-ubuntu-2404, $0.012 a minute. Rust, C++ and other compile-heavy images, against $0.022 for GitHub’s 8 vCPU runner.

How do I move Docker builds to Sengi?

  1. Connect your repository to Sengi.
  2. Change the build job’s runs-on to sengi-standard-4-ubuntu-2404. Docker, Buildx and Compose are installed.
  3. Merge. The first run fills Sengi’s cache, and every run after it starts warm.

Or paste one prompt into your coding agent and it switches every eligible job and opens the pull request. Per-minute prices for every managed runner are in the runner comparison.

Where is the data?

Every build time, the number of runs behind it, the cache write times, the pinned commits and the prices are in /data/fast-docker-builds-github-actions.json.

Frequently asked questions

How do I cache Docker builds in GitHub Actions?

Create a Buildx builder with docker/setup-buildx-action, then build with docker/build-push-action and set cache-from: type=gha and cache-to: type=gha,mode=max. The first run saves every layer to the Actions cache; the next run restores each layer whose inputs have not changed and rebuilds only the rest.

What is the buildx cache in GitHub Actions?

Buildx is Docker's builder, and its type=gha cache backend stores build layers in the GitHub Actions cache instead of on the runner's disk, which is wiped after every job. On a Sengi runner the same type=gha lines store the layers in Sengi's own cache, with no change to the workflow.

How much faster is a warm Docker build?

It depends on where your change lands in the Dockerfile. In our benchmark the Rust server rebuilt in 1m04s warm against 3m37s cold on Sengi's 4 vCPU runner, because its dependencies sit in a cached layer. The Next.js app's change lands before next build, so its warm build reran the whole build and took 1m41s against 1m52s cold.

Should I use mode=max or mode=min?

mode=max for multi-stage builds. mode=min, the default, saves only the final image's layers, so a builder stage that compiles dependencies is rebuilt on every run. All three builds in our benchmark are multi-stage and use mode=max.

Do RUN --mount=type=cache directories survive between jobs?

No. Cache mounts live on the builder's disk, and the type=gha cache exports layers, not cache mounts. On a fresh runner they start empty, so a step that relies on one, such as a Go build cache, recompiles. Put dependencies in their own layer to keep them between runs.

Which Sengi runner should build Docker images?

sengi-standard-4-ubuntu-2404 for most images, at $0.006 a minute: twice the cores of GitHub's ubuntu-latest for the same price per minute. Move compile-heavy images, such as Rust and C++ services, to sengi-standard-8-ubuntu-2404.

Does Docker layer caching cost extra on Sengi?

No. The Actions cache, and Docker layer caching through type=gha, are included with every Sengi runner. You pay only for runner minutes.

Can I keep pushing images to my registry?

Yes. Set push: true and log in with docker/login-action as you do today. Sengi changes the machine the job runs on, not where your images go.

The fastest hardware for your GitHub Actions.

AMD Ryzen 9 7950X3D desktop CPUs boosting past 5 GHz, on bare metal with local NVMe, a fresh VM for every job, and 2 vCPU jobs ready in 2 seconds. Builds finish up to 1.9× faster than on GitHub’s runners, and at up to half GitHub’s price per minute they cost up to 74% less per build. Change one runs-on line, or give your coding agent one prompt and it opens the pull request for you.

Run your next build on Sengi

See how switching works