Checkup
Checkup finds the Docker build steps that redo work on every commit, measures what they cost on your BuildPulse runner builds, and writes the fix. It is opt-in, per organization.
What Checkup is
Docker skips a build step when nothing it depends on changed, and reuses the layer it made last time. A few common Dockerfile patterns break that, so the step runs again on every commit even though its inputs did not change. Each one is small. Together they decide how long your image builds take.
Checkup looks for those patterns in two places: in your Dockerfiles, and in the Docker builds your jobs run on BuildPulse runners. A finding names the file and line, explains the cause and the effect, shows what it cost on your real builds when Checkup can time it, and comes with a fix.
Checkup works with jobs on bp-docker-builder runners. It does not change your builds, and it never pushes to your repository on its own.
Turn it on
Checkup is off until an owner or admin of your BuildPulse organization turns it on. There are two places to do that:
- Organization settings, under Checkup: flip the switch.
- The Checkup tab under Runners: it explains what Checkup reads and has a Turn on Checkup button.
Findings appear on the Checkup tab under Runners after your next Docker builds on BuildPulse runners. Turning Checkup off stops collection at once. The findings already on the tab stay there.
What it reads, and what never leaves the runner
When a docker-builder job finishes for an organization with Checkup on, the runner writes a short build summary. It holds metadata only:
- Each build step as BuildKit names it, whether it came from the cache, and how long it took.
- The build arguments, with every value replaced by a short hash on the runner.
- The layers of the image the build pushed: their digests and compressed sizes, and how long exporting and pushing them took.
- The repository, commit, workflow job and runner label the build ran on.
Build logs and source code never leave the runner host. Build argument values can be tokens, so they are hashed before the summary is written, and any place a value was substituted into a step name reads ${NAME} instead.
To explain a finding and write its fix, Checkup reads your Dockerfiles, .dockerignore files and workflow files through the BuildPulse GitHub App. For the large per-commit layer rule it also reads the file list of your repository at the build's commit, which gives it file sizes but not file contents.
What the app says when you turn it on
Checkup looks at the build steps, timings and Dockerfiles of jobs that run on BuildPulse runners and shows what wastes time and upload bytes. Build argument values are hashed on the runner and never leave it.
The rules
Each rule has a severity. High means it costs real time on most builds; low means it is cheap today but grows with the repository.
Commit argument declared too early
A workflow passes a value that changes on every commit, like github.sha, as a build argument. The Dockerfile declares that ARG near the top of a stage, above RUN steps that never read it.
BuildKit hands every declared ARG to each RUN step below it as an environment variable. A new value means a new cache key, so every one of those steps runs again on every commit, and the layers they make are uploaded again on every push.
The fix moves the ARG down to the first step that uses it. Checkup writes this one for you, and moves any ENV line that only re-exports the value along with it. If a step above reads the value from its environment without naming it, keep the ARG above that step.
Dependencies installed after a full copy
COPY . . sits above the step that installs dependencies. Every commit changes some file in the build context, so the copy makes a new layer and the install below it runs from scratch.
Copy the dependency manifests first, install, then copy the rest. The install then stays cached until a manifest or lockfile changes. Checkup shows this change as a starting point rather than a pull request, because which files the install needs depends on your layout: a workspace monorepo also needs each package's manifest.
No .dockerignore
A stage copies the whole build context and no .dockerignore applies to it. The context then carries .git, local node_modules and build output into the image. They make the layer bigger, and .git alone changes on every commit.
Add a .dockerignore at the root of the build context. Start with .git, then add anything else the image does not need.
Large per-commit layer
Copying your source makes a new layer on every commit, and that is expected. This rule speaks up only when that layer is big enough to matter: a build that pushed at least 100 MB of new layers, where the largest new layer came from a COPY or ADD of the build context and made up at least half of what was uploaded.
Checkup can only see this on a real build, so the free public audit never reports it. The finding shows the layer's compressed size, its share of the push, the seconds it took, and a breakdown of the biggest directories the copy takes. Breakdown sizes are uncompressed source sizes, read from your repository's file list after the .dockerignore. When GitHub cuts that list short for a very large repository, the page says the breakdown is partial.
The levers are a smaller context: copy only the directories the image needs, and keep everything else out with .dockerignore. When there are safe entries to add (.git, node_modules, coverage output) and the build context already has a .dockerignore, Checkup writes the added lines as a change to that file, which you can open as a pull request. The fix pull request for this rule only ever changes the .dockerignore, never the Dockerfile. Anything beyond those entries depends on what your image needs at runtime, so it is left to you.
How time and cost are measured
Every number Checkup shows is measured on your BuildPulse runner builds. None of them are estimates.
- Commit argument declared too early: Checkup compares two consecutive builds of the same image. A step counts when it missed the cache, has the changed argument in its environment, does not use it, and every step it builds on was cached. The time is the sum of those steps. It is a floor: steps further down also reran and are not counted.
- Large per-commit layer: the build reports how long it spent exporting and pushing layers. The layer gets its share by size: (export seconds + push seconds) x layer bytes / all new bytes.
- Dependencies installed after a full copy and No .dockerignore come from reading files, once a day per repository. They have no measured time, and the list shows a dash for them.
Per finding, Checkup keeps a daily count of builds and seconds. The tab shows the last 7 days ending at the latest build that showed the problem: builds in that window, total minutes, and seconds per build as the total divided by the builds. The detail page shows the working, for example 41 builds x 14 s = 9.6 min.
For the large per-commit layer, Checkup also prices those minutes at the list price per minute of the runner label the build ran on, for example $0.15 at $0.016/min on bp-docker-builder-x64-8x. That is the list price, before any free minutes or discounts on your bill.
Fix pull requests
When Checkup wrote the fix, the finding’s page shows it as a diff against the file on your default branch, with an Open fix PR button for owners and admins. One click makes one commit on a new branch named buildpulse/checkup-<rule>-<id> and opens one pull request. It changes that one file and nothing else, and the description says what was found, what it cost, how to check it and how to undo it.
- The BuildPulse GitHub App needs
contents: writeandpull_requests: writeon the repository. Without write access, the page offers the diff to copy instead, with a link for an owner to approve the permission. - If the file changed since the page loaded, nothing is written and the page asks you to reload and review the new diff.
- A finding gets one pull request. Clicking again shows the one already open.
- Checkup never merges anything. Review and merge the pull request like any other.
Fixed and dismissed findings
A finding moves to Fixed on its own when newer builds stop showing it, or when the daily file check no longer finds it. Fixed findings keep the time they cost per build before the fix. If the problem comes back, Checkup reopens the finding.
Owners and admins can dismiss an open finding they do not want to fix. A dismissed finding stays dismissed even if newer builds still show it, and it stays on the tab for the record. To bring one back, email support@buildpulse.io.
The free public audit
Anyone can audit a public GitHub repository at buildpulse.io/checkup without signing up. It reads the Dockerfiles, .dockerignore files and workflows at the latest commit of the default branch, through the GitHub API. Nothing is cloned, built or run.
- It checks the three rules a file alone can show. The large per-commit layer needs a real build, so it is not part of the public audit.
- It reports no times, because a file cannot say how long a step takes.
- For a commit argument declared too early it shows the fix Checkup wrote. For the other rules it shows a starting point to adapt.
- Private repositories are not supported there. Turn Checkup on in your BuildPulse org instead.
- Audits are rate limited per visitor, and a bot check runs in the background.
- Leaving your email is optional. It goes to a person at BuildPulse who replies about that repository.
A report has its own link, buildpulse.io/checkup?repo=owner/name, so you can send it to whoever owns the Dockerfile.
Measure your Docker builds on BuildPulse runners
Move a Docker build to a bp-docker-builder runner and turn Checkup on in org settings. Findings appear after the next builds.
3,000 free runner minutes a month · No credit card required