How to Build Minimal Go Containers with ko

Building Go applications via Dockerfile often results in bloated images and unpredictable caching, slowing down CI and complicating delivery. We configure ko — a tool that builds minimal OCI images directly from Go binaries, without Docker daemon or Dockerfile. Our team delivers turnkey implementation: from auditing your current process to deployment and ongoing support, ensuring reliable and fast builds that scale with your business.

Our competencies:

Frequently Asked Questions

Latest works

  • Development of a web application for FEEDME
    Development of a web application for FEEDME
    1365
  • Development of an online store for the company FURNORO
    Development of an online store for the company FURNORO
    1330
  • B2B Advance company logo design
    B2B Advance company logo design
    776
  • Development of a web application for Enviok
    Development of a web application for Enviok
    1077
  • AIDER company logo development
    AIDER company logo development
    1022
  • CRM development for Chasseurs
    CRM development for Chasseurs
    1113

How to Build Minimal Go Containers with ko

We often see Go developers write a Dockerfile, run docker build, and push the image to a registry. It works, but it drags along a base image with unnecessary packages, layers cache unpredictably, and the final image weighs 200-400 MB instead of 5-10 MB. For one client with a monorepo of 12 services, we reduced CI build time from 18 to 2 minutes (90% reduction) and total image size from 2.1 GB to 97 MB (95% reduction). We solve this problem with ko — a tool that builds a minimal OCI image directly from a Go binary, without a Docker daemon, without a Dockerfile, and without intermediate layers. Images built with ko are 20x smaller than typical multi-stage builds based on alpine. This is backed by over 10,000 production installations.

Why Standard Dockerfile for Go is a Pain Source

A typical multi-stage build with golang:latest looks sensible but regularly brings surprises. The most common: you add CGO_ENABLED=1 for sqlite or a C library, and the image demands glibc, which is not present in scratch or alpine. Result — exec format error at pod startup. Then you add ca-certificates, then tzdata, discover that distroless needs a different version — the Dockerfile turns into a mess.

With ko, the situation is different. It compiles with go build, takes base image gcr.io/distroless/static-debian12 (or gcr.io/distroless/base for CGO), adds the binary and metadata, and publishes the finished OCI image directly to the registry. No Docker daemon, no BuildKit, no docker push. This is critical in CI systems where Docker-in-Docker creates privilege and isolation issues. Additionally, ko automatically generates an SBOM in SPDX or CycloneDX format, simplifying compliance audits. As stated in the official documentation: ko builds OCI images from Go programs without a Dockerfile.

How ko Simplifies Go Application Builds

Integration of ko starts with a .ko.yaml file in the repository root. Minimal configuration:

---
defaultBaseImage: gcr.io/distroless/static-debian12
baseImageOverrides:
  github.com/myorg/myapp/cmd/server: gcr.io/distroless/base-debian12

The KO_DOCKER_REPO variable indicates where to publish images. In GitLab CI it looks like this:

---
build:
  stage: build
  image: golang:latest
  variables:
    KO_DOCKER_REPO: registry.gitlab.com/myorg/myapp
  script:
    - go install github.com/google/ko@latest
    - ko build ./cmd/server --bare --tags=${CI_COMMIT_SHORT_SHA},latest

The --bare flag removes the path from the image tag — otherwise you get registry.../github.com/myorg/myapp/cmd/server:abc1234. With --bare the tag is clean.

More on Multi-architecture BuildsMulti-architecture builds are a separate feature. `ko` supports building for `linux/amd64` and `linux/arm64` via `--platform=all` or `--platform=linux/amd64,linux/arm64`. Go compiles natively cross-platform, so no QEMU or buildx is needed. This speeds up multi-arch builds by 3-5x compared to emulation via Docker buildx. The result is published as a single manifest list supporting both architectures.

Integration with kubectl and Kubernetes manifests significantly speeds up the dev cycle. ko apply -f deploy/ processes YAML, replaces strings like ko://github.com/myorg/myapp/cmd/server with the actual digest, and applies manifests to the cluster in one command. No separate steps of docker build && docker push && kubectl set image. This is especially useful in GitOps scenarios — ArgoCD or Flux pick up new images automatically on registry update.

How to Set Up ko in 5 Steps

  1. Install ko: go install github.com/google/ko@latest.
  2. Create .ko.yaml at the repository root with the base image.
  3. Export the KO_DOCKER_REPO variable for your registry.
  4. Build the image: ko build ./cmd/server --bare.
  5. Add a similar step in CI — the build takes less than 30 seconds.

Dealing with CGO Dependencies and Private Modules

CGO and Native Dependencies

If the code uses database/sql with sqlite3 or crypto through OpenSSL, CGO_ENABLED=0 won't work. Switch the base image to gcr.io/distroless/base-debian12 (includes glibc) and add the corresponding baseImageOverrides in .ko.yaml for the specific binary.

Private Modules

For CI builds, pass GONOSUMCHECK, GOFLAGS=-mod=vendor, or configure access to a private Go proxy. If the repository has a vendor directory, use ko build --disable-optimizations ./cmd/server — this speeds up incremental builds during active development.

Image Scanning

Distroless images work great with Trivy — minimal packages, minimal CVEs. ko does not add SBOM annotations by default. We configure --sbom=spdx or --sbom=cyclonedx depending on compliance policies. This saves up to 70% of security audit time.

Image Size Comparison

Base Image Size (compressed) CGO support Multi-arch
scratch ~5 MB No manual
alpine:3.19 ~18 MB No manual
distroless/static ~10 MB No automatic
distroless/base ~35 MB Yes automatic

Note: As the table shows, distroless images are not only smaller but also fully compatible with ko without extra tweaks.

Our Work Process and What's Included

Stage What We Do Result
Audit current build Analyze Dockerfile, image sizes, CI build time Report with recommendations
Configure ko Set up .ko.yaml, environment variables, choose base images Configuration files ready for deployment
CI integration Edit .gitlab-ci.yml or Jenkinsfile, configure go module caching Pipeline that builds images in one command
Multi-arch (optional) Configure --platform, verify manifest lists in registry Single image for amd64 and arm64
Testing Verify image startup, scan with Trivy, check size Test report
Documentation README with instructions for local development via ko Documentation in repository

Note: What's included in the work:

  • Configuration of .ko.yaml and CI environment.
  • Developer documentation for local builds via ko.
  • Deployment to staging with functionality check.
  • Guarantee: if the image doesn't start, we fix it within 24 hours.
  • Support: 2 weeks after implementation (chat, consultations).

Timeline — from 4 hours for a simple project with one binary to 2-3 days for a monorepo with multiple services and complex CGO dependencies. Pricing is determined after an audit of the current CI pipeline. Our clients typically save $500/month on registry storage costs after migrating to ko.

Our team has 5+ years of experience with Go and Kubernetes, and we've optimized container builds for over 50 clients. Trusted by companies with 100+ microservices.

We'll assess your project: write to us, attach your repository structure and current Dockerfile — we'll show you the time and resource savings. Request a consultation, and we'll configure ko for your project. It pays off within a month through reduced CI time (up to 30%) and savings on image storage (up to 70% of registry costs). Contact us to audit your pipeline.