Dockerize your Go app

Go is quickly becoming one of my favorite languages to work with. So, today we'll dockerize our Go app by taking advantage of builder pattern and multistage builds to reduce our docker image from 850mb to just 15mb!

This article is part of the Dockerize series, make sure to checkout the Introduction where I go over some concepts which we are going to use. Code from this article is available here

I've also made a video, if you'd like to follow along

Project setup

I've initialized a simple api using Mux

├── main.go
├── go.mod
└── go.sum

Here's our main.go

package main

import (
    "encoding/json"
    "log"
    "net/http"

    "github.com/gorilla/mux"
)

func main() {
    router := mux.NewRouter()
    router.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
        response := map[string]string{
            "message": "Hello Docker!",
        }
        json.NewEncoder(rw).Encode(response)
    })

    log.Println("Server is running!")
    http.ListenAndServe(":4000", router)
}

For development

We'll be using Reflex as part of our development workflow. If you're not familiar, Refelx provides live reload when developing.

Let's continue our docker setup by adding a Dockerfile

FROM golang:1.16.5 as development
# Add a work directory
WORKDIR /app
# Cache and install dependencies
COPY go.mod go.sum ./
RUN go mod download
# Copy app files
COPY . .
# Install Reflex for development
RUN go install github.com/cespare/reflex@latest
# Expose port
EXPOSE 4000
# Start app
CMD reflex -g '*.go' go run api.go --start-service

Let's create a docker-compose.yml. Here we'll also mount our code in a volume so that we can sync our changes with the container while developing.

version: "3.8"

services:
  app:
    container_name: app-dev
    image: app-dev
    build:
      context: .
      target: development
    volumes:
      - .:/app
    ports:
      - 4000:4000

Start! Start! Start!

docker-compose up

we can also use the -d flag to run in daemon mode

Great, our dev server is up!

app-dev  | Starting service...
app-dev  | 2021/07/04 12:50:06 Server is running!

Let's checkout our image using docker images command

REPOSITORY          TAG                   IMAGE ID       CREATED         SIZE
app-dev             latest                3063740d56d8   7 minutes ago   872MB

Over 850mb for a hello world! While this might be okay for development, but for production let's see how we can reduce our image size

For production

Let's update our Dockerfile by adding a builder and production stage

Update: Notice how we define CGO_ENABLED 0 with ENV in Dockerfile rather than doing directly before go build command. Also, we will be using alpine instead of scratch as it's really hard to debug containers in production with scratch

FROM golang:1.16.5 as builder
# Define build env
ENV GOOS linux
ENV CGO_ENABLED 0
# Add a work directory
WORKDIR /app
# Cache and install dependencies
COPY go.mod go.sum ./
RUN go mod download
# Copy app files
COPY . .
# Build app
RUN go build -o app

FROM alpine:3.14 as production
# Add certificates
RUN apk add --no-cache ca-certificates
# Copy built binary from builder
COPY --from=builder app .
# Expose port
EXPOSE 4000
# Exec built binary
CMD ./app

Let's add a build our production image

docker build -t app-prod . --target production

Let's check out our built production image

docker images

Using builder pattern we reduced out image size to just ~15mb!!

REPOSITORY                    TAG                   IMAGE ID       CREATED          SIZE
app-prod                      latest                ed84a3896251   50 seconds ago   14.7MB

let's start our production container on port 80

docker run -p 80:4000 --name app-prod app-prod

We can also add a Makefile to make our workflow easier

dev:
  docker-compose up

build:
  docker build -t app-prod . --target production

start:
  docker run -p 80:4000 --name app-prod app-prod

Next steps

With that, we should be able to take advantage of docker in our workflow and deploy our production images faster to any platform of our choice.

Feel free to reach out to me on Twitter if you face any issues.

31