Setting up a Docker development enviroment for Go
Crafting a real Web Application with Golang
Welcome back to our Golang web application series! If you're excited to learn web development with Golang, you're in the right place. In this guide, we'll set up a solid development environment. By the end, you'll have a well-organized project and be able to run your code locally with Docker Compose, complete with hot reloading for easy development. Let's get started!
Prerequisites
Ensure that you have:
- docker
- https://docs.docker.com/compose/
- golang
installed
Basic setup
Create a folder that will host your project and init a git repository
mkdir freelance-invoice-hub
cd freelance-invoice-hub
git init
Let's create a new git branch
git checkout -b project-skeleton
Now we need to create a go module:
go mod init invoicehub
Also let's create a .gitignore file and a README and LICENCE and Makefile .
Let's keep the empty for now
touch README.md
touch .gitignore
touch LICENCE
touch Makefile
Golang Project structure
We need to organize our project skeleton in a way that we can easily extend and test our code.
Below you can see a good starting point for your go projects.
Let's explain the project layout and what we will put in each folder
cmd: Here we will have our executable, the main function.
Right now it just prints hello world :)
http: In this package we are going to add all the code that is related with the HTTP server, the handlers etc. For now just create an empty file http.go
mocks: This folder will hold the mock implementation of our services. The mocks will be auto-generated. More on that later. for now just add a mocks.go which only declares that this is a go package.
sqlite: This folder will contain the implementation of our database layer. Since we are going to use sqlite, we name that package sqlite. Similarly just create a file just declaring the package name.
In the root folder we are going to create .go files that represent our domain entities and their operations. For now let's do nothing
Let's test that our main runs now:
go run cmd/main.go
This must print hello world .
If not please check the error message and your go installation.
In case you run into problems please reach to me either on Twitter or in a comment and I will assist.
Setting up Dockerfile and docker-compose
We want to be able to run our application in a docker container. Additionally, the docker container should reload when the code changes (hot reload).
Let's do that ๐
touch dev.Dockerfile
and paste the following
FROM golang:1.22.1-bullseye
RUN apt-get update \
&& apt-get install -y ca-certificates curl gnupg \
&& mkdir -p /etc/apt/keyrings
WORKDIR /app
RUN go install go.uber.org/mock/mockgen@latest && \
go install github.com/air-verse/air@latest
RUN git config --global --add safe.directory /app
CMD ["air", "-c", ".air.toml"]
Notes:
We install in the docker container 2 extra go packages:
mockgen: It will be used to generate mock implementation automatically from our interfaces.
air: This is a program that monitors your files for changes and restart the process. This is the tool that we are going to use for "hot" reloading the code.
Air requires a .air.toml
file with it's configuration.
Create that file using touch .air.toml
and paste the following:
# Config file for [Air](https://github.com/cosmtrek/air) in TOML format
# Working directory
# . or absolute path, please note that the directories following must be under root.
root = "."
tmp_dir = "tmp"
[build]
# Array of commands to run before each build
pre_cmd = ["go mod download"]
# Just plain old shell command. You could use `make` as well.
cmd = "go build -o ./tmp/freelance-invoice-hub cmd/main.go"
# Array of commands to run after ^C
#post_cmd = ["echo 'hello air' > post_cmd.txt"]
# Binary file yields from `cmd`.
bin = "tmp/freelance-invoice-hub"
# Customize binary, can setup environment variables when run your app.
#full_bin = "./tmp/main"
full_bin = "export $(grep -v '^#' dev.env | xargs);tmp/freelance-invoice-hub"
# Watch these filename extensions.
include_ext = ["go", "tpl", "tmpl", "html", "js", "css", "scss", "toml"]
# Ignore these filename extensions or directories.
exclude_dir = ["bin", "tmp", "vendor", "certs", "static", "uploads"]
# Watch these directories if you specified.
include_dir = []
# Watch these files.
include_file = []
# Exclude files.
exclude_file = []
# Exclude specific regular expressions.
exclude_regex = ["_test\\.go", "mock_.*\\.go", "gomock_.*\\.go"]
# Exclude unchanged files.
exclude_unchanged = true
# Follow symlink for directories
follow_symlink = true
# This log file places in your tmp_dir.
log = "air.log"
# Poll files for changes instead of using fsnotify.
poll = false
# Poll interval (defaults to the minimum interval of 500ms).
poll_interval = 500 # ms
# It's not necessary to trigger build each time file changes if it's too frequent.
delay = 1000 # ms
# Stop running old binary when build errors occur.
stop_on_error = true
# Send Interrupt signal before killing process (windows does not support this feature)
send_interrupt = true
# Delay after sending Interrupt signal
kill_delay = 500 # nanosecond
# Rerun binary or not
rerun = false
# Delay after each executions
rerun_delay = 500
# Add additional arguments when running binary (bin/full_bin). Will run './tmp/main hello world'.
#args_bin = ["hello", "world"]
[log]
# Show log time
time = false
# Only show main log (silences watcher, build, runner)
main_only = false
[color]
# Customize each part's color. If no color found, use the raw app log.
main = "magenta"
watcher = "cyan"
build = "yellow"
runner = "green"
[misc]
# Delete tmp directory on exit
clean_on_exit = true
[screen]
clear_on_rebuild = true
keep_scroll = true
Basically, you get that file and you modify it. We are not going to explain more here.
If you want to understand what it's changed from the original focus on the [Build] section. I just changed the paths more or less to match our project.
Finally add the tmp folder in your .gitignore file:
echo "tmp" >> .gitignore
Docker-compose
to run our project we are going to use docker compose.
create a file touch dev.docker-compose.yaml
and paste the following:
services:
app:
build:
context: .
dockerfile: dev.Dockerfile
env_file:
- 'dev.env'
ports:
- '127.0.0.1:443:443'
- '127.0.0.1:80:8080'
extra_hosts:
- "local.freelance-invoice-hub.com:127.0.0.1"
volumes:
- .:/app
- freelance-invoice-hub_mod_cache:/go/pkg/mod
volumes:
freelance-invoice-hub_mod_cache:
Finally create a file touch dev.env
. We are going to add environment variables for development there later.
Let's test the setup
docker compose -f dev.docker-compose.yaml up
then it should start building the image and start the container.
you should see something like the below
[+] Running 1/0
โ Container freelance-invoice-hub-app-1 Created 0.0s
Attaching to app-1
app-1 |
app-1 | __ _ ___
app-1 | / /\ | | | |_)
app-1 | /_/--\ |_| |_| \_ v1.52.2, built with Go go1.22.1
app-1 |
app-1 | mkdir /app/tmp
app-1 | watching .
app-1 | watching cmd
app-1 | watching http
app-1 | watching mocks
app-1 | watching sqlite
app-1 | !exclude tmp
app-1 | > go mod download
app-1 | go: no module dependencies to download
app-1 | building...
app-1 | running...
app-1 | export GOLANG_VERSION='1.22.1'
app-1 | export GOPATH='/go'
app-1 | export GOTOOLCHAIN='local'
app-1 | export HOME='/root'
app-1 | export HOSTNAME='5a8638cf6ceb'
app-1 | export PATH='/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
app-1 | export PWD='/app'
app-1 | hello world
app-1 | Process Exit with Code 0
Notice the hello world
in the end. This is what our program does.
Now let's test if it will reload when we do a change.
In cmd/main.go
change the hello world
string to hello hot reload
.
Now the console that started the container automatically run the modified code:
cmd/main.go has changed
app-1 | > go mod download
app-1 | go: no module dependencies to download
app-1 | building...
app-1 | running...
app-1 | export GOLANG_VERSION='1.22.1'
app-1 | export GOPATH='/go'
app-1 | export GOTOOLCHAIN='local'
app-1 | export HOME='/root'
app-1 | export HOSTNAME='5a8638cf6ceb'
app-1 | export PATH='/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
app-1 | export PWD='/app'
app-1 | hello hot reload
Makefile
Since it's tedious to write the docker compose -f dev.docker-compose.yaml up
all the time
let's add a command for that in our Makefile
default: help
help: ## help information about make commands
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
dev: ## runs the application in development mode
@docker compose -f dev.docker-compose.yaml up
Now run
make
and you should see:
And you can run your docker development container by using:
make dev
->
Commit
It's time for us to commit and merge to the main branch
git add .
git commit -m "Initial project setup"
git checkout main
git merge project-skeleton
Summary
In this blog post, we have successfully set up the basic skeleton for our Golang-based web application. We created a structured project layout, set up essential files, and configured Docker and Docker Compose for local development with hot reloading. With this foundation in place, we are now ready to start building and extending our application.
The code is accessible in the project-skeleton
brach in github .
Stay tuned for the next part of the series, where we will dive deeper into implementing core functionalities. Happy coding!