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.