Building Go containers for Cloud Run

Gopher in a box

As I learned by writing this tutorial series, Go is great for making lightweight web apps, and it also lends itself very easily to being packaged and run in a docker container. Go compiles to a static binary (most of the time), which means your runtime container can be extremely small and efficient.

Once your app is containered up, deploying it to Google’s Cloud Run is really easy and gives you out-of-the-box auto-scaling and a secure HTTPS front-end. To follow along below, make sure you have the Google Cloud SDK set up on your local system.

First we’ll use a multi-stage docker build. In the directory of your Go web app, simply add the following Dockerfile:

FROM golang:1.13 as build
WORKDIR /go/src/app
COPY . .
RUN go build -v -o app .

FROM gcr.io/distroless/base
COPY --from=build /go/src/app/. /
CMD ["/app"]

In the first stage of this file, we use the golang:1.13 image to give us a build environment for our app. We copy everything from our local filesystem into the /go/src/app directory inside the container environment. Then we run go build to compile everything. Simple!

The next stage is the clever part. We start a new image from gcr.io/distroless/base and copy over just the files from our build stage (including our compiled runtime). In this example, we’re assuming there are supporting files to copy as well (for example, HTML and other static content), but we could refine this even more by just copying the binary application. Google’s distroless project contains just enough Linux to run our compiled binary. There’s no package manager, no shell, so it makes for a very efficient image.

You can build this image locally with Docker and push it to Google Container Registry, or just use Google’s Cloud Build to do the work for you:

gcloud builds submit --tag gcr.io/<your-project-id>/<your-image-name> .

Now you can deploy your app with just one more command:

gcloud run deploy <deployment-name> gcr.io/<your-project-id>/<your-image-name>

(Don’t forget to replace <your-project-id>, <your-image-name> and <deployment-name> in these examples)

In the Cloud Run console you should now see your new deployed service, complete with its URL.

Gopher in a box

Cloud Run offers some great features like traffic splitting and migration and custom domains. Deploying container based applications has never been easier! Except when things go wrong…

Missing libraries

You may have successfully tested your Docker image locally, but it’s failing when you deploy it. If you’re really unlucky, you may receive this error message in your docker logs:

exec user process caused "no such file or directory"

This error is almost completely useless, and will probably send you down the path of debugging the contents of your image looking for missing files or directories. What’s actually at fault is that there are system libraries missing that Go needs, because they are not part of your base image. This happens when Go’s statically compiled binaries aren’t quite as static as we’d like them to be.

To fix this, we need to send some extra parameters to the go build command so that it also compiles in any libraries it needs:

CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

This should fix the problem most of the time.

But I need CGO!

The above fix works great as long as switching off CGO isn’t a problem. But some Go libraries require CGO (for example, the rather handy go-sqlite3). So to create a completely static binary while still allowing CGO, we have to update our build line again.

RUN go get -d -v ./...
RUN CGO_ENABLED=1 GOOS=linux go build -a -ldflags '-linkmode external -extldflags "-static"' -o app .

First we run go get to include any external dependencies. Then we send extra parameters to go build to make sure it really includes everything. This results in a much longer build process, but hopefully a static binary that works.

Hooray for write once, run almost everywhere! 😊

Share this on:
comments powered by Disqus