Backend As A Service with Go, Gin, Mysql & Docker
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 hasgolang
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 aCOPY
command for same purpose. Here, we are copying thego.sum
andgo.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
anddb
. - 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 on8000
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 mentionedmysql/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
- database credentials USER, PASS, HOST and DBNAME is fetched from environment variables
- Database url is constructed utilizing environment variables and saving it to
URL
variable - A new
mysql
connection is opened with thegorm.Open
method fromURL
. - 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!