How to build, containerize and autoreload your Go Server
I explained the structure of reread's Go API server in the previous post. Now that I have a web server that's able to respond to the /_health
endpoint, I'll focus on being able to build it, test it, containerize it, and have it auto reload.
Go has some excellent tooling out of the box, so building a go project is simply executing the following command:
go build
By default, Go build will take the name of the directory the main.go file is in, which in my case is server. To name the built executable, I can give the build command an output argument:
go build -o build/reread main.go
This will create a binary with the name reread
, and place it within the build
folder. Now that there's a binary, lets start by containerizing the application.
While not always necessary, containerization is useful because it allows having a reproducible environment for an application. The most popular containerization platform is Docker, and I'll use that for reread.
Using Docker is simple, I need to create a configuration file, called a Dockerfile, that will contain the following pieces of information:
- The base Docker image to build off of
- Steps to modify the base Docker image
- Let docker know which command to execute when the container starts
My Dockerfile looks like so:
FROM golang:1.17-alpine
WORKDIR /server
COPY . ./
RUN go build -o build/reread main.go
EXPOSE 1323
CMD [ "build/reread" ]
It is doing the following things:
- Use
golang
Docker image, with the version (called atag
) of1.17-alpine
. The tag can be anything, but in this case, I've chosen one with Go version 1.17, using Alpine Linux which is a light weight linux distribution ideal for docker containers. - Set the working directory for the following commands to
/server
. When you ssh into the docker container, you'll find the library (and any associated configuration files) are present within the/server
directory - Copy over the entire contents of the
server
directory from the repo over to the docker image - Build the go binary inside the container
- Let docker know to expose port 1323 externally
- Let docker know to start
build/reread
on container start
Now that I have a Dockerfile, I can reference this within my Docker Compose configuration. I find Docker Compose a really useful utility for local development because it allows me to orchestrate parts of my app together very easily.
For running just my server, all I need is this docker-compose.yml
file:
version: '3.6'
services:
server:
build: './server'
expose:
- '8080:1323'
All I'm saying here is: I have a service called server, that you can find a Dockerfile for in ./server
and expose the port 1323 of the docker container as port 8080 on my local machine. With this set up, I can just run docker-compose up
and it will build my Go server, and make it available on http://localhost:8080
.
While is really nice, one annoying thing was that I had to stop docker-compose and restart it whenever my code changed. This gets old pretty fast, so I needed a better solution. Luckily, the brilliant open source engineers behind comstrek/air solved this problem for me.
Air is a really useful tool that can be used with Docker to allow automatic reloading. Setting it up is also trivial:
- Install the Air binary
- Auto configure Air for your project by running
air init
- Update docker compose like so:
version: '3.6'
services:
server:
image: cosmtrek/air
working_dir: /server
ports:
- '8080:1323'
volumes:
- ./server/:/server/
Now this looks more complex, but here's what I've changed:
- Instead of telling docker-compose to build our project, I'm providing an image
comstrek/air
- I'm telling docker-compose to use the working directory
/server
for the source. - I'm 'mounting' the
./server
directory from my machine to the/server
directory in the container as a volume. This basically acts as a shared folder. Any changes that I make to the code will also show up within the docker container. When that happens, Air will automatically build and reload the server.
Now, once I run docker-compose up
, I don't have to fiddle with it for the most part.
Testing
Now that running the application can be run within a container, I'd like to focus back on testing a bit. In the last article, I discussed writing tests, but never talked about how to run them. Again, Go has some really great tooling, and all you have to do is run:
go test
However, this is a bit incomplete. Go test by default only looks at the current directory. Meanwhile, I like to interleave my tests within the source code by prefixing the file name with _test
. I would like Go to run all the tests. For this, Go supplies an easy fix, described in their documentation:
go test ./...
I also add some useful flags to my test file, and it looks like so:
go test -race -cover ./...
These flags tell go to warn me about race conditions and give me a test coverage percentage.
I add all these commands to my Makefile, making them easy to use. It looks like this now:
build:
go build -o reread main.go
test:
go test -race -cover ./...
lint:
golangci-lint run
run:
go run main.go
build-docker:
docker build --tag reread-server .
The one thing I haven't discussed earlier is the lint
command. A linter is a tool to identify if you're using bad practices or certain kinds of errors in your source. While I have the linter command, VS Code also has that baked in with their Go Extension, so I don't use it often yet. It will become more useful when I start building a CICD process for reread.
I'm pretty happy with the state of the go server set up now. Next, I'll start with the Next.js part of the application.