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 12MB in size.

Creating a Simple Golang App

Let’s create a simple Golang app that we’ll containerize. Fire up your terminal and type the following command to create a Go project (Please make sure that the Go compiler is installed in your machine and the $GOPATH is set ) -

$ mkdir -p $GOPATH/src/github.com/callicoder/go-docker

Our project will be a simple Hello world server. Go to the root directory of the project and create a new file called hello_server.go -

$ cd $GOPATH/src/github.com/callicoder/go-docker
$ 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\n", name)
	w.Write([]byte(fmt.Sprintf("Hello, %s\n", 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.

  • Using dep for dependency management

    You can use dep to install the dependencies of the project. If you don’t have dep installed, then follow the instructions on the official Github page to install dep. Once installed, type the following command to initialize dep -

    $ dep init
    

    That’s it, you can now build and run the app like so -

    $ go build
    $ ./go-docker
    2018/12/22 19:16:02 Starting Server
    
  • Using Go Modules for dependency management

    If you’re using Go 1.11+, then you can use Go Modules for managing dependencies. Go Modules are enabled by default outside $GOPATH. But if your project is inside $GOPATH then you need to manually enable it by setting the following environment variable -

    # Activate Go modules inside $GOPATH (Add this to your ~/.bash_profile or ~/.bashrc file)
    export GO111MODULE=on
    

    That’s it. You can now initialize Go modules and run the app like so -

    $ go mod init 
    $ go build
    $ ./go-docker
    2018/12/22 19:33:54 Starting Server
    

    Note that, if you’ve created the project outside $GOPATH, then you need to specify the package while initializing Go modules -

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

    I highly recommend reading more about Go Modules

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 golang v1.11 base image
FROM golang:1.11

# Add Maintainer Info
LABEL maintainer="Rajeev Singh <rajeevhub@gmail.com>"

# Set the Current Working Directory inside the container
WORKDIR $GOPATH/src/github.com/callicoder/go-docker

# Copy everything from the current directory to the PWD(Present Working Directory) inside the container
COPY . .

# Download all the dependencies
# https://stackoverflow.com/questions/28031603/what-do-three-dots-mean-in-go-command-line-invocations
RUN go get -d -v ./...

# Install the package
RUN go install -v ./...

# This container exposes port 8080 to the outside world
EXPOSE 8080

# Run the executable
CMD ["go-docker"]

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      789MB    
    golang                        1.11                           2422e4d43e15        4 days ago          775MB
    
  • 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 /go-docker/logs. The container writes log files to /go-docker/logs/app.log. While running the 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 golang v1.11 base image
FROM golang:1.11

# Add Maintainer Info
LABEL maintainer="Rajeev Singh <callicoder@gmail.com>"

# Build Args
ARG APP_NAME=go-docker
ARG LOG_DIR=/${APP_NAME}/logs

# Create Log Directory
RUN mkdir -p ${LOG_DIR}

# Environment Variables
ENV LOG_FILE_LOCATION=${LOG_DIR}/app.log 

# Set the Current Working Directory inside the container
WORKDIR $GOPATH/src/github.com/callicoder/go-docker

# Copy everything from the current directory to the PWD(Present Working Directory) inside the container
COPY . .

# Download dependencies
RUN go get -d -v ./...

# Install the package
RUN go install -v ./...

# This container exposes port 8080 to the outside world
EXPOSE 8080

# Declare volumes to mount
VOLUME ["/go-docker/logs"]

# Run the binary program produced by `go install`
CMD ["go-docker"]

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 ~/app-logs
$ docker run -d -p 8080:8080 -v ~/app-logs:/go-docker/logs go-docker-volume
0c5d2b21ec3ea66f63f56b79725008ce2d229e0b6d07491aaa5b97a32fda6cb9

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

$ cd ~/app-logs
$ tail -200f 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       789MB
go-docker                     latest                         ed03a0732734        14 minutes ago      789MB
golang                        1.11                           2422e4d43e15        4 days ago          775MB

The golang:1.11 image that we’re using as our base is 775MB, and our application images are 789MB.

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:1.11 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 golang v1.11 base image
FROM golang:1.11 as builder

# Add Maintainer Info
LABEL maintainer="Rajeev Singh <rajeevhub@gmail.com>"

# Set the Current Working Directory inside the container
WORKDIR /go/src/github.com/callicoder/go-docker

# Copy everything from the current directory to the PWD(Present Working Directory) inside the container
COPY . .

# Download dependencies
RUN go get -d -v ./...

# Build the Go app
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o /go/bin/go-docker .


######## 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 /go/bin/go-docker .

EXPOSE 8080

CMD ["./go-docker"] 

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       789MB
go-docker                     latest                         ed03a0732734        14 minutes ago      789MB
go-docker-optimized           latest                         f2117958dff4        3 hours ago         12MB
golang                        1.11                           2422e4d43e15        4 days ago          775MB

Wow! Our optimized image is only 12MB 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.

Footnotes