This guide goes over some different ways to run a Go application inside of a Docker container. I’ll walk you through the process of writing a Dockerfile, building an image, and spinning up that image as a container.
By directly addressing the tradeoffs associated with building containers from each image, I’m hoping to help you Dockerize your Go program as quickly as possible, as well as present fundamental solutions that are popular in Docker and Go communities.
I recommend using the same application I am so it’s easier to follow along. It counts down the time until the 2016 Presidential Election! You can clone the app from GitHub:
$ git clone https://github.com/nmccrory/go-prez.git
Using an Official Image
Docker has an official base image for Go that is simply named golang. This image takes the hassle out of ‘Dockerizing’ your Go apps by automatically running commands commonly used to build and run Go in a container.
The golang image has several different variants which each have their own advantages and disadvantages. Here I’m only going to talk about two image variants: golang:onbuild and golang:<version>.
The :onbuild image
With golang:onbuild as our base image, we can get our hello-go app running inside of a container using a one line Dockerfile. Awesome!
Create a file named Dockerfile and save it to your base application directory.
The onbuild commands built into golang:onbuild build and start the application for us. Then it’s as simple as using Docker to build the image:
$ docker build –t yourname/go-prez .
And run the image as a container:
$ docker run yourname/go-prez
- Developers can get up-and-running in a very short amount of time.
- Little Docker knowledge needed to start ‘Dockerizing’.
- Good for creating derivative images.
- Lacks control over the image building process. You don’t control how the app is added to the container, when the Go binary is built, or the execution of onbuild.
- Images start at over 500MB! As you make more images drive space can dwindle quickly.
The golang:<version> image
The golang:<version> naming convention represents a golang base image that comes with a supported version of Go.
For this example, I’m going to use Go 1.7 by building from golang:1.7 as my base image. Building an image for the app, which we’ll call “hello-go” means our dockerfile will have to include some more lines but it’s worth it because we’ll gain build process control.
FROM golang:1.7 RUN mkdir -p /app WORKDIR /app ADD . /app RUN go build ./app.go CMD ["./app"]
To build the image and run it as a container I just use the classic docker build and run commands.
$ docker build –t yourname/hello-go . $ docker run –p 8000:8000 –d yourname/hello-go
- Control over the build order of the image.
- Don’t need to manually install Go.
- Images like golang:<version> have many layers to them. If you’re looking for a lean container image this may be a disadvantage.
- More lines of code involved in writing our dockerfile.
Using the Scratch Image
The golang base images are large and contain application layers we don’t need to use for our case. Using a golang base image was handy, but for this build let’s not worry about convenience.
The scratch image is an empty image. There is absolutely nothing linked to it and by the transitive property, its emptiness also means it has (almost) no size.
The trick to creating Go containers that aren’t gigantic in size is to use scratch as your base image. Then instead of copying your entire application and building your Go binary in the image, you COPY the Go binary by itself to the image.
Before touching our Dockerfile let’s compile our Go app outside the container.
$ GOOS=linux go build app.go
If we don’t declare our Go OS as Linux it won’t execute in the container – since the container is derived from Linux. Like any Go program, after compiling the resulting binary gets put in the project’s root directory.
Building from scratch our Dockerfile is going to look like this:
FROM scratch COPY /app /app/ CMD [“/app”]
It’s important to notice the binary we created earlier is only being copied to a container directory. Nothing is being built inside of the container and no special tools are required to execute the binary.
- Small image file sizes. (“go-prez” comes out to only ~1.7MB)
- Image does not have several layers.
- Utilizes Go’s statically linked binaries.
- For now, this container is only capable of executing a statically linked binary.
- Extra legwork is involved for programs using the net package or CGo because these packages create dynamically linked binaries.
- Missing basic application layers in most instances. For example, if you want to install any languages or frameworks you would build off of a Linux base.
Statically linked binaries are relatively large however they’re incredibly portable because you can execute them anywhere. You may not always need to leverage them, though. Using a Docker image is a great way to distribute an application and deploy it quickly.
Using an official golang image is a painless way to Dockerize Go programs and will get you up and running in no time. However, they come with a large size tradeoff and, depending on the image variant, lack process control.
The scratch image is perfect for leveraging the portability of a Go static binary. Since the binary can be executed in any environment you can simply inject it into your image and run. Starting from scratch also trims the size of your image to be much more reasonable.
There are a growing number of resources that further elaborate on image optimization and Go binaries. These resources do a great job elaborating on Go applications using Docker and I find them incredibly helpful when I have questions.
- “Docker + Golang = <3,” by Jerome Petazzoni (Docker Blog).
- “Optimizing Docker Images for Static Binaries,” by Kelsey Hightower (Medium).
- Docker Library Golang Documentation.
Docker is a sponsor of The New Stack
Feature image via Pixabay.