How can I find the minimal needed Docker image starting point?

Hi,
I have the usecase where I want to precombile a go binary and use it as a microservice in a docker network.

I build with this:

 CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o kardis

and my Dockerfile is this:

FROM ubuntu:noble

WORKDIR /app
COPY kardis .

EXPOSE 6380
ENTRYPOINT ["/app/kardis"]

This works, but if I want to build from scratch I get this error message

/lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.34' not found (required by /app/kardis)

I understand that there is stuff needed for my binary. But now the question: How can I find the minimal needed Docker image starting point? Any advice?

This is a kernel problem of the operating system, you can upgrade the kernel of the ubuntu image, or downgrade the golang version (you have to check the logs for which kernels golang version supports).
But usually, golang releases are good enough for the current mainstream kernel versions, and you may need to downgrade golang releases if you have special needs.

Thanks for the more detailed explanation. But maybe I was not clear enough:
I want to choose the smallest (or best) working starter image possible.

I think using ubuntu:latest is enough to get a lot done, and it’s a good choice to compile internally if you can (and remove golang stuff after you compile).
Compiling internally reduces a lot of compatibility issues.

1 Like

I ran into this deploying to google cloud run with a distroless (aka very slim) docker image. I decided to just downgrade to go 1.19 at the time and it works fine. According to this comment building on a machine with the same version of glibc might make it use older symbols (which I believe is what @peakedshout is referring to).

I’m not sure if it’s the smallest image, but have you tried alpine?

A minimal Docker image based on Alpine Linux with a complete package index and only 5 MB in size!

I created a minimal image that serves web traffic with the following main.go:

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	// Hello world, the web server
	helloHandler := func(w http.ResponseWriter, req *http.Request) {
		io.WriteString(w, "Hello, world!\n")
	}
	port := ":6830"
	fmt.Println("Starting web server on", port)
	http.HandleFunc("/hello", helloHandler)
	log.Fatal(http.ListenAndServe(port, nil))
}

With this as my Dockerfile:

# syntax=docker/dockerfile:1

## Build our binary
FROM golang:latest AS build
WORKDIR /app
# Copy our source code
# NOTE: usually here I would copy over go.mod and go.sum 
# and then run `RUN go mod download`. For simple demo though
# we don't even have a module.
COPY main.go ./
# Build our package 
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o kardis main.go

## Pull our binary to smaller alpine image for final image
FROM alpine:latest
WORKDIR /app
# Copy binary from build stage
COPY --from=build /app/kardis .
EXPOSE 6380
ENTRYPOINT ["/app/kardis"]

Note I’m using build stages there so I can use that large golang image but it won’t affect our final image size. Anyway, I built it and ran it:

# Build/tag
docker build -t kardis .
# Run with exposed port to verify it is working:
docker run -p 6830:6830 kardis

… and then checked the image size and it is pretty small:

REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
kardis       latest    1b01dcc2b490   11 minutes ago   12.9MB

You could PROBABLY get something smaller than 12.9mb (some of that is obviously our binary too) but there’s going to be a point of diminishing returns. That said, I was able to change my FROM to scratch and my dockerfile above worked fine. Have you tried building with cgo disabled and -a? From the docs:

 -a
	force rebuilding of packages that are already up-to-date.