Docker Builds on GitHub Actions: Remote Builders
Docker builds crawl on GitHub Actions because the layer cache dies with the runner. Move the build to a WarpBuild remote builder profile in one swap.
Last verified:
Docker builds are slow on GitHub Actions because every job starts on a fresh runner with an empty layer cache, so BuildKit rebuilds layers that have not changed and then spends more time exporting the cache back over the network. WarpBuild remote Docker builders move the build onto a dedicated builder VM that keeps its layer cache on local disk between builds, and the workflow change is one action swap plus a profile name.
This page covers the exact configuration, a before and after workflow, how a builder profile shares its cache and its billing session across parallel jobs, how to size a profile against your matrix width, and what the whole arrangement costs per month.
Overview
A Docker build on a GitHub-hosted runner pays four costs that have nothing to do with compiling your application.
The first is a cold layer store. The runner is ephemeral, BuildKit starts with nothing in /var/lib/buildkit, and every layer in the Dockerfile is a miss on the first pass.
The second is cache transport. The usual fix is cache-to: type=gha,mode=max and cache-from: type=gha, which writes every layer blob into the GitHub Actions cache at the end of the build and pulls it back at the start of the next one. That turns local disk reads into network round trips, and it competes for a shared budget: GitHub limits the total size of all caches in a repository to 10 GB by default and evicts entries that have not been accessed in seven days (GitHub Actions dependency caching docs, checked on 2026-08-13). A few multi-stage images with fat base layers exhaust that budget, and once eviction starts, the cache misses come back.
The third is the runner shape. A 2 vCPU runner runs npm ci, go build, or a Gradle assembly inside a RUN layer on two cores, and no amount of caching helps a build step that is compute bound.
The fourth is emulation. Building linux/arm64 on an x64 runner without a native builder means QEMU, which turns a compile step into an interpreted compile step.
A remote builder profile addresses the first two directly. Remote Docker builders run outside the GitHub Actions runner with a persistent layer cache on local disks, so repeat builds reuse layers instead of rebuilding them. The runner keeps its normal job: check out the repository, send the build context to the builder, and receive the resulting image or push it to a registry. The build itself happens on hardware sized for building.
Builders work with WarpBuild runners and with non-WarpBuild runners, including GitHub-hosted ones, because the connection is made over the API rather than through the runner image.
Two companion pages go deeper on the mechanism and on the cache design: how remote Docker builders work on GitHub Actions covers the builder lifecycle and the buildx remote driver, and Docker layer caching on GitHub Actions covers Dockerfile ordering and what actually invalidates a layer.
Configuration
Three steps. Create a builder profile, swap the action, delete the buildx setup step.
Step 1: create a builder profile
Go to the Docker Builders page in the dashboard and create a profile. A profile carries three settings that matter later: the machine size, the architectures it serves (amd64, arm64, or both), and its name, which is the string your workflow references. Name it after the thing it builds, for example app-images-x64, because the profile name is also the cache boundary.
Step 2: swap the action
WarpBuild ships a drop-in replacement for docker/build-push-action. The replacement takes the same inputs plus profile-name.
Before, on a GitHub-hosted runner with the Actions cache backend:
name: build-image
on:
pull_request:
jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=maxAfter, on a WarpBuild runner with a builder profile:
name: build-image
on:
pull_request:
jobs:
docker:
runs-on: warp-ubuntu-latest-x64-4x
steps:
- uses: actions/checkout@v4
- name: Log in to the registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: Warpbuilds/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
profile-name: "app-images-x64"
timeout: 600000The whole change, as a diff:
jobs:
docker:
- runs-on: ubuntu-latest
+ runs-on: warp-ubuntu-latest-x64-4x
steps:
- uses: actions/checkout@v4
- - name: Set up Buildx
- uses: docker/setup-buildx-action@v3
-
- name: Build and push
- uses: docker/build-push-action@v6
+ uses: Warpbuilds/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
- cache-from: type=gha
- cache-to: type=gha,mode=max
+ profile-name: "app-images-x64"
+ timeout: 600000Step 3: drop the buildx setup step
Remove docker/setup-buildx-action when its only purpose was creating a builder. The WarpBuild action assigns a builder from the profile, creates a buildx instance backed by the remote driver, runs the build there, and closes the session when the job finishes. timeout is the wait for the builder to become ready in milliseconds, and it defaults to ten minutes.
Drop cache-from and cache-to at the same time. The builder profile keeps layers on its own disk, so the export and import round trips have nothing left to do.
Running from a non-WarpBuild runner
On a GitHub-hosted runner, or any other runner, add an API key. Everything else is identical.
jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and push
uses: Warpbuilds/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
profile-name: "app-images-x64"
api-key: ${{ secrets.WARPBUILD_API_KEY }}
timeout: 600000The API key is required only off WarpBuild runners, where the runner already carries its own credentials.
Multi-architecture builds
Set platforms and point the job at a profile that has both architectures enabled in the dashboard. A profile with only amd64 enabled fails an arm64 build with exec format error.
jobs:
images:
runs-on: warp-ubuntu-latest-x64-4x
strategy:
fail-fast: false
matrix:
service: [api, worker, web, migrator, cron, docs]
steps:
- uses: actions/checkout@v4
- name: Build and push ${{ matrix.service }}
uses: Warpbuilds/build-push-action@v6
with:
context: services/${{ matrix.service }}
push: true
tags: ghcr.io/${{ github.repository }}/${{ matrix.service }}:${{ github.sha }}
platforms: linux/amd64,linux/arm64
profile-name: "app-images-multi"Each architecture runs on its own builder instance, which has a billing consequence covered under Sizing. For jobs that need a native ARM64 runner rather than a native ARM64 builder, the same workflow runs on warp-ubuntu-latest-arm64-4x; see Linux ARM64 runners for GitHub Actions and multi-platform Docker builds for amd64 and arm64.
Outside GitHub Actions
Builders are assigned with a POST to /api/v1/builders/assign carrying the profile name, polled at /api/v1/builders/{id}/details until the status is ready, wired into buildx with docker buildx create --driver remote and the returned TLS certificates, and released with a POST to /api/v1/builder-session-requests/complete. The full sequence, including the certificate handling and the cache reset endpoint, is in the Docker builders documentation. Billing runs until the session is completed, so the release call belongs in a trap or an always block.
Runners themselves can sit anywhere. WarpBuild cloud runners are the default, BYOC runs on AWS, GCP, and Azure, and Terraform support exists for BYOC on AWS. A builder profile serves all of them the same way.
Sizing
Two machines get sized here, and they are independent. The size of the Docker builder has no relationship to the size of the GitHub Actions runner.
The builder
The documented guidance sets a floor. There is no limit on the number of builds that can run concurrently on a builder profile, and the recommended minimum resource requirement is approximately 8 vCPU and 16GB memory per concurrent build job.
Multiply that by the widest fan-out that hits the profile at once. A six way matrix needs about 48 vCPU and 96GB. A twelve way matrix needs about 96 vCPU and 192GB, and at that width the architecture limits start to matter.
| Builder size | Price per minute | Architectures | Price per vCPU minute | Concurrent jobs at the 8 vCPU floor |
|---|---|---|---|---|
| 16 vCPU, 32GB, 100GB disk | $0.06 | amd64, arm64, multi | $0.00375 | 2 |
| 32 vCPU, 64GB, 200GB disk | $0.12 | amd64, arm64, multi | $0.00375 | 4 |
| 64 vCPU, 128GB, 200GB disk | $0.24 | amd64, arm64, multi | $0.00375 | 8 |
| 96 vCPU, 192GB, 600GB disk | $0.36 | amd64 | $0.00375 | 12 |
| 96 vCPU, 192GB, 2TB disk | $0.52 | amd64 | $0.0054 | 12 |
| 192 vCPU, 384GB, 600GB disk | $0.72 | amd64 | $0.00375 | 24 |
| 192 vCPU, 384GB, 2TB disk | $0.88 | amd64 | $0.0046 | 24 |
Rates from the WarpBuild pricing page, checked on 2026-08-13. The arm64 and multi-architecture profiles top out at 64 vCPU, so a matrix wider than eight concurrent multi-architecture builds needs a second profile rather than a bigger one. The two large disk options exist for build contexts and layer stores that outgrow 600GB, and they are the only two rows where the price per vCPU minute moves.
The runner
The runner ships the build context and waits. A 4 vCPU runner covers that for most repositories, and the money follows the builder rather than the runner.
| Runner label | vCPU | RAM | Storage | Price per minute | Price per vCPU minute |
|---|---|---|---|---|---|
| warp-ubuntu-latest-x64-2x | 2 | 8GB | 150GB SSD | $0.004 | $0.002 |
| warp-ubuntu-latest-x64-4x | 4 | 16GB | 150GB SSD | $0.008 | $0.002 |
| warp-ubuntu-latest-x64-8x | 8 | 32GB | 150GB SSD | $0.016 | $0.002 |
| warp-ubuntu-latest-arm64-4x | 4 | 16GB | 150GB SSD | $0.006 | $0.0015 |
| warp-ubuntu-latest-arm64-8x | 8 | 32GB | 150GB SSD | $0.012 | $0.0015 |
Rates from the WarpBuild pricing page and the cloud runners documentation, checked on 2026-08-13.
Compare the two price per vCPU minute columns before you decide what to move. Builder cores list at $0.00375 per vCPU minute and runner cores list at $0.002 per vCPU minute, so a builder profile is a more expensive way to rent CPU. What you buy with the difference is a layer cache that survives between jobs and a machine that several jobs share at once. A build that misses the cache on every layer anyway is better off staying on a larger runner.
Worked cost model, single image
Assumptions: 1,000 image builds per month, each build occupying the builder for 4 minutes, each runner job lasting 6 minutes, one 16 vCPU profile.
| Line | Arithmetic | Monthly |
|---|---|---|
| Builder sessions | 1,000 x 4 min x $0.06 | $240.00 |
| Runner minutes | 1,000 x 6 min x $0.008 | $48.00 |
| Total | $288.00 |
The runner line is worth checking against the GitHub-hosted list price for the same shape. Those 6,000 runner minutes on the 4 vCPU, 16GB GitHub-hosted Linux larger runner list at $0.012 per minute, which is $72.00 against $48.00 on warp-ubuntu-latest-x64-4x (GitHub Actions billing reference and GitHub pricing, both checked on 2026-08-13).
Charges apply to both machines. Runner minutes and builder session minutes are separate resources on the invoice.
Worked cost model, six way matrix
Assumptions: 300 pull request builds per month, each building six images in parallel on one amd64 profile, each job running 6 minutes with 4 minutes of builder time, and a session that stays open 7 minutes from the first builder start to the last job completion. Six concurrent jobs at the 8 vCPU floor means 48 vCPU and 96GB, so the profile is the 64 vCPU, 128GB size at $0.24 per minute.
| Line | Arithmetic | Monthly |
|---|---|---|
| Builder sessions | 300 x 7 min x $0.24 | $504.00 |
| Runner minutes | 300 x 6 jobs x 6 min x $0.008 | $86.40 |
| Total | $590.40 |
The same 10,800 runner minutes on the GitHub-hosted 4 vCPU larger runner list at $0.012 per minute, which is $129.60 against $86.40 (GitHub billing reference, checked on 2026-08-13).
Switch that profile to multi-architecture and the builder line doubles to $1,008.00 per month, because each architecture opens its own session and the two are billed independently. Build arm64 images only where you ship them.
Bottlenecks
One profile is one VM
Each builder profile corresponds to one optimized, dedicated Docker builder virtual machine with caching. Multiple jobs can run on the same builder profile in parallel, and those jobs run on the same VM in parallel. That single sentence explains most of the surprises teams hit.
Parallel jobs on a profile share CPU, memory, and disk. Under-provision the profile and a wide matrix keeps working while every job slows down. Six jobs on a 16 vCPU profile get under 3 vCPU each while the sizing guidance asks for 8.
Parallel jobs also share the cache, and the sharing is eventually consistent. Layers written by one build may not be visible to another build running at the same moment, and they become available to later builds after synchronization. A matrix of six images that all start from the same base layer will, on the first run, have several of them build that base layer independently. The second run reads it from cache.
Cache invalidation follows layer changes in the ordinary Docker way. Editing a file that a COPY instruction pulls in invalidates that layer and every layer after it, on a remote builder exactly as on a local one. Ordering the Dockerfile so dependency manifests are copied and installed before application source is the change that decides your hit rate, and no builder makes a badly ordered Dockerfile cache well.
Cache lifetime has a floor and a ceiling. A profile that goes unused for more than 10 days is reset automatically, and you can reset it deliberately through the API when a poisoned layer needs clearing.
Sessions, timeouts, and the shape of a job
Billing is per session, measured from when the builder action starts until the job completes. Overlapping jobs on one profile share one session, billed from the first start to the last completion. A job that assigns a builder early and then spends 15 minutes running tests before the build step keeps that session open the whole time. Put the build step next to the builder setup, or use Warpbuilds/docker-configure immediately before the build when you need custom build commands.
Builders carry their own idle timeout so an abandoned session does not bill forever. Deleting a builder profile while builds are in flight is a different problem: wait for the builds to finish first.
What a remote builder does not fix
It does not speed up test steps, application compiles that live outside the Docker build, or registry pushes bound by your own network. It does not repair a Dockerfile that copies the entire repository before installing dependencies. It does not shrink a 4GB build context that has no .dockerignore, and the context still travels from the runner to the builder on every build.
Watch for the case where the base image itself is the cost. An image that pulls a 2GB base layer on every build spends its time on registry reads, and a builder profile helps there only after the first build has that layer on local disk.
Proof
The Warpbuilds/build-push-action repository is public, and its end to end workflow is the clearest available evidence of the configuration on this page. The warp-e2e workflow runs on runs-on: warp-ubuntu-latest-x64-4x, builds a test Dockerfile with profile-name: super-fast-builder, and then asserts the result rather than trusting it. The workflow inspects the active buildx instance and fails unless the driver is remote and the node endpoint is a tcp:// address, which proves the build left the runner. It then rebuilds the same image and checks that the digest matches, which proves the profile served the layers from cache on the second pass.
That workflow runs on pull requests and pushes to main, so the evidence is current and anyone can read the logs. The WarpBuild runner agent is open source as well, if you want to see what runs on the runner side.
Named customer results and logos live on the WarpBuild customers page.
Every cost number on this page carries its arithmetic, its source, and a checked-on date, and the same numbers appear on the pricing page. Start with the $10 free credits, point one noisy image build at a 16 vCPU profile, and read your own invoice before moving the rest.
FAQ
Do I still need docker/setup-buildx-action?
No, if the only job that step did was create a buildx builder. Warpbuilds/build-push-action creates a remote buildx builder from your profile, points the build at it, and tears it down afterwards. Keep docker/setup-buildx-action only if you use it for something else, such as installing a specific BuildKit version for a build that stays on the runner.
Do I need cache-from and cache-to with a builder profile?
No. Each builder profile maps to one dedicated builder VM that keeps the layer cache on its local disk, so a repeat build reuses layers without any cache export or import step. Remove the cache-from and cache-to lines that pointed at type=gha, and the 10 GB per repository GitHub Actions cache limit stops applying to your image layers.
How is a builder profile billed when five jobs build in parallel?
Builders are billed per session. Jobs on the same profile with overlapping execution share one session, billed from the first job's builder start to the last job's completion. A multi-architecture profile runs each architecture on a separate builder instance, so it opens one session per architecture and each is billed independently.
How large should a builder profile be?
The documented recommended minimum is approximately 8 vCPU and 16GB memory per concurrent build job on the profile. A six way matrix that builds all six images at once needs about 48 vCPU and 96GB, so the 64 vCPU, 128GB, 200GB profile at $0.24 per minute is the first size that clears it.
Start with $10 in free credits
Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.