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.
go.sum in 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
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
@MarkusBlaschke on Twitter, the
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
RUN set -x && apt-get update && DEBIAN_FRONTEND=noninteractive
apt-get install -y ca-certificates && rm -rf
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
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 public ./public/.
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 parameters.
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
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,
docker-compose -p app up -d to run the app and
You now know how to create a Dockerfile to properly containerize your application in a lean, production-ready image. Thanks for reading!