Docker for Go: Setup for Development, Testing, and Production

Andrii Pohuliailo
5 min read

Container terminal with colorful containers and yellow cranes symbolizing the concept of Docker

Today Docker has become an integral part of any project in production - it's convenient, simple, and reliable. However, many teams still avoid using Docker during development, depriving themselves of many advantages. And when they do use Docker, they often do it inefficiently, specifically: they place infrastructure files (Dockerfile, configurations) directly among the main application code, which complicates navigation and work with the repository.

I have worked in various companies, from global corporations to small local firms, and during this time I have seen numerous projects. Most of them repeat the same mistake: infrastructure files are stored together with the main code, which complicates navigation and makes it difficult for developers to find the necessary files.

So, I developed an optimal Docker setup that works effectively for development, testing, and production. It covers both the project structure and container configuration.

In this article, I will examine the main issues of setting up Docker for Go projects that I have encountered in projects and online tutorials.

Problems

1. Lack of Docker in the project

Often there are situations where developers don't understand the need to use Docker locally and write configurations, since the project can simply be run with the go run main.go command. However, later the project will need additional services: a database, caching, or a service for sending emails. That's when deploying the project and making changes, such as updating the database version, becomes a much more complex task.

Additionally, new developers will need considerable time for the first project deployment, especially if problems arise.

The difference between environments is one of the main development challenges. It can occur not only between local and production environments, but also between different developers' computers. This often leads to the classic phrase: "I don't understand why it's not working in prod - it worked fine on my machine." So if you're still not using Docker for development, read this article to the end and implement it.

2. Infrastructure Mixed with Core Code

I often encounter this problem where the main files and directories of the project are mixed with the application code, for example: the Dockerfile is located in the project root, and database settings are stored in the config directory. This makes it difficult to understand the purpose of directories, whether they are only responsible for infrastructure configuration or also for the application settings. The structure appears chaotic and confusing for everyone. Even when the project has a separate docker directory, it often gets lost among other folders, making this approach inconvenient and unintuitive.

3. One Configuration for Multiple Environments

This is a serious problem that I often encounter. At first glance, using a single configuration for local development and production seems like the right solution, since the infrastructure is the same in both environments. However, as the project evolves, complications arise due to the need for different tools.

For example, during development, you need MailHog for testing emails or automatic builds after code changes. Using a single configuration not only makes it difficult to implement such tools but also increases the risk of problems in production.

Project Structure I Use

I am not a fan of mixing the main application code with code responsible for infrastructure. That's why I created this directory structure for my projects:

The main directory structure is as follows:

project-directory
app
docker
var
.gitignore
Makefile

This separation allows for convenient work with the code. Let's look in detail at what each directory is responsible for:

  • app — contains the main application code.
  • docker — contains all files responsible for infrastructure: Dockerfile, Nginx configurations, etc.
  • var — stores temporary infrastructure data, such as database files.
  • .gitignore — defines files and directories that should not be included in the repository.
  • Makefile — simplifies working with Docker and other tools during development.

The structure of the docker directory looks as follows:

docker
development
production

I always separate configurations for development and production environments. This adds convenience and clarity when working with configurations.

  • development — contains configurations for the development environment.
  • production — contains configurations for production.

The directory structure for each environment will look identical, so let's examine the structure of the development/ directory:

development
app
config
Dockerfile
nginx
docker-compose.yml
  • docker-compose.yml — main file for running Docker services in the development environment. This is convenient for small projects where Docker Compose is used both in development and in production.
  • app, nginx — contain Dockerfile and configuration files for the application service, with a separate directory allocated for each service.

Regarding the structure for service directories, it will be similar for all services:

  • Dockerfile — defines how to build the container for the application.
  • config — contains all necessary configuration files for this service.

Complete Project Structure

After setup, the project structure looks approximately like this:

project-directory
app
...
docker
development
app
Dockerfile
docker-compose.yml
production
app
Dockerfile
docker-compose.yml
var
.gitignore
Makefile

Configuration and What's Important to Me

Let's start with what's most important to me while working on a project. After switching from PHP to Go, one of the biggest inconveniences was the need to compile code after each change. Additionally, I often work in conditions where I need to quickly switch between different projects, which is why it's critically important that the project deploys as quickly as possible.

I developed a simple and effective development setup that automatically compiles code after changes without any unnecessary complications.

# Set the base image to Golang based on Alpine Linux
FROM golang:1.23.2-alpine  
 
# Install necessary utilities: git, curl, bash
RUN apk add --no-cache git curl bash  
 
# Download and install Air utility for automatic restarts
RUN curl -fLo install.sh https://raw.githubusercontent.com/cosmtrek/air/master/install.sh \  
    && chmod +x install.sh \  
    && sh install.sh \  
    && cp ./bin/air /bin/air  
 
# Set the working directory of the container
WORKDIR /app  
 
# Copy go.mod and go.sum files containing dependency information
COPY ./app/go.mod ./app/go.sum ./  
 
# Download all dependencies specified in go.mod
RUN go mod download  
 
# Copy the entire project code to the working directory
COPY ./app ./  
 
# Expose port 8080 for application access
EXPOSE 8080  
 
# Specify the command to start the container, using Air to watch for changes
CMD ["air", "-d", "--build.cmd", "go build -o ./tmp/main ./cmd/api", "--build.bin", "./tmp/main"]

And here is the Docker Compose file directly:

services:  
  app:  
    build:  
      context: ./../../  
      dockerfile: docker/development/app/Dockerfile  
    volumes:  
      - ./../../app:/app  
      - /app/tmp  
    environment:  
      GO_ENV: development  
    ports:  
      - '8080:8080'

Here is a simple Makefile that simplifies working with the most commonly used commands during project development:

build:
	docker compose -f ./docker/development/docker-compose.yml build 
 
start:  
	docker compose -f ./docker/development/docker-compose.yml up  
  
down:
	docker compose -f ./docker/development/docker-compose.yml down
Note
In this article, I don't cover production environment settings since they are specific to each project. However, the configurations will be available in the GitHub repository.

Conclusions

Today Docker is a powerful tool that significantly simplifies working with project infrastructure. Equally important is the proper organization of the project structure — this helps to use development time more efficiently and reduce the number of errors. I always try to make project configuration as convenient as possible and hope you'll find useful ideas here.

More details and code examples are available in my GitHub repository: pogulailo/seagopher

Docker
Go

You must be logged in to add a comment. Login

No comments yet. Be the first to comment!