Writing a Hello World Go Container Web Application

In this blog post, I will show you how to build a hello world container-based web application in the go programming language. The reason I want to do this is because I need a very small container image to do some testing in Kubernetes. I’ll also highlight some of the pitfalls I ran into to hopefully have you some time in your learnings.

Let’s build and test it locally first

Before you build a container-based application, you need an application. So let’s go ahead and build a simple hello world app in go, but running on our local system as a traditionally compiled program. I want to make sure my application works before I move onto the container build process. You’ll need to install the go programming language development tools. On a Mac, you can do that with brew install go. For other operating systems, check out the install page.

brew install go

Once we have go installed, let’s start working on our program. I want to make a simple web application that when I access the application with a web browser, it has some output, like…Hello, world! The code for that is below. We import a few libraries based on our needs. Then in the main() function, we have the code to do just that. Format the output, then expose that as a static webpage accessed via HTTP on port 8080.

package main

import (
    "fmt"
    "net/http"
    "os"
)

func main() {
  hostname, err := os.Hostname()
  if err != nil {
    panic(err)
  }

  http.HandleFunc("/", func (w http.ResponseWriter, r *http.Request){
  fmt.Fprintf(w, "Hello, world!\nVersion: 1.0.0\n" + hostname + "\n")
  })

  fs := http.FileServer(http.Dir("static/"))
  http.Handle("/static/", http.StripPrefix("/static/", fs))

  http.ListenAndServe(":8080", nil)
}

Now, save all the above code into a file named hello-app.go. Then next, you’ll need to compile the application with the go command. The code below will compile the application in the file hello-app.go and then write the binary to hello-app, defined by the -o parameter.

go build -o hello-app hello-app.go 

With our program built, you can launch it with the code here.

./hello-app

Since it’s a simple web application, we can access it with curl, and that’s what we’re doing in the code below. And you can see the output on standard out. Notice the hostname output is the hostname of my local system.

curl http://localhost:8080
Hello, world!
Version: 1.0.0
Hostname: Anthonys-Air.localdomain

So now that we know our application works, let’s start putting it in a container.

Let’s put our app in a container

This section will use a multi-stage build process. A primary benefit of using a multi-stage build is the final container will be tiny in size since the development libraries are left behind, and the final container image has just the compiled binary files for your application. For more details on multi-stage build check out this link.

Below is the multi-stage build dockerfile builds our application in the first container, then the resulting binary is copied into an apline container. In the dockerfile, we’re basing the first stage FROM golang:latest AS builder. The name builder is a way for us to refer to this stage by name in subsequent stages. Then we copy in our source code with COPY ./webappv1/hello-app.go .. And then finally, build our application with RUN go env -w CGO_ENABLED=0 GO111MODULE=off && go build -o /app/hello-app. There are environment variables defined using CGO_ENABLED=0 GO111MODULE=off. More on those later in the post. But for now, know they’re needed to build and run the application successfully.

In the second stage, we’re basing our image off the tiny (7.46MB) alpine container image with FROM alpine:latest, then set the container’s working directory with WORKDIR /app. Next, we copy the binary built in the first stage into this container with COPY --from=builder /app/hello-app . We’ll give docker information about what port our application is listening on with EXPOSE 8080 and then finally, we define which process to start when the container is started with CMD ["./hello-app"].

FROM golang:latest AS builder
COPY ./webappv1/hello-app.go .
RUN  go env -w CGO_ENABLED=0 GO111MODULE=off && go build -o /app/hello-app 

FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/hello-app .
EXPOSE 8080
CMD ["./hello-app"]

Next, it’s time to build our container-based hello world application. We do that with the command docker build -t hello-app:1.0 . This will build what’s defined in the dockerfile and also give the image a name of hello-app and defines the tag as 1.0.

docker build -t hello-app:1.0 .

The resulting container image is relatively small. We can get the container image size with docker image ls, and in the output below, you can see our image is 13.8MB.

docker image ls 
REPOSITORY           TAG                  IMAGE ID       CREATED          SIZE
hello-app            1.0                  fa44005dab70   3 minutes ago    13.8MB

With the container built, let’s go ahead and run the container and expose the application on port 8080 using the following.

docker run --name webapp --publish 8080:8080 --detach hello-app:1.0

We can next check to make sure everything is up and running with docker ps, and if you see the output like this below, you are good to go.

docker ps 
CONTAINER ID   IMAGE             COMMAND                  CREATED         STATUS         PORTS                    NAMES
09a8d832d97c   hello-app:1.0     "./hello-app"            3 seconds ago   Up 2 seconds   0.0.0.0:8080->8080/tcp   webapp

With the container up, let’s test our hello world web application. When we hit port 8080 on localhost using curl, we see the output from the app printing some basic information. I do want to call out the hostname inside the container is the CONTAINER ID which you can see in the output of docker ps above.

curl http://localhost:8080

Hello, world!
Version: 1.0.0
Hostname: 09a8d832d97c

Now, earlier, I glossed over some environment variables set in the application build in our docker file for the line of code RUN go env -w CGO_ENABLED=0 GO111MODULE=off && go build -o /app/hello-app . Let’s dive into that a bit.

If you don’t include GO111MODULE=off, you’ll get the following error. So make sure this setting is included in your command to build your application.

#10 0.142 go: go.mod file not found in current directory or any parent directory; see 'go help modules'

Next, if you don’t set CGO_ENABLED=0 , your container will build, but when you run your container, it will start up and immediately fail, and you’ll get the error below. This is because the required libraries aren’t included in the binary.

docker ps -a
91f32370d6a4   hello-app:1.0 "./hello-app" 3 seconds ago   Exited (1) 3 seconds ago                             webapp

docker logs webapp
exec ./hello-app: no such file or directory

So with that, you just learned what I did this weekend. I needed to build a simple web application in go to test some things out in a Kubernetes environment.