Hello World. We are WEB cult. We make a note for web developers.

Multi-stage Dockerfile for Go App

Written or Updated on August 20, 2022 🖋️

ARG PORT=8888

#
# Base image for development
#
FROM golang:1.18-alpine as base

ARG PORT
ENV PORT=$PORT
ENV GO_ENV=development

WORKDIR /go/app/base

COPY go.mod .
COPY go.sum .

RUN apk add build-base
RUN go mod download
RUN go install github.com/cosmtrek/air@latest

COPY . .

#
# Image to build binary file
#
FROM golang:1.18-alpine as builder

ARG PORT
ENV PORT=$PORT

WORKDIR /go/app/builder

COPY --from=base /go/app/base /go/app/builder

RUN CGO_ENABLED=0 go build -o main -ldflags "-s -w"

#
# Image to RUN go binary
#
FROM gcr.io/distroless/static-debian11 as production

ARG PORT
ENV PORT=$PORT

WORKDIR /go/app/src

COPY --from=builder /go/app/builder/main /go/app/src/main

EXPOSE $PORT

CMD ["/go/app/src/main"]

Context

There are many things to consider when creating a Dockerfile, and because it requires low-level knowledge that many developers are not usually aware of, there is a possibility that you may be writing in an anti-pattern or creating a poorly performing Container Image without even realizing it. It is not limited to Dockerfiles, but it is also confusing that different people have different opinions about what is right.

I don’t say this Dockerfile is perfect, but it follows what is generally considered best practice.

We use cosmtrek/air to enable Hot reload for development, as it is too frustrating to develop without it. realize used to be popular and standard, but it doesn’t seem to be well maintained now.

Usage

version: "3.4"
services:
  go-server:
    container_name: "go-server"
    build:
      context: .
      target: base
      args:
        PORT: 8888
    ports:
      - "8888:8888"
    volumes:
      - .:/go/app/base
    command: "air"
    depends_on:
     - db

  db:
    container_name: "db"
    image: postgres:14
    restart: always
    tty: true
    ports:
      - "5432:5432"
    volumes:
      - ./postgres:/data/postgres
    environment:
      - POSTGRES_DB=postgres
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres

Explanation

FROM golang:1.18-alpine as base

FROM golang:1.18-alpine as builder

FROM gcr.io/distroless/static-debian11 as production

The first thing that might be unusual for some people is the use of FROM multiple times and different Container Images. This is called a Multi-stage build and the idea is to separate the environment in which the app will run from the rest of the environment. The binary file finally compiled only needs a minimum number of environments to run, so the aim is to reduce the size by using as small Container Image as possible. If the Container Image does not contain anything unnecessary, there will inevitably be fewer vulnerabilities, which is also beneficial from a security aspect.

COPY --from=base /go/app/base /go/app/builder
build:
  context: .
  target: base

For each Stage, you need to give the Container Image an appropriate name using as. By specifying this name, you can copy files from previous Stage or specify which Stage to use in docker-compose. In a multi-stage build, ARG and ENV are reset for each stage, so you need to redeclare them each time.

Base Image

#
# Base image for development
#
FROM golang:1.18-alpine as base

ARG PORT
ENV PORT=$PORT
ENV GO_ENV=development

WORKDIR /go/app/base

COPY go.mod .
COPY go.sum .

RUN apk add build-base
RUN go mod download
RUN go install github.com/cosmtrek/air@latest

COPY . .

Base Image is used only during development. During development, we often want it to behave differently from the production environment, so we declare an environment variable called GO_ENV.

What we do is first copy the files go.mod and go.sum, which describe the dependencies, and then download the dependent packages with go mod download. Finally, we install Hot Reloader Air.

Builder Image

#
# Image to build binary file
#
FROM golang:1.18-alpine as builder

ARG PORT
ENV PORT=$PORT

WORKDIR /go/app/builder

COPY --from=base /go/app/base /go/app/builder

RUN CGO_ENABLED=0 go build -o main -ldflags "-s -w"

Builder Image is a container image to run the go build command to compile the source code into a binary file. The files are copied from the previous Stage, base, by writing —from=base.

When building, there are a few tricks to make the binary file smaller. The first is to specify CGO_ENABLE=0. This will make it compile without linking with C libraries. Another is to specify the flag -ldflags “-s -w ” to build without including debugging information.

Production Image

#
# Image to RUN go binary
#
FROM gcr.io/distroless/static-debian11 as production

ARG PORT
ENV PORT=$PORT

WORKDIR /go/app/src

COPY --from=builder /go/app/builder/main /go/app/src/main

EXPOSE $PORT

CMD ["/go/app/src/main"]

The last Production Image is a Container Image just for running the application, but what is important here is which Container Image to use. The following lightweight images are commonly used.

  • ・alpine
  • ・scratch
  • ・busybox
  • ・scratch

Here we use gcr.io/distroless/static-debian11.
distroless is a name of Container Images provided by Google, and static-debian11 is suitable for running Go app. distroless does not even include apt or shell and consists of the bare minimum of files to provide a secure and lightweight runtime environment.