Setting up a Docker development enviroment for Go

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!

๐Ÿ’ก
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!
ย