Written 19th of October 2020, updated 22nd of October 2020
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:
- Your deployment system needs it (e.g. Kubernetes or something like AWS ECS or Google Cloud Run)
- You have static assets that you want to include with your app, but don't want to use something like go-bindata
- You want to use something like Docker Compose to bring up your app in development
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.
The Go Dockerfile
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.15-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"]
Some things to understand first
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.
The builder image line by line
Let's start with the image that builds your application.
FROM golang:1.15-buster AS builder tells Docker to pull
offical Go image as a
base image for our build process. In this case, we use the tag
1.15-buster to ensure that we build with Go 1.15 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
/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
The runner image line by line
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 your image
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
Bonus: Using the image with Docker Compose
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
docker-compose -p app up -d to run the app
You now know how to create a Dockerfile to properly containerize your application in a lean, production-ready image. Thanks for reading!