Table of Contents
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.
- Go runtime version 1.18.9 or later installed
- 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
$ 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 usinggolang: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 calledsrc
.COPY go mod
.
command copies files and folders from your computer into a docker container. In this case we are copying go mod into thesrc
in your docker container.COPY . .
command copies everything in the current working directory in your machine into the docker containerRUN go build -o /go-docker
command compiles your source code and creates executables. The -o flag specifies the output file name for the compiled binaryEXPOSE 5000
command tells docker container to listen on port5000
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
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.
FROM golang:1.18.3-alpine as builder
command means we are building your image based ongolang:1.18.3-alpine
and asbuilder
gives a name to the stage for easier reference in later steps in our multistage build.RUN CGO_ENABLED=on GOOS=linux build -a installsuffix cgo -o ./main .
command reduces the image size.FROM alpine:latest
builds a minimal docker image of5MB
size big from scratch.WORKDIR /root/
command creates a new working directory in your new container.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.