Written 19th of October 2020, updated 11th of May 2021.
The Go compiler produces a nice, single binary that's easy to deploy already. However, sometimes it's convenient to containerize your application, for example if:
In this blog post, I'll show you a Dockerfile you can use as a template, and give you the reasoning behind each line in it.
I'll start by giving you the file. Have a quick read through it, but if you don't understand everything, that's fine. We'll get to it.
FROM golang:1.16-buster AS builder WORKDIR /src COPY go.mod go.sum ./ RUN go mod download -x COPY . ./ RUN go build -v -o /bin/server cmd/server/*.go FROM debian:buster-slim RUN set -x && apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates && \ rm -rf /var/lib/apt/lists/* WORKDIR /app COPY production.toml ./ COPY --from=builder /bin/server ./ CMD ["./server", "-config", "production.toml"]
There are a few things to understand about this Dockerfile first.
The first has to do with how Docker builds images. This file uses something that's called a multi-stage build in Docker, which is just a fancy way of saying that we can build multiple Docker images defined in the same file. In this case, we use one image to build our Go application, and another for running it. We do it this way so we don't have to include our source code, the Go compiler etc. in our final image.
The second thing has to do with caching. To speed up the build process, Docker caches the result of each line of a Dockerfile. That means that we don't necessarily run all the commands in a Dockerfile on each build, but instead re-use the results of previous lines. Therefore, the order of lines is important, and you should generally do things that don't change very often earlier in the Dockerfile than things that do change more often. One common example is pulling dependencies before building your app from source.
Okay, so with that out of the way, let's go through the file.
Let's start with the image that builds your application.
FROM golang:1.16-buster AS builder tells Docker to pull the offical
Go image as a base image for our build process. In this case, we use the tag
1.16-buster to ensure
that we build with Go 1.16 in Debian Buster. We call the resulting image
WORKDIR /src sets the working directory to
/src, so that we don't have to write it on every
line. This is where files are now copied to.
COPY go.mod go.sum ./ copies the go module dependency files to the image.
particular is a lock-file that has all the (transitive) dependencies in it that your application has, so your
dependencies have changed only if this file has changed. This also means that the Docker cache is invalidated at this
line if these files have changed.
RUN go mod download -x downloads the app dependencies and prints the progress to standard out. Again,
because of the caching, this is only done if your dependencies change, which is nice because it can take a while.
COPY . ./ copies all of your code into the image. Note that, if you need to exclude anything from being
copied into your image, use a file called
RUN go build -v -o /bin/server cmd/server/*.go uses go to build your app (in this case located under
/cmd/server) into a binary located at
/bin/server. Note that the path is outside the
dir, so we avoid potential name collisions from your source directory (e.g. if you have a directory/package called
After the builder image comes the image that will be used to actually run your application.
FROM debian:buster-slim starts us out with a specially slimmed-down version of Debian Buster. You could
of course use other images here if you prefer. As pointed
out by user habarnam on Reddit and @MarkusBlaschke
on Twitter, the Distroless
base image could also be a good choice. Some also prefer to use
FROM scratch, if you don't need anything
from the OS at all.
RUN set -x && apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates && rm -rf
/var/lib/apt/lists/* is a long line that basically updates the OS dependencies, installs some certificates so
your app can connect to TLS-enabled HTTPS endpoints without erroring, and then deletes some intermediary files. This
is done in one line to (you guessed it) make caching easier for Docker. If your application has other OS package
dependencies, add them here after
WORKDIR /app sets the working directory to
/app, again so that we don't have to repeat it
all the time.
COPY production.toml ./ copies a configuration file to the image. You should copy other assets your
application needs here. For example, to copy a whole directory called
COPY --from=builder /bin/server ./ copies the compiled binary from the builder image into this image.
CMD ["./server", "-config", "production.toml"] sets the default command to run, along with some
Building the image is very easy. Once you have Docker running on your machine, just run
docker build -t app
. to build your image under the name
Now that you have an image for your application, you can use it with Docker Compose to bring up your application,
along with your dependencies like the database. All it takes is a configuration file. I won't go through this one in
detail, but have a look at the Docker Compose documentation to get an
overview. Put this in a file called
docker-compose.yaml to spin up Postgres 12 along with your app:
version: '3.8' services: db: image: postgres:12 environment: POSTGRES_USER: app POSTGRES_PASSWORD: 123 ports: - 5432:5432 volumes: - type: bind source: ./data target: /var/lib/postgresql/data app: build: context: ./ dockerfile: Dockerfile ports: - "8080:8080" depends_on: - db restart: always
docker-compose -p app build to (re-)build your image, and
docker-compose -p app up -d
to run the app and Postgres.
You now know how to create a Dockerfile to properly containerize your application in a lean, production-ready image. Thanks for reading!