1. Order matters

Docker rebuilds images layer by layer, starting with first change. So

FROM golang:1.17

# modules cached in a separate layer
# it would redownload modules, only if dependencies were changed
WORKDIR /go/src/foo/
COPY go.mod go.sum ./
RUN go mod download

# copy verything else & build static binary
COPY . .
RUN CGO_ENABLED=0 go build -o /bin/foo -a ./

ENTRYPOINT ["/bin/foo"]

2. Split stages

One can run build, code-generation, chore tasks in a prep, build stages, while keeping result clean. Resulting image will be few megabytes in size.

FROM golang:1.17-alpine AS build

# Download fresh TLS certificates
RUN apk add --no-cache ca-certificates 
RUN update-ca-certificates

# modules cached in a separate layer
WORKDIR /go/src/foo/
COPY go.mod go.sum ./
RUN go mod download

# build static binary
COPY . .
RUN CGO_ENABLED=0 go build -o /bin/foo -a ./

# This results in a single layer image which consists from binary and CA TLS certificates
FROM scratch
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /bin/foo /bin/foo

ENTRYPOINT ["/bin/foo"]

3. Be aware of build platform and target platform.

docker buildx ... multi-arch build can be done either with crosscompiling or emulation. Go can build binaries for almost any platform, so we would stick with crosscompiling, since it’s way faster. Dockerfile

ARG GOVERSION=1.17
ARG BUILDPLATFORM="linux/amd64"

# specify that regardless of target, we use image native to build platform.
FROM --platform=$BUILDPLATFORM golang:${GOVERSION}-alpine AS build
RUN apk add --no-cache ca-certificates 
RUN update-ca-certificates

# modules cached in a separate layer
WORKDIR /go/src/foo/
COPY go.mod go.sum ./
RUN go mod download

# build static binary
COPY . .

# after this line, layers would be splited for each target platform.
ARG TARGETOS TARGETARCH

# specify default target ${FOO:-default value}, so regular docker builds will remain working. 
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64} \
    go build -o /bin/foo -a ./

# This results in a single layer image
FROM scratch

# We do not use following args, but It's important to accept same them here.
# Otherwise layer cache is broken, and all architectures will get same binary
ARG TARGETOS TARGETARCH

COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /bin/foo /bin/foo
ENTRYPOINT ["/bin/foo"]

4. Enable Github Workflow to build & push your containers

Specify secrets of your container registry (E.G: Dockerhub, ECR, or GHCR)

.github/workflows/push.yaml

name: build and push latest image

on:
  push:
    branches:
      - 'master'

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1
      - name: Login to DockerHub
        uses: docker/login-action@v1 
        with:
          username: ${{ secrets.DOCKERHUB_USER }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - name: Build and push
        uses: docker/build-push-action@v2
        with:
          push: true
          platforms: linux/amd64,linux/arm64
          tags: foo/bar:latest
          build-args: |
                        COMMIT=${{ github.sha }}

Here you can pass additional information, via build-args.