If you write web service API implementations, it’s worth considering the Go programming language. A go implementation is particularly well-suited to deployment in docker. Go executables are self-contained and statically-linked, so no elaborate base image is required. You don’t need any libraries, or additional runtime support. In particular, a go web service can be built into a docker image that is derived from Docker’s reserved, minimal image, scratch. This means that the attack surface of your container is exactly that of your program — no potential for surprises from your base image.

Go Highlights

By now, go is a fairly mature language and environment. It started as a public open source project in late 2009, the stable Go 1.0 release occurred in early 2012 and the current release is 1.7. The express purpose of Go 1 is long-term stability, and the project guarantees that there will be no backward-incompatible changes to any Go 1 dot release. Participation is significant:

  • > 30,000 commits from 780 contributors in the 22 repos that comprise the project
  • > 90,000 go projects on Github

The syntax is strongly C-derived, and the language is fairly small, simple, and accessible. Thus, the barriers to entry are minimal. Go does not represent any giant leap forward in programming language (or runtime) design, but it has a number of nice features:

  • Control structures and data structures are familiar and useful
  • Functions are first-class citizens and closures are supported
  • The approach to methods and interfaces is interesting and powerful
  • The standard library is fairly modern and capable. Significant work can be done without the need for external packages.
    • archives and compression (tar, zip, bzip2, gzip, zlib, …)
    • cryptography and hashing
    • SQL database access
    • encodings (asn1, base64, csv, json, xml, …)
    • images (gif, jpeg, png, ..)
    • logging
    • networking (TCP, UDP, DNS, unix-domain sockets, http, mail/smtp, rpc, …)
    • reflection
    • runtime support (debug, execution tracing, …)
  • The language runtime support contains some significant capabilities:
    • garbage collection
    • rich support for concurrency: goroutines, channels and locking primitives
    • goroutines are lightweight threads — it is common to create thousands of goroutines in a single program

Go Lowlights

  • I’m not a big fan of the way that go manages dependencies on third-party packages. Basically, you must obtain and refer to the source code of your dependencies. If you follow prescribed conventions, go’s build tooling will locate and retrieve dependencies from public repositories such as github. But there is no prescription for how that source code is then managed in relation to your project. The community seems to have settled on vendoring as the answer, but there’s still quite a bit of variance in how different projects handle this. Also, the retrieval of dependencies becomes more complicated when they are stored in private repos, or in non-github SCMs.

Conclusions

If you’re looking to build light-weight web service APIs, go is worthy of your attention. It is a mature language with a capable library and runtime. It is competitive with nodejs for getting something up and running quickly (see the example “hello world” web servers in the following section). Perhaps most importantly, it is a perfect match with docker containers. Self-contained binaries with no external dependencies mean minimal docker image size and minimal attack surface for your deployed service.

Web Server Example

It is trivial to create a simple “hello world” webserver with minimal source code in Go. The size of the executable binary is also minimal.

Since nodejs seems to be a common choice when a quick/simple webserver implementation is desired, here is a side-by-side comparison of go and nodejs implementations of a “hello world” web service.

Observations

  • Source code is minimal in both languages, and is quite simple. There is full-featured and capable library code doing most of the work so that you don’t have to do it yourself.
  • The docker image for the go server is appreciably smaller than the node image (3.6MB vs. 40.8MB)
    • the node image is certainly small enough that you may not care about the difference in raw size. It is worth noting though that the node image contains a lot more “stuff” as you can see from the docker build output.
    • the Dockerfiles are trivial for both, although in the node case this is largely due to some nice work done in the ficusion/node-alpine:onbuild base image

Go Source


package main

import (
        "fmt"
        "io"
        "log"
        "net/http"
)

const port = 8000

func handler(w http.ResponseWriter, r *http.Request) {
        io.WriteString(w, "Hello World")
}

func main() {
        http.HandleFunc("/", handler)
        log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil))
}

Go Build

CGO_ENABLED=0 go build -a -ldflags '-s'

The go installation includes build tooling.

Note that on linux systems, use of go’s “net” package can produce dynamically-linked executables when “cgo” is enabled. The “CGO_ENABLED=0” environment setting used here will disable “cgo”, assuring a statically linked executable. The “-ldflags” setting removes symbols from the binary, minimizing its size.

Go Dockerfile

FROM scratch
ADD ./helloweb /helloweb
CMD ["/helloweb"]

Go Docker build


$ docker build -t gohello .
Sending build context to Docker daemon 3.579 MB
Step 1 : FROM scratch
 --->
Step 2 : ADD ./helloweb /helloweb
 ---> 81dca4495b55
Removing intermediate container 3c5bddcfd0c5
Step 3 : CMD /helloweb
 ---> Running in 5594bdfb6def
 ---> bbeed673a7d7
Removing intermediate container 5594bdfb6def
Successfully built bbeed673a7d7

 

$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE gohello latest bbeed673a7d7 About a minute ago 3.573 MB

Node Source


var express = require('express');

var port = 8000;

var app = express();

app.get('/', function(req, res) {
    res.send('Hello World');
});

app.listen(port, function() {
    console.log('listener on PORT: ' + port);
});

Node Build

No explicit build step, although it is necessary to run npm install for any/all packages used in the implementation (e.g., express in this example).

Node Dockerfile

FROM ficusio/node-alpine:onbuild
CMD ["node", "app.js"]

Node Docker build


$ docker build -t nodehello .
Sending build context to Docker daemon 1.456 MB
Step 1 : FROM ficusio/node-alpine:onbuild

Executing 4 build triggers...

Step 1 : COPY package.json npm-shrinkwrap.json* /app/
Step 1 : RUN info(){ printf '\n--\n%s\n--\n\n' "$"; }  && info '==> Installing build deps...'  && apk add --update --virtual build-deps     make gcc g++ python musl-dev openssl-dev zlib-dev     linux-headers binutils-gold  && info '==> Installing NPM modules...'  && npm install --production  && mv node_modules node_modules_new  && info '==> Finishing...'  && apk del build-deps  && npm cache clean  && rm -rf ~/.node-gyp /tmp/  && info '==> Deps installed! =)'
 ---> Running in df2b2daed9e6

--

==> Installing build deps...

fetch http://dl-4.alpinelinux.org/alpine/v3.3/main/x86_64/APKINDEX.tar.gz
fetch http://dl-4.alpinelinux.org/alpine/v3.3/community/x86_64/APKINDEX.tar.gz
(1/34) Upgrading musl (1.1.12-r2 -> 1.1.12-r5)
(2/34) Upgrading libcrypto1.0 (1.0.2g-r0 -> 1.0.2h-r2)
(3/34) Upgrading libssl1.0 (1.0.2g-r0 -> 1.0.2h-r2)
(4/34) Installing make (4.1-r0)
(5/34) Installing binutils-libs (2.25.1-r0)
(6/34) Installing binutils (2.25.1-r0)
(7/34) Installing gmp (6.1.0-r0)
(8/34) Installing isl (0.14.1-r0)
(9/34) Installing libgomp (5.3.0-r0)
(10/34) Installing libatomic (5.3.0-r0)
(11/34) Installing pkgconf (0.9.12-r0)
(12/34) Installing pkgconfig (0.25-r1)
(13/34) Installing mpfr3 (3.1.2-r0)
(14/34) Installing mpc1 (1.0.3-r0)
(15/34) Installing gcc (5.3.0-r0)
(16/34) Installing musl-dev (1.1.12-r5)
(17/34) Installing libc-dev (0.7-r0)
(18/34) Installing g++ (5.3.0-r0)
(19/34) Installing libbz2 (1.0.6-r4)
(20/34) Installing expat (2.1.1-r1)
(21/34) Installing libffi (3.2.1-r2)
(22/34) Installing gdbm (1.11-r1)
(23/34) Installing ncurses-terminfo-base (6.0-r6)
(24/34) Installing ncurses-terminfo (6.0-r6)
(25/34) Installing ncurses-libs (6.0-r6)
(26/34) Installing readline (6.3.008-r4)
(27/34) Installing sqlite-libs (3.9.2-r0)
(28/34) Installing python (2.7.12-r0)
(29/34) Installing zlib-dev (1.2.8-r2)
(30/34) Installing openssl-dev (1.0.2h-r2)
(31/34) Installing linux-headers (4.1.12-r0)
(32/34) Installing binutils-gold (2.25.1-r0)
(33/34) Installing build-deps (0)
(34/34) Upgrading musl-utils (1.1.12-r2 -> 1.1.12-r5)
Executing busybox-1.24.1-r7.trigger
OK: 219 MiB in 44 packages

--

==> Installing NPM modules...

hello@1.0.0 /app
-- express@4.14.0
  +-- accepts@1.3.3
  | +-- mime-types@2.1.11
  | |-- mime-db@1.23.0
  | -- negotiator@0.6.1
  +-- array-flatten@1.1.1
  +-- content-disposition@0.5.1
  +-- content-type@1.0.2
  +-- cookie@0.3.1
  +-- cookie-signature@1.0.6
  +-- debug@2.2.0
  |-- ms@0.7.1
  +-- depd@1.1.0
  +-- encodeurl@1.0.1
  +-- escape-html@1.0.3
  +-- etag@1.7.0
  +-- finalhandler@0.5.0
  | +-- statuses@1.3.0
  | -- unpipe@1.0.0
  +-- fresh@0.3.0
  +-- merge-descriptors@1.0.1
  +-- methods@1.1.2
  +-- on-finished@2.3.0
  |-- ee-first@1.1.1
  +-- parseurl@1.3.1
  +-- path-to-regexp@0.1.7
  +-- proxy-addr@1.1.2
  | +-- forwarded@0.1.0
  | -- ipaddr.js@1.1.1
  +-- qs@6.2.0
  +-- range-parser@1.2.0
  +-- send@0.14.1
  | +-- destroy@1.0.4
  | +-- http-errors@1.5.0
  | | +-- inherits@2.0.1
  | |-- setprototypeof@1.0.1
  | -- mime@1.3.4
  +-- serve-static@1.11.1
  +-- type-is@1.6.13
  |-- media-typer@0.3.0
  +-- utils-merge@1.0.0
  `-- vary@1.1.0

npm WARN hello@1.0.0 No description
npm WARN hello@1.0.0 No repository field.

--

==> Finishing...

(1/30) Purging build-deps (0)
(2/30) Purging make (4.1-r0)
(3/30) Purging g++ (5.3.0-r0)
(4/30) Purging gcc (5.3.0-r0)
(5/30) Purging binutils (2.25.1-r0)
(6/30) Purging isl (0.14.1-r0)
(7/30) Purging libatomic (5.3.0-r0)
(8/30) Purging libc-dev (0.7-r0)
(9/30) Purging python (2.7.12-r0)
(10/30) Purging musl-dev (1.1.12-r5)
(11/30) Purging openssl-dev (1.0.2h-r2)
(12/30) Purging zlib-dev (1.2.8-r2)
(13/30) Purging pkgconfig (0.25-r1)
(14/30) Purging pkgconf (0.9.12-r0)
(15/30) Purging linux-headers (4.1.12-r0)
(16/30) Purging binutils-gold (2.25.1-r0)
(17/30) Purging binutils-libs (2.25.1-r0)
(18/30) Purging mpc1 (1.0.3-r0)
(19/30) Purging mpfr3 (3.1.2-r0)
(20/30) Purging gmp (6.1.0-r0)
(21/30) Purging libgomp (5.3.0-r0)
(22/30) Purging libbz2 (1.0.6-r4)
(23/30) Purging expat (2.1.1-r1)
(24/30) Purging libffi (3.2.1-r2)
(25/30) Purging gdbm (1.11-r1)
(26/30) Purging readline (6.3.008-r4)
(27/30) Purging ncurses-libs (6.0-r6)
(28/30) Purging ncurses-terminfo (6.0-r6)
(29/30) Purging ncurses-terminfo-base (6.0-r6)
(30/30) Purging sqlite-libs (3.9.2-r0)
Executing busybox-1.24.1-r7.trigger
OK: 7 MiB in 14 packages

--

==> Deps installed! =)

Step 1 : COPY . /app/
Step 1 : RUN rm -rf node_modules   && mv node_modules_new node_modules
 ---> Running in 3e2566e00c5d
 ---> 29630f53b3e3
Removing intermediate container df2b2daed9e6
Removing intermediate container 68db4d6c31cf
Removing intermediate container 3e2566e00c5d
Removing intermediate container 181c329851d4
Step 2 : CMD node app.js
 ---> Running in 9f30a8c25c76
 ---> 93266b9137b5
Removing intermediate container 9f30a8c25c76
Successfully built 93266b9137b5

$ docker images
REPOSITORY            TAG                 IMAGE ID            CREATED             SIZE
nodehello             latest              93266b9137b5        50 seconds ago      40.8 MB


Interested in learning more about Yipee.io? Sign up for free to see how Yipee.io can help your team streamline their development process.