Containers are standalone software packages that bundle an application with all its dependencies, tools, libraries, runtime, configuration files, and anything else necessary to run the application.

Containers abstract away the application from any of the environments that they will run on. That means, containerized apps run consistently across environments from dev to staging to production.

Containers are created from images. The container image is the actual package that contains everything needed to run the application. And, a running instance of an image is referred to as a container.

Docker is a software platform that lets you build, ship, and run containers. You can read more about Docker and Containers in general from the official documentation.

In this article, you’ll learn how to build a docker image for a Go application. We’ll start with a simple image, then we’ll learn how to attach a volume to the docker image. Finally, we’ll build an optimized image using docker’s multi-stage builds that’s only 12.8MB in size.

Creating a Simple Golang App

Let’s create a simple Go app that we’ll containerize. Fire up your terminal and type the following command to create a Go project -

$ mkdir go-docker

We’ll use Go modules for dependency management. Change to the root directory of the project and initialize Go modules like so -

$ cd go-docker $ go mod init github.com/callicoder/go-docker

We’ll be creating a simple Hello world server. Create a new file called hello_server.go -

$ touch hello_server.go

Following are the contents of the hello_server.go file -

package main import ( "context" "fmt" "log" "net/http" "os" "os/signal" "syscall" "time" "github.com/gorilla/mux" "gopkg.in/natefinch/lumberjack.v2" ) func handler(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() name := query.Get("name") if name == "" { name = "Guest" } log.Printf("Received request for %s

", name) w.Write([]byte(fmt.Sprintf("Hello, %s

", name))) } func main() { // Create Server and Route Handlers r := mux.NewRouter() r.HandleFunc("/", handler) srv := &http.Server{ Handler: r, Addr: ":8080", ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, } // Configure Logging LOG_FILE_LOCATION := os.Getenv("LOG_FILE_LOCATION") if LOG_FILE_LOCATION != "" { log.SetOutput(&lumberjack.Logger{ Filename: LOG_FILE_LOCATION, MaxSize: 500, // megabytes MaxBackups: 3, MaxAge: 28, //days Compress: true, // disabled by default }) } // Start Server go func() { log.Println("Starting Server") if err := srv.ListenAndServe(); err != nil { log.Fatal(err) } }() // Graceful Shutdown waitForShutdown(srv) } func waitForShutdown(srv *http.Server) { interruptChan := make(chan os.Signal, 1) signal.Notify(interruptChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) // Block until we receive our signal. <-interruptChan // Create a deadline to wait for. ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() srv.Shutdown(ctx) log.Println("Shutting down") os.Exit(0) }

The server uses gorilla mux to create HTTP routes. It listens for connections on port 8080 .

Building and Running the app locally

Let’s first build and run our application locally. Please type the following command to build the app -

$ go build

The build command will produce an executable file named go-docker . You can run the binary executable like so -

$ ./go-docker 2018/12/22 19:16:02 Starting Server

Our hello server is now running. Try interacting with the hello server using curl -

$ curl http://localhost:8080 Hello, Guest $ curl http://localhost:8080?name=Rajeev Hello, Rajeev

Defining the Docker image using a Dockerfile

Let’s define the Docker image for our Go application. Create a new file called Dockerfile inside the root directory of your project with the following contents -

# Dockerfile References: https://docs.docker.com/engine/reference/builder/ # Start from the latest golang base image FROM golang:latest # Add Maintainer Info LABEL maintainer="Rajeev Singh <rajeevhub@gmail.com>" # Set the Current Working Directory inside the container WORKDIR /app # Copy go mod and sum files COPY go.mod go.sum ./ # Download all dependencies. Dependencies will be cached if the go.mod and go.sum files are not changed RUN go mod download # Copy the source from the current directory to the Working Directory inside the container COPY . . # Build the Go app RUN go build -o main . # Expose port 8080 to the outside world EXPOSE 8080 # Command to run the executable CMD ["./main"]

Building and Running the Docker image

Now that we have the Dockerfile defined, let’s build and run the docker image -

Building the image $ docker build -t go-docker . You can list all the available images by typing the following command - $ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE go-docker latest ed03a0732734 21 seconds ago 830MB golang latest 2422e4d43e15 4 days ago 814MB

Running the Docker image Type the following command to run the docker image - $ docker run -d -p 8080:8080 go-docker fff93d13a4849accd965d5d342b7f6bf55ba50b7b2202b16f4188c076e667563

Finding Running containers You can list all the running containers like so - $ docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES fff93d13a484 go-docker "go-docker" 13 seconds ago Up 12 seconds 0.0.0.0:8080->8080/tcp hardcore_kare

Interacting with the app running inside the container Finally, Let’s interact with our Go application that is running inside the container - $ curl http://localhost:8080?name=Rajeev Hello, Rajeev

Stopping the container To stop the container, type the following command with the container id - $ docker container stop fff93d13a484 fff93d13a484

Attaching a Volume to the Docker Container

Let’s see another example of Dockerfile. This time, we’ll attach a volume to the container that will be used to store all the logs generated by the application. A volume is used to share directories from the host OS with the container or persist data generated from the container on the Host os.

Dockerfile.volume

In the following Dockerfile, we declare a volume at path /app/logs . The container will write log files to /app/logs/app.log . When we run the docker image, we can mount a directory of the Host OS to this volume. Once we do that, we’ll be able to access all the log files from the mounted directory of the Host OS.

# Dockerfile References: https://docs.docker.com/engine/reference/builder/ # Start from the latest golang base image FROM golang:latest # Add Maintainer Info LABEL maintainer="Rajeev Singh <callicoder@gmail.com>" # Set the Current Working Directory inside the container WORKDIR /app # Build Args ARG LOG_DIR=/app/logs # Create Log Directory RUN mkdir -p ${LOG_DIR} # Environment Variables ENV LOG_FILE_LOCATION=${LOG_DIR}/app.log # Copy go mod and sum files COPY go.mod go.sum ./ # Download all dependencies. Dependencies will be cached if the go.mod and go.sum files are not changed RUN go mod download # Copy the source from the current directory to the Working Directory inside the container COPY . . # Build the Go app RUN go build -o main . # This container exposes port 8080 to the outside world EXPOSE 8080 # Declare volumes to mount VOLUME [${LOG_DIR}] # Run the binary program produced by `go install` CMD ["./main"]

Let’s build the image by typing the following command -

$ docker build -t go-docker-volume -f Dockerfile.volume .

Let’s now run the image. Notice how we mount a directory of the Host OS to the volume specified by the docker container -

$ mkdir -p ~/logs/go-docker $ docker run -d -p 8080:8080 -v ~/logs/go-docker:/app/logs go-docker-volume 0c5d2b21ec3ea66f63f56b79725008ce2d229e0b6d07491aaa5b97a32fda6cb9

That’s it. You can now access your application’s logs from the ~/logs/go-docker directory -

$ tail -200f ~/logs/go-docker/app.log 2018/12/22 14:13:27 Starting Server

Building an Optimized Docker image for Go applications using Multi-stage builds

The docker images that we built in the previous sections are quite big. If you type docker image ls , you can see the size of all the images -

$ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE go-docker-volume latest f7b09f7e8a5a 9 minutes ago 830MB go-docker latest ed03a0732734 14 minutes ago 830MB golang latest 2422e4d43e15 4 days ago 814MB

The golang:latest image that we’re using as our base is 814MB in size, and our application images are 830MBs in size.

To reduce the size of the docker image, we can use a multi-stage build. The first stage of the multi-stage build will use the golang:latest image and build our application. The second stage will use a very lightweight Alpine linux image and will only contain the binary executable built by the first stage.

This way, our final image will be very small because It won’t have all the Golang runtime. It will only contain the things needed to run the binary executable -

Dockerfile.multistage

# Dockerfile References: https://docs.docker.com/engine/reference/builder/ # Start from the latest golang base image FROM golang:latest as builder # Add Maintainer Info LABEL maintainer="Rajeev Singh <rajeevhub@gmail.com>" # Set the Current Working Directory inside the container WORKDIR /app # Copy go mod and sum files COPY go.mod go.sum ./ # Download all dependencies. Dependencies will be cached if the go.mod and go.sum files are not changed RUN go mod download # Copy the source from the current directory to the Working Directory inside the container COPY . . # Build the Go app RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . ######## Start a new stage from scratch ####### FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR /root/ # Copy the Pre-built binary file from the previous stage COPY --from=builder /app/main . # Expose port 8080 to the outside world EXPOSE 8080 # Command to run the executable CMD ["./main"]

Type the following command to build the above image -

$ docker build -t go-docker-optimized -f Dockerfile.multistage .

Now let’s see the size of the image -

$ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE go-docker-volume latest f7b09f7e8a5a 9 minutes ago 830MB go-docker latest ed03a0732734 14 minutes ago 830MB go-docker-optimized latest f2117958dff4 3 hours ago 12.8MB golang latest 2422e4d43e15 4 days ago 814MB

Wow! Our optimized image is only 12.8MB in size. That’s awesome!

Conclusion

In this article, you learned how to build a docker image for your Go application. We started with a simple image, then we learned how to attach a volume to the image. Finally, we learned how to build an optimized image for our Go application.

You can find the complete source code for the Go app and all the Dockerfiles in the Github Repository.

I hope you enjoyed the article. Thanks for reading. See you in the next post.