How To Dockerize Your API With Go + PostgreSQL + Gin + Docker
Hello mates, still with me Ramadhan, In this article, i will be share you how to dockerize your API with Go as base language, postgreSQL as database, Gin as web framework, and the core of the materials, Docker as container management. Okay, without further ado, let’s break the code.
Docker
Docker is a containerization platform that allows developers to build, package, and deploy applications in a lightweight, portable, and consistent manner across different environments. It uses a client-server architecture where the Docker client communicates with the Docker daemon to perform operations such as building, running, and distributing Docker images.
Docker images are created using Dockerfiles, which contain instructions for building an application and its dependencies. By using Docker, developers can avoid the “works on my machine” problem and ensure that their applications run consistently across different environments, from development to production.
Install Modules
Firstly, we have to install all of the modules are support the API working. Copy these command to install all of modules.
go get -u github.com/gin-gonic/gin
go get github.com/golang-migrate/migrate/v4
go get github.com/lib/pq
Url sources :
Go gin : https://github.com/gin-gonic/gin
Go migrate : https://github.com/golang-migrate/migrate
postgreSQL driver : https://github.com/lib/pq
Create Database Configuration
Next, we need to set the database configuration includes host, username, password, and database name. Create a folder named app, then create a file named config.go. Copy the code below :
package app
var (
UNAMEDB string = "postgres"
PASSDB string = "postgres123"
HOSTDB string = "postgres"
DBNAME string = "mangastore"
)
Create Database
Then, we need to create database using shell command. Then execute it on postgreSQL docker container by copy it to the database initialization entrypoint postgreSQL. Create a folder named dbshell, then create a file named db.sh. You can copy the code below :
#!/bin/bash
set -e
export PGPASSWORD=postgres123;
psql -v ON_ERROR_STOP=1 --username "postgres" --dbname "mangastore" <<-EOSQL
CREATE DATABASE mangastore;
GRANT ALL PRIVILEGES ON DATABASE mangastore TO "postgres";
EOSQL
That code will be create mangastore database.
Create Table Migrations
We need to create a table migration. The table will be called manga. We just need to write a create table query on sql file. Those files will be execute by go-migrate module. Create a folder named migrations, then create a file named 1_create_table_manga.up.sql located there, then copy the code below.
CREATE TABLE IF NOT EXISTS manga(
id BIGSERIAL NOT NULL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
genre VARCHAR(255),
volumes INTEGER,
chapters INTEGER,
author VARCHAR(255)
);
And a file named 1_create_table_manga.down.sql, then copy the code below.
DROP TABLE IF EXISTS manga;
Create Database Connection and Routes
Next step, we need to create a file named app.go in app folder. This file is used to write a create connection function and create routes function. Copy the code below.
package app
import (
"database/sql"
"fmt"
"log"
"github.com/gin-gonic/gin"
"github.com/ramadhanalfarisi/go-simple-dockerizing/controller"
)
type App struct {
DB *sql.DB
Routes *gin.Engine
}
func (a *App) CreateConnection(){
connStr := fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable", UNAMEDB, PASSDB, HOSTDB, DBNAME)
db, err := sql.Open("postgres", connStr)
if err != nil {
log.Fatal(err)
}
a.DB = db
}
func (a *App) CreateRoutes() {
routes := gin.Default()
controller := controller.NewMangaController(a.DB)
routes.GET("/manga", controller.GetManga)
routes.POST("/manga", controller.InsertManga)
a.Routes = routes
}
func (a *App) Run(){
a.Routes.Run(":8080")
}
App struct contains database connection and routes. All of the functions will call on main function. CreateConnection function will call to create a database connection. CreateRoutes function will call to create routes of the API. and Run function to run the API services.
Create Migration Function
To execute all of SQL queries in the migrations folder, we need to create a file named migrate.go in app folder to write migration function to execute all of those. This function will use go-migrate to execute those SQL queries. Copy the code below.
package app
import (
"log"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
_ "github.com/lib/pq"
)
func (a *App) Migrate() {
driver, err := postgres.WithInstance(a.DB, &postgres.Config{})
if err != nil {
log.Println(err)
}
m, err := migrate.NewWithDatabaseInstance(
"file://./migrations/",
"mangastore", driver)
if err != nil {
log.Println(err)
}
if err := m.Steps(2); err != nil {
log.Println(err)
}
}
This function need the database connection to connect to postgreSQL. It also need to know folder location of SQL queries and database name. This function will call on main function to migrate all of database tables.
Create Models
We need to create a model to form API responses and requests. Create a folder model, then create a file there named manga.go. Copy the code below.
package model
type Manga struct {
Id uint `json:"id"`
Title string `json:"title"`
Genre string `json:"genre"`
Volumes uint8 `json:"volumes"`
Chapters uint16 `json:"chapters"`
Author string `json:"author"`
}
type PostManga struct {
Title string `json:"title" binding:"required"`
Genre string `json:"genre"`
Volumes uint8 `json:"volumes"`
Chapters uint16 `json:"chapters"`
Author string `json:"author"`
}
Manga struct is used to form API responses, while PostManga struct is used to form API requests. binding syntax is used to set validation rules. required means the Title parameter had to fill.
Create Repositories
Repository is the package is used to manage database command. Usually, the commands are intended for INSERT, SELECT, UPDATE, and DELETE will execute in this package. Create a folder named repository, then create files named manga_interface.go and manga.go there.
manga_interface.go is the repository interface for design the repository architecture. Copy the code below.
package repository
import "github.com/ramadhanalfarisi/go-simple-dockerizing/model"
type MangaRepositoryInterface interface {
SelectManga() []model.Manga
InsertManga(post model.PostManga) bool
}
manga.go is the repository for execute all commands to database include INSERT and SELECT. Copy the code below.
package repository
import (
"database/sql"
"log"
"github.com/ramadhanalfarisi/go-simple-dockerizing/model"
)
type MangaRepository struct {
DB *sql.DB
}
func NewMangaRepository(db *sql.DB) MangaRepositoryInterface {
return &MangaRepository{DB: db}
}
// InsertManga implements MangaRepositoryInterface
func (m *MangaRepository) InsertManga(post model.PostManga) bool {
stmt, err := m.DB.Prepare("INSERT INTO manga (title, genre, volumes, chapters, author) VALUES ($1, $2, $3, $4, $5)")
if err != nil {
log.Println(err)
return false
}
defer stmt.Close()
_, err2 := stmt.Exec(post.Title, post.Genre, post.Volumes, post.Chapters, post.Author)
if err2 != nil {
log.Println(err2)
return false
}
return true
}
// SelectManga implements MangaRepositoryInterface
func (m *MangaRepository) SelectManga() []model.Manga {
var result []model.Manga
rows, err := m.DB.Query("SELECT * FROM manga")
if err != nil {
log.Println(err)
return nil
}
for rows.Next() {
var (
id uint
title string
genre string
volumes uint8
chapters uint16
author string
)
err := rows.Scan(&id, &title, &genre, &volumes, &chapters, &author)
if err != nil {
log.Println(err)
} else {
manga := model.Manga{Id: id, Title: title, Genre: genre, Volumes: volumes, Chapters: chapters, Author: author}
result = append(result, manga)
}
}
return result
}
InsertManga function executes insert SQL command to create a manga data row. While SelectManga function executes select SQL command to get all manga data.
Create Controllers
Controller is the package is used to connect all packages to get the expected result, then return it as API response. Create a folder named controller, then create files named manga_interface.go and manga.go.
manga_interface.go is the controller interface for design the controller architecture. Copy the code below.
package controller
import "github.com/gin-gonic/gin"
type MangaControllerInterface interface {
InsertManga(g *gin.Context)
GetManga(g *gin.Context)
}
manga.go is the controller that is used to connect all packages to get the expected result, then return it as API response. Copy the code below.
package controller
import (
"database/sql"
"github.com/gin-gonic/gin"
"github.com/ramadhanalfarisi/go-simple-dockerizing/model"
"github.com/ramadhanalfarisi/go-simple-dockerizing/repository"
)
type MangaController struct {
DB *sql.DB
}
func NewMangaController(db *sql.DB) MangaControllerInterface {
return &MangaController{DB: db}
}
// GetManga implements MangaControllerInterface
func (m *MangaController) GetManga(g *gin.Context) {
db := m.DB
repo_manga := repository.NewMangaRepository(db)
get_manga := repo_manga.SelectManga()
if get_manga != nil {
g.JSON(200, gin.H{"status": "success", "data": get_manga, "msg": "get manga successfully"})
} else {
g.JSON(200, gin.H{"status": "success", "data": nil, "msg": "get manga successfully"})
}
}
// InsertManga implements MangaControllerInterface
func (m *MangaController) InsertManga(g *gin.Context) {
db := m.DB
var post model.PostManga
if err := g.ShouldBindJSON(&post); err == nil {
repo_manga := repository.NewMangaRepository(db)
insert := repo_manga.InsertManga(post)
if insert {
g.JSON(200, gin.H{"status": "success", "msg": "insert manga successfully"})
} else {
g.JSON(500, gin.H{"status": "failed", "msg": "insert manga failed"})
}
} else {
g.JSON(400, gin.H{"status": "success", "msg": err})
}
}
GetManga function requests all manga data from SelectManga function on repository package, then return a json response. While InsertManga catches the requests from json request and validate it. then, insert the data into database with InsertManga function on repository package.
Create Main Function
Main function will be the function built as an app service. Create a file named main.go on root folder. Copy the code below :
package main
import (
"github.com/ramadhanalfarisi/go-simple-dockerizing/app"
)
func main() {
var a app.App
a.CreateConnection()
a.Migrate()
a.CreateRoutes()
a.Run()
}
Create Dockerfile
Dockerfile is used to create a docker image of the app service. Create file name Dockerfile on root folder. Copy the code below :
FROM golang:1.20
WORKDIR /app
# pre-copy/cache go.mod for pre-downloading dependencies and only redownloading them in subsequent builds if they change
COPY go.mod ./
RUN go mod download && go mod verify
COPY . .
RUN go build -v -o main .
CMD ["/app/main"]
- FROM means the app service use golang version 1.20 as base image
- WORKDIR means the set the working directory when the app execute any commands
- COPY will copy the target file to the target destination in container
- RUN will execute any commands on working directory
- CMD will run the app service
Create Docker Compose
Docker compose contains all container configurations. With docker compose, we don’t need to create container many times, just create a file to create many containers with one execution. Create a file named docker-compose.yml. Copy the code below :
version: '3'
services:
postgres:
image: postgres:latest
container_name: postgres_dockerizing
ports:
- 5432:5432
restart: always
environment:
POSTGRES_PASSWORD: postgres123
volumes:
- database_dockerizing:/var/lib/postgresql/data
- ./dbshell:/docker-entrypoint-initdb.d/
networks:
- fullstack
api:
container_name: api_dockerizing
build: .
ports:
- 8080:8080
restart: always
volumes:
- api_volume:/usr/src/app/
depends_on:
- postgres
networks:
- fullstack
volumes:
database_dockerizing:
api_volume:
networks:
fullstack:
driver: bridge
Execute App Service
To run the app service, we just need to execute docker command. See the command below :
docker-compose up --build
Test API
Insert Manga API
Get All Manga API
See the full code in :
Github : https://github.com/ramadhanalfarisi/go-simple-dockerizing