Deploying Go applications with Docker

When dealing with a new language, if it’s migrating from PHP to Node, or if it’s your first time trying out Go, you eventually get to the point when you have to think about how you will use your new application in production. How you will deploy it, how you will run it, and how you will make sure that it’s running well. I’m a big advocate od Docker, let me show you why.

Deploying an application can be as simple as copying the binary produced with go build, and managing scripts and configuration around it. But deploying the application with docker itself has immediate benefits. Creating an image containing the docker binary might seem like an unusual or wasteful practice, but gives many benefits.

  1. The application can be downloaded and run with a simple docker run command (pull mode),
  2. Docker images can be transfered between hosts (push mode),
  3. Docker is also a simple process manager (--restart=always)

The other option is to create init.d scripts, or run an instance of supervisord to manage execution and restarting of your service. There are also other benefits from docker - isolation from a security standpoint for one.

Even if your application uses many hosts because of scale, running it from Docker should be considered - the Docker images can be versioned, and if you’re not exactly doing upgrades of the data model, this means that you can also safely roll-back if you deployed a bug to production.

Automating a deployment of an application can be a dirty business, but when you’re deploying the complete environment for the application along with it, you’re saving a lot of time by avoiding certain discussions like “Did you copy all the files?”, or “I don’t see the changes, when will the deploy finish?”, “This one file is outdated.” and so on.

In a sense, deployment with docker is “atomic”. In another sense - it’s progressive. You can spawn new container instances of the updated application, and when you verify it works, you can spin down the old, out of date containers. You can use clustering tools like Docker Swarm to achieve this from the start.

Creating a Docker image

Docker images are created from a Dockerfile. Since Go binaries are ‘portable’, they can run on the smallest Docker image around, Alpine Linux. Keep in mind, that sometimes, just having the binary is not enough, as some of the libraries used need external data files. For example, a web server might need /etc/mime.types to return correct Content-type headers for files, an SSL library would need a list of SSL root certificates, usually found in /etc/ssl/certs. This is why it’s better to use Alpine Linux, instead of ‘scratch’ as the base image - it provides a package manager, allowing you to install some things, without having to COPY them in.

I created a short Go application, flags.go

package main

import "github.com/namsral/flag"
import "fmt"
import "os"

func main() {
  fs := flag.NewFlagSetWithEnvPrefix(os.Args[0], "GO", 0)
  var (
    port = fs.Int("port", 8080, "Port number of service")
  )
  fs.Parse(os.Args[1:])

  fmt.Printf("Server port: %d\n", *port)
}

The application only reads a flag from the environment, argument or config, using the namsral/flag package. It’s a starting point to an application server that you might write. It’s functional enough to deploy it, but first we need to create a Dockerfile.

FROM alpine:latest

ADD flags /flags
RUN chmod +x /flags ; sync; sleep 1

WORKDIR /

ENV GO_PORT 8080
EXPOSE $GO_PORT

ENTRYPOINT ["/flags"]

Building the Docker image is a one liner -

#!/bin/bash
go build flags.go
docker build --rm --no-cache=true -t go-flags .

We can run our application with full control of the network port on which it runs. You can expose this port on the host using -p or -P docker options. If you’re using --net=host mode, you can prevent some conflicts here with existing services by running multiple containers on different ports.

$ docker run -e GO_PORT=81 --rm -i go-flags
Server port: 81

The images you create can be used on multiple hosts, by using a Docker registry like Docker Hub or Quay.io. You can also set up your own docker registry. The most basic way to transfer the images between hosts is to save and load them.

$ docker save -o go-flags.tar go-flags
$ scp go-flags.tar luxor:/root
go-flags.tar                                                                       100% 7565KB   2.5MB/s   00:03
$ ssh luxor "docker load -i go-flags.tar"
$ ssh luxor "docker run -e GO_PORT=1234 --rm go-flags"
Server port: 1234

Depending on what you need, pick a deployment strategy which works for you.

I’m of the opinion that each container should be disposable. If you provide any kind of data storage, it should be done with volume mounts, or some external API service (Amazon S3 for example). You know exactly which data needs to be backed up, and it doesn’t include the application itself. The app can be rebuilt at any time, and the container re-created from scratch. I’m aiming to reproduce these processes with automation, not by following a setup checklist. My longest running practice is, that I can remove any running container with “docker rm -f [name]“. Starting them again or creating a new environment should be just as trivial.

If you have been following along, you’ll remember that I’m writing a book API foundations in Go. The contents of this post are a part of the chapter “Running your API in production”. Please check out my book, a sample first chapter is available. Check if out if you’re interested in Go or Docker, and give me your opinions on how I can make the book better.

While I have you here...

It would be great if you buy one of my books:

I promise you'll learn a lot more if you buy one. Buying a copy supports me writing more about similar topics. Say thank you and buy my books.

Feel free to send me an email if you want to book my time for consultancy/freelance services. I'm great at APIs, Go, Docker, VueJS and scaling services, among many other things.