31
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
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)
}
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
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 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
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