Asset 31Asset 32
Navigate back to the homepage

Dockerize your Go Microservice

Sebastian Ojeda
September 30th, 2019 · 2 min read

Docker. Microservice. Two of the biggest buzz-words over the last couple of years. As new technology and architectural styles come and go, these are two that I have become particularly interested in. Docker as the most popular containerization platform, and microservices, a way of loosely coupling services. These come together in a very natural way, so I figure why not discuss how to put the two together, thus creating: a Docker-ized Go microservice.

Creating the Dockerfile

The Dockerfile is pretty straight forward but it is crucial that we handle a few subtle details. Best practices dictate that you want to maximize the number of layers that are cached when running subsequent Docker builds, so the order in which we run commands is important. We also want to minimize the image size as much as possible, to decrease the time it takes to send and build our Docker images. We can accomplish this using a few Go compiler options to minimize the size of our binary and by utilizing the builder pattern in our Docker image. Finally, we want to ensure that we take into account security when creating our image. We will do all this with the following Dockerfile:

1# We will use the alpine linux tag in our image as it
2# provides a resource efficient and secure base.
3FROM golang:1.13-alpine AS builder
4
5LABEL maintainer="Sebastian Ojeda <sebastian@fullstackgo.io>"
6
7# We can make our container more secure by creating an
8# unprivileged user to run our service.
9RUN adduser -D -u 1000 serviceuser
10
11RUN apk add --update libcap gcc musl-dev git
12
13WORKDIR /go/src/app
14
15# Copy over and install dependencies (if any). This MUST
16# be run prior to copying over any remaining source files
17# to minimize subsequent build time.
18COPY go.mod go.sum ./
19
20RUN go mod download
21
22RUN go mod verify
23
24# Copy over remaining source files.
25COPY . .
26
27# We wish to include all dependencies in the generated binary
28# as well as specify the target os and architecture.
29RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -a -o main
30
31RUN setcap CAP_NET_BIND_SERVICE=+eip main
32
33# For a lightweight image, we will copy over our binary
34# into a scratch image.
35FROM scratch
36
37# Copy over certificates (which we will generate shortly), user info, and our Go binary.
38COPY --from=builder /go/src/app/server.crt /go/src/app/server.key /etc/ssl/
39COPY --from=builder /etc/passwd /etc/passwd
40COPY --from=builder /go/src/app/main /
41
42EXPOSE 443
43
44# Remeber to use our unprivileged user to run the service.
45USER serviceuser
46
47CMD ["/main"]

Building a Go microservice.

So now comes the fun part. Let’s build our Go microservice. Our microservice will be handling a secure request and returning a random compliment to the user.

First thing’s first. How do we secure our service? Well, this is made easy via the net/http package. Namely, using the ListenAndServeTLS function, whose signature is:

1func (srv *Server) ListenAndServeTLS(certFile string, keyFile string) error

To create a CSR for prod-like environments (i.e. not localhost), we begin by creating and RSA key pair for signing using openssl.

1openssl genrsa -out server.key 2048

And then use that key to create our certificate signing request (CSR).

1openssl req -new -sha256 -key server.key -out server.csr

Typically, you must then submit that CSR to a certificate authority (CA) to be signed before you can use it in your application. However, for testing purposes, we will be using a self-signed certificate instead, which can be created using the following commands:

1# Generates our RSA private key.
2openssl genrsa -out server.key 2048
3
4# Creates a self signed cert from our key file.
5openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650

We’ll be using the gorila/mux package to create a mux server, so we’ll need to install the package first.

1go get -u github.com/gorilla/mux

At its simplest, our HTTP server can be spun up with the following.

1func main() {
2 mux := mux.NewRouter()
3 mux.HandleFunc("/", Handler) // Our request Handler function
4
5 srv := &http.Server{
6 Addr: ":8080",
7 ReadTimeout: 5 * time.Second,
8 WriteTimeout: 10 * time.Second,
9 IdleTimeout: 120 * time.Second,
10 Handler: mux,
11 }
12
13 log.Println("Starting server...")
14 err := srv.ListenAndServe()
15 if err != nil {
16 log.Fatalf("Unable to start the server: %v", err)
17 }
18}

However, to make use of TLS, we can start by defining our TLS configuration.

1// See https://blog.cloudflare.com/exposing-go-on-the-internet/ for
2// information regarding these settings
3tlsConfig := &tls.Config{
4 // Causes servers to use Go's default ciphersuite preferences,
5 // which are tuned to avoid attacks. Does nothing on clients.
6 PreferServerCipherSuites: true,
7 // Only use curves which have assembly implementations
8 CurvePreferences: []tls.CurveID{
9 tls.CurveP256,
10 tls.X25519, // Go 1.8 only
11 },
12
13 MinVersion: tls.VersionTLS12,
14 CipherSuites: []uint16{
15 tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
16 tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
17 tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, // Go 1.8 only
18 tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, // Go 1.8 only
19 tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
20 tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
21 },
22}

Which allows us to now upgrade our server to use TLS.

1srv := &http.Server{
2 Addr: ":443", // Use TLS port
3 ReadTimeout: 5 * time.Second,
4 WriteTimeout: 10 * time.Second,
5 IdleTimeout: 120 * time.Second,
6 TLSConfig: tlsConfig, // Add our config
7 Handler: mux,
8}

Finally, will all the boilerplate out of the way, we can define our handler function and put everything together!

1package main
2
3import (
4 "crypto/tls"
5 "log"
6 "math/rand"
7 "net/http"
8 "time"
9)
10
11var compliments = []string{
12 "You are awesome!",
13 "You’ve got all the right moves!",
14 "On a scale from 1 to 10, you’re an 11.",
15 "I bet you sweat glitter.",
16 "You’re more fun than bubble wrap.",
17 "Your creative potential seems limitless.",
18 "You’re even better than a unicorn, because you’re real.",
19 "You never forget to fill the ice-cube tray.",
20 "You could open that jar of mayonnaise using only 3 fingers.",
21}
22
23func Handler(w http.ResponseWriter, r *http.Request) {
24 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
25 w.WriteHeader(http.StatusOK)
26 w.Write([]byte(compliments[rand.Intn(len(compliments))]))
27}
28
29func main() {
30 mux := http.NewServeMux()
31 mux.HandleFunc("/", Handler)
32
33 // See https://blog.cloudflare.com/exposing-go-on-the-internet/ for
34 // information regarding these settings
35 tlsConfig := &tls.Config{
36 // Causes servers to use Go's default ciphersuite preferences,
37 // which are tuned to avoid attacks. Does nothing on clients.
38 PreferServerCipherSuites: true,
39 // Only use curves which have assembly implementations
40 CurvePreferences: []tls.CurveID{
41 tls.CurveP256,
42 tls.X25519, // Go 1.8 only
43 },
44
45 MinVersion: tls.VersionTLS12,
46 CipherSuites: []uint16{
47 tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
48 tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
49 tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, // Go 1.8 only
50 tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, // Go 1.8 only
51 tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
52 tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
53 },
54 }
55
56 srv := &http.Server{
57 Addr: ":443",
58 ReadTimeout: 5 * time.Second,
59 WriteTimeout: 10 * time.Second,
60 IdleTimeout: 120 * time.Second,
61 TLSConfig: tlsConfig,
62 Handler: mux,
63 }
64
65 log.Println("Starting server...")
66 err := srv.ListenAndServeTLS("/etc/ssl/server.crt", "etc/ssl/server.key")
67 if err != nil {
68 log.Fatalf("Unable to start the server: %v", err)
69 }
70}

And we can now build and test our image.

1# The `DOCKER_BUILDKIT` variable is needed here due to
2# a known bug https://github.com/moby/moby/issues/35699
3DOCKER_BUILDKIT=1 docker build --rm -t go-microservice .
4
5docker run -it -p 443:443 --name go-service go-microservice

When we send a request to localhost:433 we should see our service up and running ready to dish out compliments!

1curl -skL https://localhost:443
2You’re even better than a unicorn, because you’re real.

More articles from Fullstack Go

Prefix Trees in Go

A prefix tree (or trie) data structure is great for text searching. Let's try implementing one in Go.

September 25th, 2019 · 2 min read

Building an API endpoint with AWS Lambda and Go

Cloud computing has taken over. In this article, let's explore how to create an API endpoint with Go on AWS.

September 19th, 2019 · 3 min read
© 2019 Fullstack Go
Link to $https://twitter.com/fullstack_goLink to $https://github.com/fullstackgo