Containerizing Golang App with Docker [Tutorial]


Written By - Antony Shikubu
Advertisement

Introduction

In this article, you will learn how to dockerize a simple Go web application. You will build from scratch a web application in Go that utilizes the net/http package in Go. You will then use Docker to build, share and run your web application.

There are other available containerization technologies apart from Docker such as Containerd, rklet etc, but Docker is the most known and used in the software world.

Docker is a containerization technology  that developers use to package software into a standardized unit for development, shipment and deployment. Docker makes it easy to build , share and run containers. It is worth noting that Docker is built using Golang.

 

Pre-requisite

To be able to follow along this article, ensure you have a basic understanding of how web servers work, because your application is  a simple web application. Apart from web server knowledge , please ensure you have the following technologies installed in your machine.

  1. Go runtime version 1.18.9  or later installed
  2. Docker version  installed

 

Application structure

This application has a very simple application structure that is made up of two Go files namely main.go and main_test.go. Before dockerizing our application, we will run some tests as practice for TDD. Using your terminal navigate to your working directory and issue the below commands.

Create application folder and navigate into it

$ mkdir go-docker && cd go-docker

Create main.go and main_test.go files

$ touch main.go main_test.go

Initialize Go module

Advertisement
$ go mod init go-docker

 

Main Application code

In this section we write code for handling HTTP requests. We will use the net/http package to run the application. This application consists of two routes, namely “/” and “/counter” . The “/” route will be the home route while the “/counter” will increment a counter whenever we make a GET HTTP request to the server.

On the other hand, we will add code for testing our code. This code will be put in the main_test.go file.

main.go

package main


import (
   "fmt"
   "log"
   "net/http"
)


var counter int


func main() {
   fmt.Println("Welcome to counter application")
   http.HandleFunc("/", HomeRouter)
   http.HandleFunc("/counter", CountRouter)


   if err := http.ListenAndServe(":5000", nil); err != nil {
       log.Fatal(err)
   }
   fmt.Println("Server running at localhost:5000")


}


func HomeRouter(w http.ResponseWriter, r *http.Request) {
   w.Write([]byte("Introduction to Go with Docker\n"))
}


func CountRouter(w http.ResponseWriter, r *http.Request) {
   fmt.Printf("*** HTTP request counter at : %d\n", counter)


   w.Write([]byte(fmt.Sprintf("Counter : %d\n", counter)))
   counter++
}

To run the application, enter the below common in the terminal.

$ go run main.go

Welcome to counter application

Using your web browser, add localhost:5000 in the address bar and hit enter. A text Introduction to Go with Docker” will be shown. To use the counter, add /counter in the existing URL in the address bar such that you have localhost:5000/counter and hit enter. In the terminal you should see the below output to indicate that the server is running and serving requests.

$ go run main.go
Welcome to counter application
*** HTTP request counter at : 0
*** HTTP request counter at : 1
*** HTTP request counter at : 2
*** HTTP request counter at : 3
*** HTTP request counter at : 4

Now that the application is running , add unit test code to ensure that everything

 

Create unit test package

main_test.go

package main


import (
   "io/ioutil"
   "net/http"
   "net/http/httptest"
   "testing"
)


func TestHomeRouter(t *testing.T) {
   r := httptest.NewRequest(http.MethodGet, "/", nil)
   w := httptest.NewRecorder()
   HomeRouter(w, r)


   res := w.Result()
   defer res.Body.Close()


   data, err := ioutil.ReadAll(res.Body)
   if err != nil {
       t.Errorf("expected error to be nil got %v ", err)
   }


   if string(data) != "Introduction to Go with Docker\n" {
       t.Errorf("expected 'Introduction to Go with Docker\n' got %s", string(data))
   }
}


func TestCountRouter(t *testing.T) {
   r := httptest.NewRequest(http.MethodGet, "/", nil)
   w := httptest.NewRecorder()
   CountRouter(w, r)


   res := w.Result()
   defer res.Body.Close()


   data, err := ioutil.ReadAll(res.Body)
   if err != nil {
       t.Errorf("expected error to be nil got %v ", err)
   }
   counter := string(data)
   expectedCounter1 := "Counter : 0\n"


   if expecterCounter1 != counter {
       t.Errorf("expected counter to be %s got %s", expectedCounter1, counter)
   }
}

To run the test issue the below command in the terminal.

$ go test -v
=== RUN   TestHomeRouter
--- PASS: TestHomeRouter (0.00s)
=== RUN   TestCountRouter
*** HTTP request counter at : 0
--- PASS: TestCountRouter (0.00s)
PASS
ok      go-docker       0.004s

 

Create and Build Docker Image with GO Program

Now it's time to dockerize your counter application. In the root directory of your application create a new file called Dockerfile . Please ensure that the name is exactly the same as shown here. Add the below code in the Dockerfile file.

Create Dockerfile

FROM golang:1.18.3-alpine
WORKDIR /src
COPY go.mod .
RUN go mod download
COPY . .
RUN go build -o /go-docker
EXPOSE 5000
CMD ["/go-docker"]

Here,

  • FROM  golang:1.18.3-alpine command refers to the base image we want to run our container. In this case we are using golang:1.18.3-alpine.Alpine is a minimal image based on the Alpine
    Linux with a complete package index and very small in size.
  • WORKDIR /src command defines the working directory of your docker container at any given time. In this case our working directory is called src.
  • COPY go mod . command copies files and folders from your computer into a docker container. In this case we are copying go mod into the src  in your docker container.
  • COPY .  .  command copies everything in the current working directory in your machine into the docker container
  • RUN go build -o /go-docker command compiles your source code and creates executables. The -o flag specifies the output file name for the compiled binary
  • EXPOSE 5000 command tells docker container to listen on port 5000
  • CMD [“/go-docker”] command run your application.

 

To be able to use our dockerized application we need to create a docker image and use the docker image to run a container. Issue the below commands to build, and run your docker container.

Build a docker image

$ docker build -t go-docker .

Run docker container

$ docker run -p “5000:5000” go-docker

Build docker image

$ docker build -t go-docker .
Sending build context to Docker daemon  6.144kB
Step 1/8 : FROM golang:1.18.3-alpine
---> 155ead2e66ca
Step 2/8 : WORKDIR /src
---> Using cache
---> 0746dc168e0c
Step 3/8 : COPY go.mod .
---> Using cache
---> 5296c2125e0e
Step 4/8 : RUN go mod download
---> Using cache
---> 5dd1de68a052
Step 5/8 : COPY . .
---> Using cache
---> faef8a88496b
Step 6/8 : RUN go build -o /go-docker
---> Using cache
---> 6aed2a7ffacc
Step 7/8 : EXPOSE 5000
---> Using cache
---> 6dbcced72c87
Step 8/8 : CMD ["/go-docker"]
---> Using cache
---> c1ea897a0f98
Successfully built c1ea897a0f98
Successfully tagged go-docker:latest

Run docker container

$ docker run -p "5000:5000" go-docker
Welcome to counter application

Now we have both containers and images created and ready for use. The next step is to check the image size. Run the below command in the terminal to get a list of all images in your computer with their sizes. In our case we will be checking the size of <b>“go-docker”</b> image.

$ docker images
REPOSITORY          TAG             IMAGE ID       CREATED         SIZE
go-docker           latest          c1ea897a0f98   8 minutes ago   334MB

In the above result, your go-docker image is 344MB in size. That is quite a big sized application. We can reduce the size of the image using a multistage image creation. Now go back to your Dockerfile and add the below docker commands.

Dockefile

Advertisement
FROM golang:1.18.3-alpine as builder
WORKDIR /src
COPY go.mod .
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o ./main .
FROM alpine:latest
WORKDIR /root/
COPY --from=builder /src/main .
EXPOSE 5000
CMD ["./main"]

Build image

$ docker build -t go-docker .
Run container

$ docker run -p "5000:5000" go-docker

Check image size

$ docker images
REPOSITORY         TAG             IMAGE ID       CREATED              SIZE
go-docker          latest          2ba50e10d290   8 seconds ago        13.8MB
<none>             <none>          edb10acfebcd   9 seconds ago        388MB

In the above Docker commands we have new commands that will help in reducing the code size from 388MB to 13.8MB. In this section we will discuss the new commands.

  1. FROM golang:1.18.3-alpine as builder command means we are building your image based on golang:1.18.3-alpine  and as builder gives a name to the stage for easier reference in later steps in our multistage build.
  2. RUN CGO_ENABLED=on GOOS=linux build -a installsuffix cgo -o ./main . command reduces the image size.
  3. FROM alpine:latest builds a minimal docker image of 5MB size big from scratch.
  4. WORKDIR /root/ command creates a new working directory in your new container.
  5. COPY –from=builder /src/main . copies the files from /src/main in the build stage called builder to the current working directory(.) in the container file system

 

Summary

This is a simple article that demonstrates how to get started with Golang and Docker. In this article we learn how to build an image and run a container using Docker. We also learn how to create a single stage image and a multistage image. The multistage is super useful because it reduces the size of our image significantly.

 

Can't find what you're searching for? Let us assist you.

Enter your query below, and we'll provide instant results tailored to your needs.

If my articles on GoLinuxCloud has helped you, kindly consider buying me a coffee as a token of appreciation.

Buy GoLinuxCloud a Coffee

For any other feedbacks or questions you can either use the comments section or contact me form.

Thank You for your support!!

Leave a Comment