Long build times are the silent killer of developer productivity β especially when working with compiled languages like Go. If every commit triggers a 10+ minute container build, your team is burning hours waiting instead of shipping.
In this post, we'll walk through how we reduced build times in a Go microservice pipeline from over 11 minutes to just 39 seconds, using Docker BuildKit, buildx, mount-based caching, and some strategic GitHub Actions enhancements β all without compromising on security or reproducibility.
Our original setup was straightforward β but inefficient:
Each CI run rebuilt our Go service image from scratch. With no caching enabled, Docker:
That led to a consistent 11+ minute wait on every commit.
Here's what our old Dockerfile looked like:
FROM golang:1.24 AS build
WORKDIR /go/src/app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o dist/server cmd/main.go
FROM gcr.io/distroless/static
COPY --from=build /go/src/app/dist/server /server
EXPOSE 8000
CMD ["/server"]
With a few changes to our pipeline and Dockerfile
, we brought the same job down to 39 seconds:
That's a 94% improvement, with no loss in reproducibility or security.
Go projects have a unique structure that makes them highly cacheable β but only if your tooling takes advantage of it.
We followed Docker's official guidance on caching strategies and implemented the following improvements:
Here's our optimized Dockerfile:
FROM golang:1.24 AS build
WORKDIR /go/src/app
RUN go env -w GOMODCACHE=/root/.cache/go-build
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/root/.cache/go-build go mod download
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -o dist/server cmd/main.go
FROM gcr.io/distroless/static
COPY --from=build /go/src/app/dist/server /server
EXPOSE 8000
CMD ["/server"]
This pipeline sets up BuildKit, restores cached layers and Go modules, and builds your image in under a minute β ready for deployment.
name: Release
on:
push:
permissions:
contents: read
id-token: write
jobs:
build-push-server:
runs-on: ubuntu-latest
name: Docker - Publish server OCI to registry
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
id: setup-buildx
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
id: cache
with:
path: cache-mount
key: cache-mount-${{ hashFiles('**/go.sum') }}
restore-keys: |
cache-mount-
- uses: reproducible-containers/buildkit-cache-dance@5b81f4d29dc8397a7d341dba3aeecc7ec54d6361 # v3.3.0
with:
builder: ${{ steps.setup-buildx.outputs.name }}
cache-dir: cache-mount
skip-extraction: ${{ steps.cache.outputs.cache-hit }}
- uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
platforms: linux/arm64
tags: server:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
This pipeline is built on top of Docker's official caching strategies for CI, specifically tailored for GitHub Actions. Here's why each part matters:
With caching solved, we're now looking at:
We'll cover those in an upcoming post. If you're optimizing your own CI/CD pipelines and want to go from "slow and safe" to "fast and secure" stay tuned.
π Reference:Docker CI Cache Management Docs
Back to Blog