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 /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 /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 /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 /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 /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.