Setting up Docker and MySQL for Go development

Setting up docker with go for local development

Published on

11 min read

Backend As A Service with Go, Gin, Mysql & Docker

Chapters
Feature Image

In previous Chapter-1 we configured Hello world server in normal way. In this chapter we will setup the project with Docker for local development.

Learning Objectives

  • Dockerizing Go application
  • Environment variables
  • Setting up MySQL database normal way and Docker way
  • Usage of Docker compose to setup docker

Why Docker?

To put it simply, Docker is a software platform that makes it easier to create, execute, manage, and distribute applications. It accomplishes this by running the computer's operating system as a virtual machine. Here is an awesome article about docker, please do have a look once.

Fun fact : Docker is developed using the Go language

Setting up Dockerfile

Dockerfile is an entry/base file from which Docker containers are built. Let's get started.

Create a file Dockerfile in the project root with the following code.

FROM golang:1.19.1-alpine

RUN mkdir /app

WORKDIR /app

ADD go.mod .
ADD go.sum .

RUN go mod download
ADD . .

EXPOSE 8000

CMD ["go", "run", "."]

Let’s Review the Dockerfile :

  • FROM : FROM refers which base image to use for the container. The golang:1.19.1-alpine image is a Linux-based image which has golang installed and no other additional programs or software.
  • WORKDIR : WORKDIR Changes the working directory. In our case to /app. It sets a working directory for subsequent commands.
  • ADD : ADD instruction literally copies the file from one location to another. ADD [SOURCE] [DESTINATION] is the syntax. There is also a COPY command for same purpose. Here, we are copying the go.sum and go.mod file first so that we will have all the libraries installed before copying all the files.
  • RUN : RUN instruction executes any commands in a new layer on top of the current image and commit the results. The resulting committed image is used for the next step in the Dockerfile.
  • EXPOSE : EXPOSE instructs that services running on Docker container is available in port 8000.
  • CMD : CMD runs the command inside the container once a container is created from an image. Here, The CMD command runs our project.

Configuring Docker Compose

As of now we have only created a base Dockerfile. Moving forward we will configure and make use of the Dockerfile to run our application.

why docker compose?

We need docker compose for defining and running multi-container Docker applications. In our case Database and our application are two different services.

Let's start by created a docker-compose.yml file at the same level of Dockerfile with the following content.

version: '3'
services:
  web:
    build: .
    ports:
      - '8000:8000'
    volumes:
      - '.:/app'

Let’s review terms mentioned inside the compose file.

  • version ‘3’: Docker compose version.
  • services: The services section defines all the different containers that are to be created. We have two services namely, web and db.
  • web: This is the name of our Gin app service. It can be anything. Docker Compose will create containers with this name.
  • build: This clause specifies the Dockerfile location. . represents the current directory where the Dockerfile is located and is used to build an image and run the container from it.
  • ports: The ports clause is used to map or expose the container ports to the host machine. Here we are mapping port "8000:8000" , so that we can access our application on 8000 port of host machine.
  • volumes: Here, we are attaching our code files directory to the containers, ./app directory so that we don’t have to rebuild the images for every change in the files. This will also help in auto-reloading the server when running in debug mode. We will setup auto-reloading on change later in upcoming articles.

Use the below command to build and spin up the server.

docker-compose up --build

You should see the similar log as below.

web_1  | [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
web_1  |
web_1  | [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
web_1  |  - using env:  export GIN_MODE=release
web_1  |  - using code: gin.SetMode(gin.ReleaseMode)
web_1  |
web_1  | [GIN-debug] GET    /                         --> main.main.func1 (3 handlers)
web_1  | [GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
web_1  | Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
web_1  | [GIN-debug] Listening and serving HTTP on :8000
web_1  | [GIN] 2022/09/10 - 10:40:35 | 200 |     346.792µs |      172.18.0.1 | GET      "/"

Go to Localhost to see the output hello world.

Setting up MySQL

Let's configure the docker-compose.yml file to setup multiple services. Let's modify the file to include db service.

version: '3'
services:
  db:
    image: mysql/mysql-server:5.7
    ports:
      - '3305:3306'
    env_file: .env
    environment:
      - 'MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}'
      - 'MYSQL_USER=${DB_USER}'
      - 'MYSQL_PASSWORD=${DB_PASSWORD}'
      - 'MYSQL_DATABASE=${DB_NAME}'
  web:
    build: .
    ports:
      - '8000:8000'
    volumes:
      - '.:/app'
    depends_on:
      - db
    links:
      - 'db:database'
    env_file: .env

Let's review the above modifications:

  • links: Links literally links one service to another. Here, we are linking the database container to the web container, so that our web container can reach the database in the bridge network. (Networking alert !!). Please if you want to learn about network in docker in detail do refer to : Network containers
  • image: If we don’t have a Dockerfile and want to run a service directly using an already built image, we can specify the image location using the image clause. Compose will pull the image from docker hub and fork a container from it. In our case We mentioned mysql/mysql-server:5.7 to our database service
  • environment: Any environment variable that needs to be set up in the container can be created using the ‘environment’ clause.
  • env_file: This tells the container to use .env to export environment variables

Environment variables

There are two type of variables in the project program variables and environment variables. Program variables are normal variables that stores values within the code block or module, where as environment variables is available globally through the project.

Environment variables can be set up using various ways. We are using .env file to set them. Create a .env file on the project root and add below variables.

MYSQL_ROOT_PASSWORD=root
DB_USER=user
DB_PASSWORD=Password@123
DB_HOST=db
DB_PORT=3306
DB_NAME=golang
SERVER_PORT=8000
ENVIRONMENT=local

Now that everything is configured. Let's spin up the server once again

docker-compose up --build

If you see following log on the terminal that means database is up.

db_1   | 2022-09-10T11:01:31.052348Z 0 [Note] Plugin 'FEDERATED' is disabled.
db_1   | 2022-09-10T11:01:31.097439Z 0 [Note] InnoDB: Buffer pool(s) load completed at 220910 11:01:31
db_1   | 2022-09-10T11:01:31.101893Z 0 [Note] Found ca.pem, server-cert.pem and server-key.pem in data directory. Trying to enable SSL support using them.
db_1   | 2022-09-10T11:01:31.101959Z 0 [Note] Skipping generation of SSL certificates as certificate files are present in data directory.
db_1   | 2022-09-10T11:01:31.102190Z 0 [Warning] A deprecated TLS version TLSv1 is enabled. Please use TLSv1.2 or higher.
db_1   | 2022-09-10T11:01:31.102226Z 0 [Warning] A deprecated TLS version TLSv1.1 is enabled. Please use TLSv1.2 or higher.
db_1   | 2022-09-10T11:01:31.115362Z 0 [Warning] CA certificate ca.pem is self signed.
db_1   | 2022-09-10T11:01:31.116305Z 0 [Note] Skipping generation of RSA key pair as key files are present in data directory.
db_1   | 2022-09-10T11:01:31.119295Z 0 [Note] Server hostname (bind-address): '*'; port: 3306
db_1   | 2022-09-10T11:01:31.120839Z 0 [Note] IPv6 is available.
db_1   | 2022-09-10T11:01:31.121275Z 0 [Note]   - '::' resolves to '::';
db_1   | 2022-09-10T11:01:31.121572Z 0 [Note] Server socket created on IP: '::'.
db_1   | 2022-09-10T11:01:31.171094Z 0 [Note] Event Scheduler: Loaded 0 events
db_1   | 2022-09-10T11:01:31.172470Z 0 [Note] /usr/sbin/mysqld: ready for connections.
db_1   | Version: '5.7.39'  socket: '/var/lib/mysql/mysql.sock'  port: 3306  MySQL Community Server (GPL)

Connect MySQL with our application

Now that we have configure MySQL database. We need to connect this database with out application. Let's create a folder infrastructure and file db.go inside it. Paste following come inside db.go.

package infrastructure

import (
    "fmt"
    "os"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

//Database struct
type Database struct {
    DB *gorm.DB
}

//NewDatabase : intializes and returns mysql db
func NewDatabase() Database {
    USER := os.Getenv("DB_USER")
    PASS := os.Getenv("DB_PASSWORD")
    HOST := os.Getenv("DB_HOST")
    DBNAME := os.Getenv("DB_NAME")

    URL := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local", USER, PASS, HOST, DBNAME)
    db, err := gorm.Open(mysql.Open(URL))

    if err != nil {
        panic("Failed to connect to database!")

    }
    fmt.Println("Database connection established")
    return Database{
        DB: db,
    }
}

Inside NewDatabase function the following work is being done

  1. database credentials USER, PASS, HOST and DBNAME is fetched from environment variables
  2. Database url is constructed utilizing environment variables and saving it to URL variable
  3. A new mysql connection is opened with the gorm.Open method from URL.
  4. Lastly, Database struct is returned with gorm database instance as parameter, which is used later on the application to access the database.

Install MySQL and Gorm. They are required by the our application to connect the database as referred/imported in infrastructure/db.go.

go get gorm.io/driver/mysql gorm.io/gorm

Finally we need to modify our main.go to setup the database connection.

package main

import (
  "net/http"
  "github.com/gin-gonic/gin"
  "blog/infrastructure"
)

func main() {
  router := gin.Default() //new gin router initialization
  infrastructure.NewDatabase()
  router.GET("/", func(context *gin.Context) {
    context.JSON(http.StatusOK, gin.H{"data": "Hello World !"})
  }) // first endpoint returns Hello World
  router.Run(":8000") //running application, Default port is 8080
}

Spin up the server to verify it is running correctly.

docker-compose up --build

You should see a log print on terminal.

web_1  | Database connection established
web_1  | [GIN-debug] GET    /                         --> main.main.func1 (3 handlers)
web_1  | [GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
web_1  | Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
web_1  | [GIN-debug] Listening and serving HTTP on :8000

Woohooo! Congratulations!

At this point the project structure should look like this :

├── Dockerfile
├── docker-compose.yml
├── go.mod
├── go.sum
├── infrastructure
│   └── db.go
├── main.go
└── .env

You guys are awesome! All code for this chapter is available here.

That's a Wrap

That's it for Chapter-2 of the series. In the next Chapter we will be working on:

  • Adding models (structs) to the application
  • Adding CRUD apis

Make sure to sign up the newsletter so that you never miss my upcoming articles.
Every comment/feedback/suggestions adds a great value and motivates me to keep up the good work!

Thank You!
Happy learning!