Image by Pramoon Design on Shutterstock

Compiling Your Go Application for Containers

An excerpt from Powerful Command-Line Applications in Go by Ricardo Gerardi

The Pragmatic Programmers
The Pragmatic Programmers
8 min readJun 15, 2023

--

📘 If you enjoy this excerpt, please consider purchasing the book or joining the conversation on DevTalk (or both).

Another alternative way to distribute your application that has become increasingly popular in recent years is allowing your users to run the application in Linux containers. Containers package your application and all the required dependencies using a standard image format, and they run the application in isolation from other processes running on the same system. Containers use Linux kernel resources such as Namespaces and Cgroups to provide isolation and resource management.

There are different container runtimes available, such as Podman and Docker. If you’re running these examples on a Linux system, you can use either one interchangeably. If you’re running on Windows or macOS, Docker provides a desktop version that makes it easier to start. You can also use Podman on these operating systems, but you need to install a Virtual Machine to enable it. We’ll not cover a container runtime installation process here. For more details, check the respective project’s documentation.

To distribute your application as a container, you have to create a container image. You can do this in several ways, but a common way is by using a Dockerfile, which contains a recipe for how to create an image. Then you pass this file as input to docker or podman commands to build the image. For more details on how to create the Dockerfile, consult its documentation.

The focus of this section is to provide some build options to optimize your application to run in containers. Go is a great choice for creating applications that run in containers because it generates a single binary file that you can add to the container image without additional runtimes or dependencies.

To make the binary file even more suitable to run in a container, you can pass additional build options. For example, you’ll enable a statically linked binary by setting CGO_ENABLED=0, and you can pass additional linker options using the flag -ldflags. To reduce the binary size, use the options -ldflags=”-s -w” to strip the binary of debug symbols. Before you get started, take a closer look at some of the build options that you’ll use:

  • CGO_ENABLED=0: Enables statically linked binaries to make the application more portable. It allows you to use the binary with source images that don’t support shared libraries when building your container image.
  • GOOS=linux: Since containers run Linux, set this option to enable repeatable builds even when building the application on a different platform.
  • -ldflags=”-s -w”: The parameter -ldflags allows you to specify additional linker options that go build uses at the link stage of the build process. In this case, the option -s -w strips the binary of debugging symbols, decreasing its size. Without these symbols, it’s harder to debug the application, but this is usually not a major concern when running in a container. To see all linker options you can use, run go tool link.
  • -tags=containers: This is specific to your Pomodoro application. Build the application using the files specified with the container tag to remove dependency on SQLite and notifications as you did in Conditionally Building Your Application.

Now build your binary using these options:

​ ​$ ​​CGO_ENABLED=0​​ ​​GOOS=linux​​ ​​go​​ ​​build​​ ​​-ldflags=​​"-s -w"​​ ​​-tags=containers​

Inspect this file to verify its properties and size:

​ ​$ ​​ls​​ ​​-lh​​ ​​pomo​
​ -rwxr-xr-x 1 ricardo users 7.2M Feb 28 12:06 pomo
​ ​$ ​​file​​ ​​pomo​
​ pomo: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked,
​ ​ ...​​ ​​...​​ ​​,​​ ​​stripped​

Notice the file size is about 7MB and the file is statically linked and stripped.

Compare this with building the application without these options:

​ ​$ ​​go​​ ​​build​
​ ​$ ​​ls​​ ​​-lh​​ ​​pomo​
​ -rwxr-xr-x 1 ricardo users 13M Feb 28 12:09 pomo
​ ​$ ​​file​​ ​​pomo​
​ pomo: ELF 64-bit LSB executable, x86-64, version 1 (SYSV),
​ dynamically linked,
​ interpreter /lib64/ld-linux-x86-64.so.2, ..., for GNU/Linux 4.4.0,
​ not stripped

The binary file optimized for containers is almost 50% smaller than the original. It’s also statically linked and stripped of debugging symbols.

Once you have the binary, you’ll create a container image by using a Dockerfile. Switch back to the chapter’s root directory and create a new subdirectory containers:

​ ​$ ​​cd​​ ​​$HOME/pragprog.com/rggo/distributing​
​ ​$ ​​mkdir​​ ​​containers​

Create and edit a file called Dockerfile in this subdirectory. Add the following contents to create a new image from the base image alpine:latest, create a regular user pomo to run the application, and copy the binary file pomo/pomo you built before to the image under directory /app:

distributing/containers/Dockerfile

​ FROM alpine:latest
​ RUN mkdir /app && adduser -h /app -D pomo
​ WORKDIR /app
​ COPY --chown=pomo /pomo/pomo .
​ CMD ["/app/pomo"]

Build your image using the docker build command providing this Dockerfile as input:

​ ​$ ​​docker​​ ​​build​​ ​​-t​​ ​​pomo/pomo:latest​​ ​​-f​​ ​​containers/Dockerfile​​ ​​.​
​ STEP 1: FROM alpine:latest
​ STEP 2: RUN mkdir /app && adduser -h /app -D pomo
​ ​-->​​ ​​500286ad2c9​
​ STEP 3: WORKDIR /app
​ ​-->​​ ​​175d6b43663​
​ STEP 4: COPY --chown=pomo /pomo/pomo .
​ ​-->​​ ​​2b05fa6dbba​
​ STEP 5: CMD ["/app/pomo"]
​ STEP 6: COMMIT pomo/pomo:latest
​ ​-->​​ ​​998e1c2cc75​
​ 998e1c2cc75dc865f57890cb6294c2f25725da97ce8535909216ea27a4a56a38

This command creates an image tagged with pomo/pomo:latest. List it using docker images:

​ ​$ ​​docker​​ ​​images​
​ REPOSITORY TAG IMAGE ID CREATED SIZE
​ localhost/pomo/pomo latest 998e1c2cc75d 47 minutes ago 13.4 MB
​ docker.io/library/alpine latest e50c909a8df2 4 weeks ago 5.88 MB

Run your application using Docker, providing the -it flags to enable a terminal emulator, which is required to run Pomodoro’s interactive CLI:

​ ​$ ​​docker​​ ​​run​​ ​​--rm​​ ​​-it​​ ​​localhost/pomo/pomo​

You can also use Docker to build the application with Go’s official image and a multistage Dockerfile. A multistage Dockerfile instantiates a container to compile the application and then copies the resulting file to a second image, similar to the previous Dockerfile you created. Create a new file called Dockerfile.builder in the containers subdirectory. Define the multistage build using the following code:

distributing/containers/Dockerfile.builder

​ ​FROM​​ golang:1.15 AS builder​
​ ​RUN ​mkdir /distributing
​ ​WORKDIR​​ /distributing​
​ ​COPY​​ notify/ notify/​
​ ​COPY​​ pomo/ pomo/​
​ ​WORKDIR​​ /distributing/pomo​
​ ​RUN ​CGO_ENABLED=0 GOOS=linux go build -ldflags=​"-s -w"​ -tags=containers

​ ​FROM​​ alpine:latest​
​ ​RUN ​mkdir /app && adduser -h /app -D pomo
​ ​WORKDIR​​ /app​
​ ​COPY​​ --chown=pomo --from=builder /distributing/pomo/pomo .​
​ ​CMD​​ ["/app/pomo"]​

Now use this image to build the binary and the container image for your application:

​ ​$ ​​docker​​ ​​build​​ ​​-t​​ ​​pomo/pomo:latest​​ ​​-f​​ ​​containers/Dockerfile.builder​​ ​​.​
​ STEP 1: FROM golang:1.15 AS builder
​ STEP 2: RUN mkdir /distributing
​ ​-->​​ ​​e8e2ea98b04​
​ STEP 3: WORKDIR /distributing
​ ​-->​​ ​​81cee711389​
​ STEP 4: COPY notify/ notify/
​ ​-->​​ ​​ac86b302a7a​
​ STEP 5: COPY pomo/ pomo/
​ ​-->​​ ​​5353bc4d73e​
​ STEP 6: WORKDIR /distributing/pomo
​ ​-->​​ ​​bfddd5217bf​
​ STEP 7: RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -tags=containers
​ go: downloading github.com/spf13/viper v1.7.1
​ go: downloading github.com/spf13/cobra v1.1.1
​ go: downloading github.com/mitchellh/go-homedir v1.1.0
​ go: downloading github.com/mum4k/termdash v0.13.0
​ go: downloading github.com/spf13/afero v1.1.2
​ go: downloading github.com/spf13/cast v1.3.0
​ go: downloading github.com/pelletier/go-toml v1.2.0
​ go: downloading gopkg.in/yaml.v2 v2.2.8
​ go: downloading github.com/mitchellh/mapstructure v1.1.2
​ go: downloading github.com/spf13/pflag v1.0.5
​ go: downloading golang.org/x/text v0.3.4
​ go: downloading github.com/subosito/gotenv v1.2.0
​ go: downloading github.com/magiconair/properties v1.8.1
​ go: downloading github.com/fsnotify/fsnotify v1.4.7
​ go: downloading github.com/mattn/go-runewidth v0.0.9
​ go: downloading github.com/spf13/jwalterweatherman v1.0.0
​ go: downloading github.com/hashicorp/hcl v1.0.0
​ go: downloading github.com/gdamore/tcell/v2 v2.0.0
​ go: downloading gopkg.in/ini.v1 v1.51.0
​ go: downloading golang.org/x/sys v0.0.0-20201113233024-12cec1faf1ba
​ go: downloading github.com/gdamore/encoding v1.0.0
​ go: downloading github.com/lucasb-eyer/go-colorful v1.0.3
​ ​-->​​ ​​de7b70a3753​
​ STEP 8: FROM alpine:latest
​ STEP 9: RUN mkdir /app && adduser -h /app -D pomo
​ ​-->​​ ​​Using​​ ​​cache​​ ​​500286ad2c9f1242184343eedb016d53e36e1401675eb6769fb9c64146...​
​ ​-->​​ ​​500286ad2c9​
​ STEP 10: WORKDIR /app
​ ​-->​​ ​​Using​​ ​​cache​​ ​​175d6b43663f6db66fd8e61d80a82e5976b27078b79d59feebcc517d44...​
​ ​-->​​ ​​175d6b43663​
​ STEP 11: COPY --chown=pomo --from=builder /distributing/pomo/pomo .
​ ​-->​​ ​​0292f63c58f​
​ STEP 12: CMD ["/app/pomo"]
​ STEP 13: COMMIT pomo/pomo:latest
​ ​-->​​ ​​3c3ec9fafb8​
​ 3c3ec9fafb8f463aa2776f1e45c216dc60f7490df1875c133bb962ffcceab050

The result is the same image as before, but with this new Dockerfile, you don’t have to compile the application manually before creating the image. The multistage build does everything for you in a repeatable and consistent way.

Go builds applications into single binaries; you can build them statically linked and can also create images that have no other files or dependencies. These tiny images are optimized for data transfer and are more secure since they contain only your application binary.

To create such an image, you’ll use a multistage Dockerfile. So copy the file containers/Dockerfile.builder into a new file containers/Dockerfile.scratch, and edit this new file, replacing the second stage image on the FROM command with scratch. This image has no directories or users, so replace the remaining commands with a command to copy the binary to the root directory. When you’re done, your Dockerfile will look like this:

distributing/containers/Dockerfile.scratch

​ ​FROM​​ golang:1.15 AS builder​
​ ​RUN ​mkdir /distributing
​ ​WORKDIR​​ /distributing​
​ ​COPY​​ notify/ notify/​
​ ​COPY​​ pomo/ pomo/​
​ ​WORKDIR​​ /distributing/pomo​
​ ​RUN ​CGO_ENABLED=0 GOOS=linux go build -ldflags=​"-s -w"​ -tags=containers

​ ​FROM​​ scratch​
​ ​WORKDIR​​ /​
​ ​COPY​​ --from=builder /distributing/pomo/pomo .​
​ ​CMD​​ ["/pomo"]​

Build your image using this Dockerfile as you did before:

​ ​$ ​​docker​​ ​​build​​ ​​-t​​ ​​pomo/pomo:latest​​ ​​-f​​ ​​containers/Dockerfile.scratch​​ ​​.​
​ STEP 1: FROM golang:1.15 AS builder
​ STEP 2: RUN mkdir /distributing
​ ​-->​​ ​​9021735fd16​
​ ​...​​ ​​TRUNCATED​​ ​​OUTPUT​​ ​​...​
​ STEP 8: FROM scratch
​ STEP 9: WORKDIR /
​ ​-->​​ ​​00b6e665a3f​
​ STEP 10: COPY --from=builder /distributing/pomo/pomo .
​ ​-->​​ ​​c6bbaccb87b​
​ STEP 11: CMD ["/pomo"]
​ STEP 12: COMMIT pomo/pomo:latest
​ ​-->​​ ​​4068859c281​

Check your new image and notice that its size is close to the binary size because it’s the only file in the image:

​ ​$ ​​docker​​ ​​images​
​ REPOSITORY TAG IMAGE ID CREATED SIZE
​ localhost/pomo/pomo latest 4068859c281e 5 seconds ago 8.34 MB

Not all applications are good candidates to run in a container, but for the ones that are, this is another option to distribute your application for your users.

Next, let’s explore go get to distribute your application with source code.

We hope you enjoyed this excerpt from Powerful Command-Line Applications in Go by Ricardo Gerardi. You can continue reading on Medium or purchase the ebook directly from the Pragmatic Programmers.

https://pragprog.com/newsletter/
https://pragprog.com/newsletter/
Book cover featuring illustrated shadow hands with tools on a light blue diagonal stripe background

--

--

The Pragmatic Programmers
The Pragmatic Programmers

We create timely, practical books and learning resources on classic and cutting-edge topics to help you practice your craft and accelerate your career.