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\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. 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 /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.