Building dynamically-linked binaries in distroless images

SQLite3/Golang use-case example

- 3 mins read

Case

I’ve been developing an application in Go that uses SQLite as a local cache of the backend. The story would end there if it wasn’t that I decided to go for a distroless image for opmitization purposes, and also, why not?

The thing here is that for enabling certain features of the database/sql driver, the artifact should be dynamically linked through CGO_ENABLED=1 and with the -tags "fts5" (for the FTS5 feature support, see this issue comment).

Otherwise, if you don’t use any feature tag in the driver – in this case CGO_ENABLED=0 – the artifact will be static, and you can just use the static-<distro> image.

Since base images are available in the packages, you can now compose your dinamically linked binary in a distroless image. Base image contains glibc, and libssl.

Why Distroless?

Aside of the perks of minimalistic images, such as size, there are some benefits from the security perspective. Reducing the image dependencies, there is a reduction on inherited security vulnerabilities.

There is software that ain’t that straightfoward to implement as a distroless, but eventually, it is possible to do so.

Also, some implementations might need tooling or rely on it, which can be a caveat when implementing. Those tools can be implemented as sidecars, along a pod – if you use k8s, eg. –, but at the cost of a more complex deployment.

Layer Building

First, keep in mind that you need to build your component in the same distribution as the one you want to use as the distroless. If you take a peek on the available images for Golang, you’ll see that each version has a different upstream distribution.

The following example, compiles the artifact in golang:1.22-bookworm(Debian 12) and copies the generated artifact to the distroless image gcr.io/distroless/base-debian12.

## Build layer
FROM golang:1.22-bookworm as build
WORKDIR /go/scr/app_demo
COPY . .

RUN go mod download
RUN go vet -v ./...
RUN go test -v ./...

RUN CGO_ENABLED=1 go build -tags "fts5"  -o /app/app_demo cmd/backend/main.go
RUN chmod a+x /app/app_demo

## Final image 
FROM gcr.io/distroless/base-debian12
COPY --from=build /app /app
WORKDIR /app
CMD ["/app/app_demo"]

Sizes

The final image in my case with the base is about 65.4MBs. A smaller image with glibc, cgr.dev/chainguard/glibc-dynamic ended up with a ~32MBs, which is suitable if you don’t expect to use libssl.

Executing image

Building the binary might take some time, specially if running the code generation, install templ , or do the full vet over the code. In order to decouple these, we’ll add the configuration files and directories for custom assets via volumes. This will allow us to modify the configuration locally in the host, instead of copying files to the image.

You won’t be able to write a file directly into the image, that’s the reason on why we map a volume to a local folder that will contain the database file for SQLite.

networks: 
  app_demo:

services:
  backend:
    build:
      context: .
      dockerfile: ./docker/backend/Dockerfile
    image: app_demobackend
    container_name: "app_demo_backend"
    ports:
      - 8082:8082
    volumes:
      - type: bind
        source: ./docker/backend/app_demo.yaml
        target: /app/app_demo.yaml
      - ./docker/backend/cache:/app/cache
    env_file:
      - ./docker/backend/.env
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      - app_demo
    command: >
      /app/app_demo -index=true -config=/app/app_demo.yaml      

... other services ...

The volume /app/cache is an empty folder in the host that will be mounted for keeping the database file locally.


comments powered by Disqus