10 August 2022

Application & Containers

How to optimize the containerization of your application.

Sidorenko Konstantin
Sidorenko Konstantin thecampagnards

In this post I will take the example of a Go application but it can be well applied to other languages.

Scratch

Scratch is a reserved image of size 0 with nothing in it. Using a scratch image reduces the size of a final Docker image by ~50% compare to an alpine image! Not bad, right?

Except that the image can’t do SSL certificate verification because of the missing SSL certificates!

Writing a simple Go application that request https://google.com, and package it inside scratch:

FROM golang:1.19-alpine as builder

WORKDIR /go/src/app
RUN go mod init example
RUN echo -e 'package main\n\
import "net/http"\n\
func main() {\n\
	if _, err := http.Get("https://google.com"); err != nil {\n\
		panic(err)\n\
	}\n\
}' > main.go

RUN CGO_ENABLED=0 go build -a -ldflags "-s -w" -o /go/bin/app

FROM scratch
COPY --from=builder /go/bin/app /
CMD ["/app"]

Go playground: https://go.dev/play/p/U9_V7aoIiaZ.

panic: Get "https://google.com": x509: certificate signed by unknown authority

goroutine 1 [running]:
main.main()
        /go/src/app/main.go:5 +0x4c

As you can see, it is impossible to request an https URL. You’ll have to do this to make it work:

FROM alpine:3.16 as builder-cert

RUN apk add --no-cache ca-certificates

FROM golang:1.19-alpine as builder-go

WORKDIR /go/src/app
RUN go mod init example
RUN echo -e 'package main\n\
import "net/http"\n\
func main() {\n\
	if _, err := http.Get("https://google.com"); err != nil {\n\
		panic(err)\n\
	}\n\
}' > main.go

RUN CGO_ENABLED=0 go build -a -ldflags "-s -w" -o /go/bin/app

FROM scratch
# import the certs to make them available in scratch
COPY --from=builder-cert /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder-go /go/bin/app /
CMD ["/app"]

Personally, you can opt for scratch if you don’t need the SSL certificates. But I think it’s better to use a ~2 MiB distroless images, which we’ll see now.

Distroless

Distroless images contain only your application and its runtime dependencies. They do not contain package managers, shells or other programs that you would expect to find in a standard Linux distribution.

Limiting the contents of your runtime container to precisely what is needed for your application is a good practice used. It improves the signal-to-noise ratio of scanners (e.g. CVE) and reduces the burden of establishing provenance to what you need.

Google Images

The smallest distroless image, gcr.io/distroless/static-debian11, is about 2 MiB. This is about 50% of the size of Alpine (~5 MiB), and less than 2% of the size of Debian (124 MiB).

Dockerfile using the distroless image:

FROM golang:1.19-alpine as builder

WORKDIR /go/src/app
RUN go mod init example
RUN echo -e 'package main\n\
import "net/http"\n\
func main() {\n\
	if _, err := http.Get("https://google.com"); err != nil {\n\
		panic(err)\n\
	}\n\
}' > main.go

RUN CGO_ENABLED=0 go build -a -ldflags "-s -w" -o /go/bin/app

FROM gcr.io/distroless/static-debian11
COPY --from=builder /go/bin/app /
CMD ["/app"]

Results

Overview of different sizes you can get depending on the base images:

$ docker image ls
test-alpine        latest   fae3b8af8e1b   3 seconds ago   10.1MB # alpine image
test-distroless    latest   98117ef14761   1 minute ago    6.93MB # distroless static-debian11
test-scratch-certs latest   b015a7cd8a59   14 minutes ago  4.78MB # scrtach with certs
test-scratch       latest   2aee7e47308f   2 hour ago      4.57MB # scratch

Categories

Container CI