buildstats.io

Track Docker image size over time in GitHub Actions

buildstats records the size of every Docker image your CI builds, charts it per commit in your README and comments the delta on pull requests. Free for public repos, 50,000 points per metric; private $9/month.

Image size decides pull time, cold start time and registry cost, and it changes for reasons nobody reviews: a base image bump, an apt cache left behind, a build stage that leaks into the final layer. A size on every push to the default branch gives you the chart; the pull request comment tells you a change added 180 MB before it merges.

Setup: build, inspect, push

After the build, docker image inspect returns the uncompressed size in bytes. The compressed size, which is what a registry pull transfers, comes from the manifest after a push, or from docker save | gzip | wc -c for an estimate that needs no registry.

name: Image

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read
  id-token: write
  pull-requests: write

jobs:
  image:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: docker build -t app:ci .

      - id: size
        run: |
          echo "raw=$(docker image inspect app:ci --format '{{.Size}}')" >> "$GITHUB_OUTPUT"
          echo "gzip=$(docker save app:ci | gzip -1 | wc -c)" >> "$GITHUB_OUTPUT"
          echo "layers=$(docker image inspect app:ci --format '{{len .RootFS.Layers}}')" >> "$GITHUB_OUTPUT"

      - uses: bitgate/buildstats@v1
        with:
          metrics: |
            image_size=${{ steps.size.outputs.raw }} bytes lower
            image_size_gzip=${{ steps.size.outputs.gzip }} bytes lower
            image_layers=${{ steps.size.outputs.layers }} lower
          max-regression: 10

Each line is name=value [unit] [higher|lower]. lower is what makes growth show up as worse in the comment and count against max-regression. With the registry's compressed size available after a push, read it from the manifest with docker manifest inspect or crane manifest and push that instead of the docker save estimate.

Several images from one repository push as variants: variant: api and variant: worker store image_size/api and image_size/worker, which overlay on one chart. Multi-platform builds work the same way with variant: ${{ matrix.platform }}.

What the pull request sees

On pull_request runs the Action compares every metric with the latest value on the base branch and keeps one comment per project up to date, with the base value, the pull request value and the delta in bytes and percent. max-regression: 10 fails the step when an image grew by more than 10%, after the comment and the job summary are written. Make the check required in branch protection and the pull request cannot merge until the image is back under the line. Metrics without a base value yet never trip the gate.

The chart in the README

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://buildstats.io/acme/app/image_size_gzip.svg?theme=dark">
  <img alt="compressed image size over time" src="https://buildstats.io/acme/app/image_size_gzip.svg">
</picture>

The chart shows the last 200 points by default, up to 1,000 with n=1000, between 400 and 1,200 pixels wide. The badge at /image_size_gzip/badge.svg shows the latest value; /badge.json serves the same in the shields.io endpoint format. Public images are cached for 5 minutes.

Limits and pricing

Public repositoriesPrivate repositories
PriceFree$9/month per GitHub user or organization
TrialNot needed14 days, no card
Projects per owner10025
Metrics per project100100
Points per metric and branch50,00050,000
Metrics per push5050
Pushes per minute and project6060
MembersUnlimitedUnlimited

Full details are on the pricing page.

Other ways to do this

dive and docker-slim show what is inside an image and run locally. Registries show the size of each tag, but not as a series over commits, and not on the pull request. A GitHub Actions step can print the size to the job summary, which disappears with the run. Pushing the number to buildstats keeps it for the life of the project and puts it where people look: the README and the pull request.

Questions

Uncompressed or compressed size?

Both, as two metrics. The uncompressed size from docker image inspect is what the disk holds; the compressed size is what a pull transfers. The gzip estimate from docker save is close to the registry's number and needs no push.

Does buildstats pull or store the image?

No. It stores the metric name, the value and the unit, plus the commit it came from. The image never leaves your runner or registry.

Can I track several images from one repository?

Yes, as variants: image_size/api and image_size/worker overlay on one chart with up to 8 colours, and the pull request comment lists each one.

Does this work outside GitHub Actions?

Yes. Create a project API key and POST the same metrics with curl from GitLab CI, Jenkins or a build box. The Action is a shell wrapper around that call.

What happens when a base image bump adds 200 MB?

The pull request comment shows the delta against the base branch, and with max-regression set the step fails. Merge it anyway and the chart on the default branch shows the step on that commit.

Last updated 2026-10-07.