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
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.
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.
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! 😊