Shipping a Golang service as a Docker container
At this day and age Docker has established itself as the de facto tool for containerisation, it is deserved given how well it abstract its complexity away from its users. In the cloud it allows us to run containerised services as if the programming language those services have been written on don’t really matter, and in fact it doesn’t, provided that the service are exposed on a port.
On this blog post I will my preferred set up to Dockerize Go services.
Binary, Binary, Binary
Binary, Binary, Binary - Steve Balmer
The command go build
generates a binary, and this is a relevant and non-trivial fact that is going to impact how I am going to set up the Dockerfile, but I am going to come back at this later, for now here is the code snippet:
1##
2## Builder stage
3##
4FROM golang:1.20 AS builder
5
6ARG VERSION
7
8WORKDIR /usr/app
9COPY . ./
10
11ENV CGO_ENABLED=0
12RUN go build -buildvcs=false -o bin/service -ldflags="-X main.Version=${VERSION}" ./
13
14##
15## Final stage
16##
17
18FROM alpine
19WORKDIR /usr/app
20COPY --from=builder /usr/app/bin/service ./service
21
22ENTRYPOINT ["./service"]
The Dockerfile above has two stages where the first stage is solely focused on building a binary from the source code:
- It relies on the base image
golang:1.20
and it is aliased asbuilder
because it’s referenced further down - Defines docker argument
VERSION
, which is used to specify the image’s and the binary’s version during build time - It sets a workdir, and copy everything from the project root to the root of the working directory set as
/usr/app
- CGO is disabled because the source code doesn’t rely on any library written in
C
. Read more here - Builds the binary:
- Omitting the version control metadata with
-buildvcs
set to false - Defines the output directory and binary filename to
bin/service
- Sets the docker argument
VERSION
to the variableVersion
defined inmain.go
- Omitting the version control metadata with
After the binary gets built on the state named builder
, Docker jumps into the second stage and does the following:
- It uses a very small Docker image based on Linux named alpine, which is as big as ~5 MB in size, and that is possible because the newly built binary doesn’t rely on an external runtime such as Node.js or JVM
- The working directory is set to
/usr/app
- The binary generated in the previous stage gets copied thanks to
COPY --from=builder
- Finally, the entrypoint is a call to the binary
./service
, just like you would do if you were running it locally
How to build it, and run it
To build a new Docker image versioned 0.0.1, run the following command:
docker build --build-arg VERSION=0.0.1 -t go-docker-demo:0.0.1 .
And to run the image newly built run the following command:
docker run -p 8080:8080 go-docker-demo:0.0.1
Conclusion
That’s the end of this post, and perhaps to I would like to conclude that the fact that Go generates a binary that can be executed without an external runtime helps to build Docker images based in lightweight images such as alpine
, which makes the build faster, and very cheap to host it on an Images Registry provided by a Cloud vendor, which tend to charge by data transferred in and out.
Cheers, Firmino