Dockerizing a Go App — and Optimising Its Image Size | by Stephen Wayne | Apr, 2022

Docker is an open-source software that generates and runs transportable, self-sufficient containers on your backend service or app. The containers may be run on something that helps Docker — cloud providers, on-prem, and even your native growth machine.

The developer (you) will get to outline the setting and dependencies accessible for this system in a repeatable, infrastructure-as-code trend.

The construct course of is outlined utilizing a Dockerfile, and setup directions may be discovered here.

Docker has a great explainer, nevertheless it boils all the way down to developer effectivity. By defining the precise setting that the code runs in everybody may be on the identical web page, whether or not they’re working regionally in dev-mode, debugging a manufacturing outage, or exploring multi-cloud deployment. No extra spending days establishing a growth setting, and the “works effective on my machine” rigidity disappears. Docker isn’t the one software that provides these advantages, nevertheless it’s definitely in style and has a big neighborhood round it.

We’ll give attention to containerizing a Chuck Norris joke server that I’ve previously written about.

We’ll begin by constructing from Alpine Linux, which is a light-weight distro that has a package deal supervisor and different instruments we’d need when constructing code. On this case, we’ll use one that’s particular to Golang 1.17.

Subsequent, we’ll use our package deal supervisor, apk, to put in no matter dependencies we have to construct our app. On this case, we solely want ca-certificates to offer a safe connection between shopper and server.

Subsequent, we’ll arrange a working listing to construct our app in. From this level on within the Dockerfile, we’ll be working on this listing. Right here we copy all the pieces from our native listing into the working listing within the Docker picture, but when there are information that aren’t required to construct the picture you may be extra selective.

Copy is of the shape Copy <supply location> <vacation spot location>, so we’re saying “copy the issues on this listing on the native machine to the working listing within the picture that we’re constructing.”

Subsequent, we’ll construct our code. For Go, we compile and construct a binary from the supply materials.

This can look very acquainted when you’ve constructed many Go apps, but when not it’s basically saying “Use this code to construct a binary referred to as joke-web-server, and place that binary ‘right here’. This binary may be statically linked (the binary gained’t depend on libraries from the working system), and we intend to run it on linux.”

We’ll end our Dockerfile by documenting the port that the app listens on (contained in the container), and we’ll outline the way to run the app when the ensuing picture is run.

EXPOSE doesn’t really do a lot, it’s basically documentation that the app is listening on port 5000 inside Docker. The app code listens on port 5000, so we’re documenting that right here. We are going to arrange the port on the host machine after we run the picture.

ENTRYPOINT is the command to run when the picture is ran. So on this case, it’s working the binary, positioned in WORKDIR, referred to as joke-web-server.

We are able to construct our picture by working:

docker construct -t joke-web-server .

The command is of the shape docker construct [OPTIONS] PATH, so we’re saying “Use the Dockerfile (and different issues) positioned within the present listing to construct a picture referred to as ‘joke-web-server’”. After the method completes, we will see that our picture was constructed by itemizing Docker photos with docker picture ls :

To run the picture, we have to hyperlink the Docker port (loosely outlined by our earlier EXPOSE directive) to the port on our host machine. This can be a command line possibility of the shape p <native machine port>:<docker port>. So if we run our picture as:

docker run -p 5001:5000 joke-web-server

we’re telling Docker to hyperlink inner port 5000 (the place our app is listening) to native port 5001 (the port you’d use to entry the app outdoors of Docker).

The previous, dependable ctrl-c can be utilized to cease the server once you’re completed with it.

And there we have now it! This basic components may be utilized to run quite a lot of applications, built-in Go or in any other case.

The above resulted in a 370MB picture — not nice for such a small app. If I do a go construct on my growth machine (M1 Max MacBook Professional), I find yourself with a 6.7MB picture. What’s happening?

Every instruction in our Dockerfile provides a layer to the picture, which may be considered a diff to the earlier state. As we construct we’re carrying a number of artifacts required to construct the app ahead with us into the ultimate container. The OS we’ve chosen for the picture can be there and certain has issues we don’t want, and any packages we’ve put in are included too. Even the information used to construct our binary are nonetheless hanging round! All of this will increase the dimensions of the ultimate consequence, to not point out including to the potential assault floor if/after we deploy it out in the actual world.

We might give attention to cleansing up all of the pointless bits as we go alongside, however that provides important complexity (and alternatives for bugs or safety points down the road). Enter Docker multi-stage builds.

Multi-stage builds are mainly a pipeline the place we assemble a sequence of photos to construct varied output artifacts. These artifacts are then shuffled to the following “stage” of the construct.

However! We don’t must preserve the bits that have been required to create that artifact anymore. We preserve doing this till we arrive at a closing container that incorporates the minimal set of dependencies to deploy our app. Fairly often, this manifests as a two-stage construct: one to construct the app’s binary, and one to retailer the app (and any dependencies) for deployment.

For our instance above, that appears like the next:

Stage 1: Construct

Notice the road FROM Golang:1.17-alpine as build-stage — we’re calling this primary stage build-stage, and within the subsequent step we will pull artifacts from its filesystem.

Stage 2: Construct our Closing Picture

Right here, we’re copying the ca-certificates dependency and the app binary that we created in build-stage. We’re forsaking all of the bits that we used to construct the binary (and its dependencies).

As a result of our app binary is statically linked, we have been in a position to construct FROM scratch for a minimalistic ensuing Docker picture. You possibly can learn extra in regards to the scratch key phrase here.

We now have a 7.2MB deployable picture — a ~51x enchancment over the single-stage construct, and solely a ~7% enhance overbuilding the uncooked binary for my machine. This now looks like a fairly good tradeoff for a transportable, easily-deployable app.

You possibly can run this picture precisely as you ran the single-stage picture, however now it’s a lot smaller with little or no relative assault floor.

Docker is a big undertaking with many, many attainable configurations that can be particular to your app, however for me getting began was the toughest half. I hope that this will function a superb springboard, and would love any suggestions on the way to enhance!

More Posts