Create small Docker Image for Golang

Containerize Go application, build an optimal Docker Image using Dockerfile
Share this page:

If you’ve just landed here, we’re doing a “Become a Cloud Architect” Unicorn Workshop by building a Unicorn Pursuit Web App step by step, and you’re more then welcome to join!

About Docker and Dockerfile

A container is a standard unit of software that packages up code and all its dependencies so the application runs quickly and reliably from one computing environment to another. A Docker container image is a lightweight, standalone, executable package of software that includes everything needed to run an application:

  • Code
  • Runtime
  • System tools
  • System libraries
  • Settings

Docker

To run Unicorn Pursuit Golang web, we have two options:

  • Virtual Machine (EC2), which would mean we’d have to manage and operate a VM, upload our app (1 GB) and pay for it. Alongside, auto-scaling is somewhat complex, and slow.
  • Containers (ECS), which would mean that we only need to “push” the image (around 30 MB) of our app to ECS, and then everything else (auto-scaling, deployment of new release) would come as a managed service within ECS.

It seems like a no-brainer, right? In any organization, our priority is to improve reliability and reduce cost, and the second option requires less effort, less operational overhead, and above all - offers better performance.

There are two Videos we’d recommend you to see, depending on how important Docker and Containers are for you.

First one is a brief 7 minute introduction, to just understand the basics

Second video is a 2 hour deep dive, and it’s only recommended if you’re only getting started with Docker, and you need it for your project.

Code

Check out the code on GitHub repo. This is our application, and basically what we need to containerize. This is done using a Dockerfile, which represents a set of instructions on how to create a Docker container from your code.

First step is to understand the basic Dockerfile. Dockerfile defines your container, and you need to include instructions on how to build it, including all dependencies. Have in mind that your Container needs to run the same on any environment.

Basic Dockerfile for Golang

Let’s get to it. This is a first version of Dockerfile for Go. It basically copies all your local folder content to your container, installs all dependencies, and exposes a port 8080.

FROM golang:latest
# Set the Current Working Directory inside the container
WORKDIR $GOPATH/src/unicorn
# Copy everything from the current directory to the PWD (Present Working Directory) inside the container
COPY . .
# Download all the dependencies
RUN go get -d -v ./...
# Install the package
RUN go install -v ./...
# This container exposes port 8080 to the outside world
EXPOSE 8080
# Run the executable
CMD ["unicorn"]

This is Ok. But there’s a small issue: when we build our image, this creates a 1.5 GB Unicorn Pursuit image. Containers should be lightweight, so that they can scale out and in easily, so - this won’t do.

Improvement 1: ALPINE and .Dockerignore

Let’s introduce two improvements:

  • Instead of using Golang latest, let’s switch to alpine. Alpine is a minimal Docker image based on Alpine Linux with a complete package index and only 5 MB in size! The problem is that it doesn’t include basic stuff, such as Git, so we need to take care of that in our Dockerfile adding RUN apk update && apk add --no-cache git.
  • When copying our folder, there are sub-folders and files we don’t need, and ignoring them might reduce our docker image size. This is done using the .Dockerignore file, and storing it in your app folder (the one from where you’re doing a docker build).

Our new Dockerfile will now look like this:

FROM golang:alpine
RUN apk update && apk add --no-cache git
WORKDIR $GOPATH/src/unicorn
COPY . .
RUN go get -d -v ./...
RUN go install -v ./...
EXPOSE 8080

CMD ["unicorn"]

And our .Dockerignore will be:

.vscode
.idea
.git
bin
iac

When we now execute docker build -t unicorn ., our image is reduced, to 1 GB. Better… but not perfect.

Improvement 2: Multi Stage Build

Multi Stage build let’s us define stages, and only copy the stuff we really need. This means, that we can define Stage 1: Builder, and build our binary… and instead of copying everything - we can just copy the binary, and the template folder where our HTML files are. In the end - that’s all we need!

Around the internet you’ll find that most people use SCRATCH, not alpine. In our case, tests with Scratch required installation of additional packages into a final Stage, and the file was much bigger, so - the alpine was the way to go.

Our final Dockerfile would then look something like this:

# Stage 1 is our Builder Stage, where we create a unicorn binary
FROM golang:alpine AS builder
RUN apk update && apk add --no-cache git
WORKDIR /go/src/unicorn
COPY . .
RUN go get -d -v

# Let's build our unicorn binary now
RUN GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/unicorn

# Stage 2 uses the binary, and copies it to a new container
FROM alpine:3.10
COPY --from=builder /go/bin/unicorn /go/bin/unicorn
WORKDIR /go/src/unicorn

# We also need to copy our HTML files to our final container
COPY templates ./templates
EXPOSE 8080
ENTRYPOINT ["/go/bin/unicorn"]

What’s the size of our docker image now?

docker images | grep latest
unicorn latest dddfb7b1fa18 ... 29.8MB

Wow, reduced from 1.5 GB to only 30 MB.

Where to find more info




Last modified June 7, 2020: Golang4DockerAdded (ad39767)