fbpx
Hero Illustration
1 Comments
Docker, Mitrais, Software Development

How to Dockerize a Restful API with Golang and Postgres

Prerequisites

To make the most of the instruction in this article about Docker, you will need basic knowledge of git, Golang language, and RDBMS. In addition, you will need to install the following applications on your machine.

  • Text editor/IDE e.g., VSCode
  • PgAdmin
  • Docker
  • Git (this is optional if you like to write your Golang project from scratch)

Why Docker?

Developing an app with a team, especially on a large-scale complex project, can be challenging. A particular code modification may work on one system but not on another. That’s why “It’s working on my machine” was a popular meme among developers in the past. This issue occurs because of the variations of the machine or the system environment, missing dependencies, or a hardcoded path/directory.

Using Docker can swiftly resolve this problem. It ensures that all developers have an identical environment where the application operates. Docker is a set of platform-as-a-service technologies that leverage OS-level virtualization in the form of a container. As a result, Docker provides a lighter service than a virtual machine. Docker containers are isolated, yet they can communicate over well-defined channels. Docker also provides hundreds of official docker images with various environments to make the process even easier.

Setting Up The Project

This article focuses on how to configure a Docker for our Go app. The Go app is a simple CRUD app that uses Postgres to store the data. You can clone the app through this repository (https://github.com/Agus-Wei/go-app.git) or build one yourself.
Before configuring the Docker, make sure you have the Docker installed on your machine. You can find the Docker installation instruction file on the Docker official website on https://docs.docker.com/get-docker/

Preparing Dockerfile

A Dockerfile should be created on the project root directory to create a Docker image. A Dockerfile is a text document that contains all the commands used on the command line. To execute these commands on the Dockerfile, use the “Docker build” one. The following section will explain how we can set up multiple containers/services on Docker.

# Start from golang base image
FROM golang:alpine

# Add Maintainer info
LABEL maintainer="Agus Wibawantara"

# Install git.
# Git is required for fetching the dependencies.
RUN apk update && apk add --no-cache git && apk add --no-cach bash && apk add build-base

# Setup folders
RUN mkdir /app
WORKDIR /app

# Copy the source from the current directory to the working Directory inside the container
COPY . .
COPY .env .

# Download all the dependencies
RUN go get -d -v ./...

# Install the package
RUN go install -v ./...

# Build the Go app
RUN go build -o /build

# Expose port 8080 to the outside world
EXPOSE 8080

# Run the executable
CMD [ "/build" ]

Build Application Services

In this article, the app will need two containers/services: the first is the application container/service for the API, and the second is the Postgres container. We will use docker-compose to define those containers. To determine the containers, a docker-compose.yml file needs to be created in our root project directory.

The Below configuration will create 2 Postgres containers. The postgres_container will be used as the main database on the project. When running the test suite, the postgres_test_container will be used as the test db.

version: '3.9'
services:
  app:
    container_name: golang_container
    environment:
      - POSTGRES_USER=${DB_USER}
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_DB=${DB_NAME}
      - DATABASE_HOST=${DB_HOST}
      - DATABASE_PORT=${DB_PORT}
      - TEST_POSTGRES_USER=${TEST_DB_USER}
      - TEST_POSTGRES_PASSWORD=${TEST_DB_PASSWORD}
      - TEST_POSTGRES_DB=${TEST_DB_NAME}
      - TEST_DATABASE_HOST=${TEST_DB_HOST}
      - TEST_DATABASE_PORT=${TEST_DB_PORT}
    tty: true
    build: .
    ports:
      - 8000:8080
    restart: on-failure
    volumes:
      - .:/app
    depends_on:
      - postgresdb
    networks:
      - learning

  postgresdb:
    image: postgres:latest
    container_name: postgres_container
    environment:
      - POSTGRES_USER=${DB_USER}
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_DB=${DB_NAME}
      - DATABASE_HOST=${DB_HOST}
    ports:
      - '1234:5432'
    networks:
      - learning

  postgresdb_test:
    image: postgres:latest
    container_name: postgres_test_container
    environment:
      - POSTGRES_USER=${TEST_DB_USER}
      - POSTGRES_PASSWORD=${TEST_DB_PASSWORD}
      - POSTGRES_DB=${TEST_DB_NAME}
      - DATABASE_HOST=${TEST_DB_HOST}
    ports:
      - '4568:5432'
    networks:
      - learning

# Networks to be created to facilitate communication between containers
networks:
  learning:
    driver: bridge

Since our docker-compose.yml uses an environment variable, we need to set it up through the .env file, which should be placed on the root project directory.

DB_HOST=postgresdb
DB_DRIVER=postgres
DB_USER=spuser
DB_PASSWORD=SPuser96
DB_NAME=project
DB_PORT=5432

# Postgres Test
TEST_DB_HOST=postgres_test
TEST_DB_DRIVER=postgres
TEST_DB_USER=spuser
TEST_DB_PASSWORD=SPuser96
TEST_DB_NAME=project_test
TEST_DB_PORT=5432

The above basic setup will only work in the same container used. A new empty container will be created whenever the Docker container needs to be rebooted or when a Docker run is being executed. The data that is being stored on the previous container will be lost.
To deal with that issue, store our Postgres data inside a mounted volume.

First, create a new directory, e.g., “pg_data”. After that, inside the docker-compose.yml, mount your host directory to “/var/lib/postgresql/data” directory under the volumes keyword. The updated docker-compose.yml will look like the snippet below.

version: '3.9'
services:
  app:
    container_name: golang_container
    environment:
      - POSTGRES_USER=${DB_USER}
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_DB=${DB_NAME}
      - DATABASE_HOST=${DB_HOST}
      - DATABASE_PORT=${DB_PORT}
      - TEST_POSTGRES_USER=${TEST_DB_USER}
      - TEST_POSTGRES_PASSWORD=${TEST_DB_PASSWORD}
      - TEST_POSTGRES_DB=${TEST_DB_NAME}
      - TEST_DATABASE_HOST=${TEST_DB_HOST}
      - TEST_DATABASE_PORT=${TEST_DB_PORT}
    tty: true
    build: .
    ports:
      - 8000:8080
    restart: on-failure
    volumes:
      - .:/app
    depends_on:
      - postgresdb
    networks:
      - learning

  postgresdb:
    image: postgres:latest
    container_name: postgres_container
    environment:
      - POSTGRES_USER=${DB_USER}
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_DB=${DB_NAME}
      - DATABASE_HOST=${DB_HOST}
    ports:
      - '1234:5432'
    volumes:
      - ./pg_data:/var/lib/postgresql/data
    networks:
      - learning

  postgresdb_test:
    image: postgres:latest
    container_name: postgres_test_container
    environment:
      - POSTGRES_USER=${TEST_DB_USER}
      - POSTGRES_PASSWORD=${TEST_DB_PASSWORD}
      - POSTGRES_DB=${TEST_DB_NAME}
      - DATABASE_HOST=${TEST_DB_HOST}
    ports:
      - '4568:5432'
    volumes:
      - ./pg_data_test:/var/lib/postgresql/data
    networks:
      - learning

volumes:
  pg_data:
  pg_data_test:

# Networks to be created to facilitate communication between containers
networks:
  learning:
    driver: bridge

Please note that when you run Docker on a Windows host machine, it might be necessary to create a volume using “docker volume create -d local –name pg_data.” Also, it must be flagged as external volume.

Run The Application

Once the Dockerfile and the docker-compose.yml have been prepared, run “docker-compose build” and grab a cup of coffee while Docker is starting up your application.
You will see a bunch of commands being executed by Docker as shown on the image below.

Once it is completed, run “docker-compose up.” You will notice that the “golang_contrainer” exits with code 2 because the DB has not yet existed on Postgres.

To create the database, use PgAdmin. Once you are on PgAdmin, create two new databases and run the SQL script on the db directory to create the items table.

Setting-Up Hot Reloading

However, there is a limitation in the above Dockerfile configuration. When the code is updated, it does not trigger the app rebuild. For example, if the response message on the test endpoint is changed, your updated message won’t show up on the API call response. We can update our Dockerfile to rebuild the app after changing the file. The following steps show how to set up Dockerfile to handle hot-reload using CompileDaemon.

First, tell Docker to install two additional go packages by adding the command below.
RUN go get github.com/githubnemo/CompileDaemon
RUN go get -v golang.org/x/tools/gopls

Remove the code, starting from the build the go app section to the end of the file.

# Build the Go app
RUN go build -o /build

# Expose port 8080 to the outside world
EXPOSE 8080

# Run the executable
CMD [ "/build" ]

Once it is removed, add the following command on the Dockerfile.

ENTRYPOINT CompileDaemon –build=”go build -a -installsuffix cgo -o main .” –command=./main

A complete Dockerfile should look like the snippet below:

# Start from golang base image
FROM golang:alpine

# Add Maintainer info
LABEL maintainer="Agus Wibawantara"

# Install git.
# Git is required for fetching the dependencies.
RUN apk update && apk add --no-cache git && apk add --no-cache bash && apk add build-base

# Setup folders
RUN mkdir /app
WORKDIR /app

# Copy the source from the current directory to the working Directory inside the container
COPY . .
COPY .env .

# Download all the dependencies
RUN go get -d -v ./...

# Install the package
RUN go install -v ./...

#Setup hot-reload for dev stage
RUN go get github.com/githubnemo/CompileDaemon
RUN go get -v golang.org/x/tools/gopls

ENTRYPOINT CompileDaemon --build="go build -a -installsuffix cgo -o main ." --command=./main

Now, rebuild the image and run docker-compose up. Looking at the terminal closer, you will find that the container has “Running the build command!”. It indicates that the container is rebuilding the app based on the latest changes in the code.

Update the “/test” endpoint response message on the IDE and save the file. Go to the terminal, and there will be another Running build command! Once the build is completed, open Postman, and make another request. The API response should have the updated message at this point.

Conclusion

This article has dockerized a Restful API project with Golang and Postgres. The docker configuration above allows us to have hot-reload functionality. You can read how to set up and run the Golang test suite on a dockerized project in the next topic.

Author: Gusti Ngurah Made Agus Wibawantara, Analyst Programmer

Contact us to learn more!

Please complete the brief information below and we will follow up shortly.

    ** All fields are required
    Leave a comment


    Wisnu Pramoedya
    1 year ago

    I add the Dockerfile as you have shown. But, when I change the code in my main.go, like deleting the route, it doesn’t rebuild again. This is my Dockerfile.
    “`
    #Setup hot-reload for the dev stage (I change go get to go install -mod=mod because an error in CompileDaemon was not found)
    RUN go install -mod=mod github.com/githubnemo/CompileDaemon
    RUN go get -v golang.org/x/tools/gopls

    ENTRYPOINT CompileDaemon –build=”go build -a -installsuffix cgo -o main .” –command=./main
    “`

    Reply