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:
root = "."
tmp_dir = "tmp"
[build]
pre_cmd = ["go mod download"]
cmd = "go build -o ./tmp/freelance-invoice-hub cmd/main.go"
bin = "tmp/freelance-invoice-hub"
full_bin = "export $(grep -v '^#' dev.env | xargs);tmp/freelance-invoice-hub"
include_ext = ["go", "tpl", "tmpl", "html", "js", "css", "scss", "toml"]
exclude_dir = ["bin", "tmp", "vendor", "certs", "static", "uploads"]
include_dir = []
include_file = []
exclude_file = []
exclude_regex = ["_test\\.go", "mock_.*\\.go", "gomock_.*\\.go"]
exclude_unchanged = true
follow_symlink = true
log = "air.log"
poll = false
poll_interval = 500
delay = 1000
stop_on_error = true
send_interrupt = true
kill_delay = 500
rerun = false
rerun_delay = 500
[log]
time = false
main_only = false
[color]
main = "magenta"
watcher = "cyan"
build = "yellow"
runner = "green"
[misc]
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:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
dev:
@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!
💡
Please comment or ask on
Twitter if you have issues or need extra help. Happy to help
💡
If you found this guide helpful, please consider subscribing to our newsletter for more tutorials and updates. And don't forget to share this article with others who might benefit from it!