# Setting up a Docker development enviroment for Go

Welcome back to our [Golang web application](https://blog.gkomninos.com/series/webapp-using-golang) 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/get-docker/)  
\- [https://docs.docker.com/compose/](https://docs.docker.com/compose/)  
\- [golang](https://go.dev/)

installed

### Basic setup

Create a folder that will host your project and init a git repository

```bash
mkdir freelance-invoice-hub
cd freelance-invoice-hub
git init
```

Let's create a new git branch

```bash
git checkout -b project-skeleton
```

Now we need to create a go module:

```bash
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

```bash
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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1718258441751/39a0194e-3520-4b0f-beb9-3771aeff1465.png align="center")

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 :)

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1718258541093/c219bd5c-3618-4f6c-8df4-474c294a0598.png align="center")

**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

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1718258601783/9934c1c6-812a-42db-b79a-16075060a824.png align="center")

**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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1718258697624/a88db52c-5804-469e-8636-c2b298066c10.png align="center")

**sqlite**: This folder will contain the implementation of our database layer. Since we are going to use [sqlite](https://sqlite.org/), we name that package sqlite. Similarly just create a file just declaring the package name.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1718258776676/6b479278-a984-4cb8-967f-6e0474950a98.png align="center")

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:

```bash
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](https://x.com/gkomdev) 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 🚀

```bash
touch dev.Dockerfile
```

and paste the following  

```dockerfile
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:

```yaml
# 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](https://github.com/air-verse/air/blob/master/air_example.toml) 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:  

```bash
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:

```yaml
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

```bash
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

```bash
[+] 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:

```bash
         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

```bash
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:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1718261142919/0b0a5ff2-4d41-4de8-b1d7-97d10786bb1b.png align="center")

And you can run your docker development container by using:  

```bash
make dev
```

\-&gt;

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1718261311806/1fbd2194-6d65-4f18-8198-b29d5b1c63bc.png align="center")

### Commit

It's time for us to commit and merge to the main branch  

```bash
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](https://github.com/gosom/freelance-invoice-hub/tree/project-skeleton) .

Stay tuned for the next part of the series, where we will dive deeper into implementing core functionalities. Happy coding!

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Please comment or ask on <a target="_blank" rel="noopener noreferrer nofollow" href="https://x.com/gkomdev" style="pointer-events: none">Twitter</a> if you have issues or need extra help. Happy to help</div>
</div>

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">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!</div>
</div>
