Dockerize your Go app

karanpratapsingh

Karan Pratap Singh

Posted on July 24, 2021

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
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Start! Start! Start!

docker-compose up
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

Let's checkout our image using docker images command

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

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
Enter fullscreen mode Exit fullscreen mode

Let's add a build our production image

docker build -t app-prod . --target production
Enter fullscreen mode Exit fullscreen mode

Let's check out our built production image

docker images
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

let's start our production container on port 80

docker run -p 80:4000 --name app-prod app-prod
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
karanpratapsingh
Karan Pratap Singh

Posted on July 24, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related