Dockerize a Go App
Last edited on November 14, 2024What is docker?
To keep it simple, Docker is an open-platform used for developing, running, and deploying applications in a containerized environment. Well what is a container you might ask. You can think of a container as a micro operating system that can be spun up or down at whim. The container can host media, applications, utilities, or whatever else a typical operating system can run. Multiple containers can be run simultaneously, and via a network-bridge they can even communicate with each other and the host operating system as well! In theory docker is much more, but to avoid being esoteric this is the general idea of Docker and containers.
How can I use docker?
Docker can be used in many ways, but to keep it simple we'll focus on using it for creating application images that can be made portable to host virtually anywhere!
The docker CLI tool is the best way of getting started with becoming familiar with docker and it's abilities. All documentation can be found at docker's website here.
Again this article is less about getting familiar with docker and more about using it for a controlled deployment cycle, so I'll skip the deep explanation and focus primarily on the use case. I will assume if you're reading this you have docker installed and have used it at least a handful of times.
Creating a simple application
We'll start off by putting together a fairly normal application. In this case we'll use the std library net/http package for simplicity to create a small http server and a route that returns a health check ( status 200 "OK" ).
package main
import (
"fmt"
"net/http"
)
func HttpServer() {
// Handler function for the base path ( "/health" )
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {})
// Start the server listening on port 5000
fmt.Println("Server listening on http://localhost:5000/")
http.ListenAndServe(":5000", nil)
}
That's it! You can run the app to test it out go run main.go and visit localhost:5000/health. You won't see any content, but the status code will resolve to 200 which is all we care about at the moment.
Time to dockerize
Now we can actually build out some docker tooling. To do so you should have docker cli installed. Aside from the the first step is creating a docker file at the root of your project. touch Dockerfile.
The Dockerfile should look like this:
# Start with the golang base image
FROM golang:1.22.4-alpine3.20 as base
#ENV GO111MODULE=on
ENV config=production
# Set the current working directory inside the container
WORKDIR /build
# Copy go mod and sum files
COPY go.mod go.sum ./
# Install ca-certificates for X509 validation
RUN apk update && apk upgrade && apk add --no-cache ca-certificates
RUN update-ca-certificates
# Download all dependencies. Dependencies will be cached if the go.mod and the go.sum files are not changed
RUN go mod download && go mod verify
# # Copy the source from the current directory to the working Directory inside the container
COPY . .
WORKDIR /build
# Build the Go app
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main
# Start a new stage from scratch
FROM scratch
# Run the config as a different user than root for safety
WORKDIR /root/
ENV config=production
# Copy the Pre-built binary file from the previous stage. Observe we also copied the .env file
COPY /build/main .
COPY /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Expose port 5000 to the outside world
EXPOSE 5000
#Command to run the executable
CMD ["./main"]
What we've done here is created a two-stage docker build. In the first phase declred as base we gather everything we need to build the go app. The second stage copies the finished binary and the self-signed certs needed to run on the minimalist (scratch) operating system image. We then declare that the app will use port 5000 and set a command for running the executable. With this in place we can hit the last step of actually building an image using docker!
Build our docker image
I know i didn't clarify too well what an 'image' is. But just think of it as a snapshot of your apps build that we can save, commit, move, run, etc.. Now that we have everything we need let's put it all together. Using Docker CLI we can run a simple command to actually run the build.
docker build --tag my-go-app:latest .
Here we're specifying that we're building a docker image with docker build and tagging it. The general convention used is {BASE_NAME}:{VERSION} You should see a progress screen of the container being built and ultimately a success message.
You can view all of your images by running docker image ls which will output a list of images including your my-go-app image.
Let's tell docker to actually run our image:
docker run -d -p 5000:80 my-go-app:latest
This should get your app and running. In this command -d stands for detached mode which runs the container as a background process. The -p is the port command and in our case we are running our app on port 5000 locally, but want to expose it externally on port 80.
You can check the status of your running app container using docker ps and by visting http://localhost/health which should resolve a status of 200!
Summary
Today we've put together quite a bit of development magic. We created a simple Go app, created a Docker image and ran it as an independent container. This is quite the feat! There's A LOT more that can be done with docker such as:
- Connecting multiple containers over a network bridge
- Orchestrating docker containers using Kubernetes
- Building a CI/CD flow to build the images, tag them and run them automatically
Let me know if this article helped you out, or if there are any ways I could improve. Thanks!