DEV Community

Arthur Obo-Nwakaji
Arthur Obo-Nwakaji

Posted on

Integrating Django and Golang with Docker and PostgreSQL: A Scalable Approach

In this tutorial guide, we will walk through setting up a scalable, production-ready backend architecture using PostgreSQL, Django, and Golang for microservice in development. In this setup, we’re using Django as our main backend framework and we will utilize the Django ORM for all models structuring and database migrations. Our Django Backend will serve as our single source of truth and will be the only backend to manage Database migrations and schemas.

This architectural setup ensures modularity, performance, and the flexibility to assign the right tool for each job and ensures scalability for a real world production ready architecture.

We'll start by establishing a shared PostgreSQL database docker container accessible by both Django and Golang services. We are only setting up a shared PostgreSQL database container because of development purposes. In a production environment, we’ll definitely want to use a cloud solution for managing our database.

This guide will be divided into 3 parts, the first part is all about Setting Up a simple and straightforward containerized PostgreSQL Database for development purpose. Then the second part will be setting up our Django application and all starter code needed this guide. Then the last part will be our Golang microservice architecture and simple API endpoints to test our connections.

Part 1: Setting Up the PostgreSQL Database

Project Structure:

guide/
├── backend/
├── database/
└── golang/
Enter fullscreen mode Exit fullscreen mode

Our project structure naming convention is self explanatory as we easily know what each directory will handle.

We will begin in the database/ directory where our PostgreSQL setup lives.

database/compose.yml

services:
  postgres:
    image: postgres:14
    container_name: django_golang_db
    hostname: django_golang_db
    restart: unless-stopped
    environment:
      POSTGRES_DB: django_golang_db
      POSTGRES_USER: django_golang_user
      POSTGRES_PASSWORD: django_golang_password
    ports:
      - "5433:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - shared-net

volumes:
  postgres_data:

networks:
  shared-net:
    external: true
Enter fullscreen mode Exit fullscreen mode

In this setup, we, we have our compose.yml file, we have the basic PostgreSQL setup in a docker container. Here are the explanations of all we have implemented in our compose.yml file below:

services: → This defines the services (containers) to be started.
image: → This uses the official PostgreSQL Docker image, version 14.

container_name: django_golang_db → this sets a custom name for our database container instead of a generated one

hostname: django_golang_db → This sets the internal hostname of the container (this will be used in networking between our different containers).

restart: unless-stopped → This will automatically restart our container if it crashes, unless we explicitly stop it.

POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD → All these are self explanatory as they represent our database name, database username, database password respectively.

ports: → This exposes our PostgreSQL's default port 5432 inside the container as 5433 on your host machine. This means we will connect to PostgreSQL via localhost:5433 on our host machine. I am using port 5433 because on my local machine, I have PostgreSQL already using port 5432 which is the standard default PostgreSQL port.

volumes: → This mounts a named volume called postgres_data to persist database data, even if the container is destroyed.

networks:
   shared-net
Enter fullscreen mode Exit fullscreen mode

This here connects the PostgreSQL container to the user-defined external network named shared-net which we will create shortly in the next command we will run. This is useful since other containers like our Django backend or Go service need to talk to this PostgreSQL container using its hostname.

volumes: → This Declares a named volume called postgres_data, used above to persist database data.

networks: → This is needed as it declares the shared-net network as external, meaning it must already exist before we run this compose file. Docker won't create this for us automatically. It enables the communication with other containers

Why External Network?

We are using a named external network (shared-net) to ensure our postgres container can be accessed by both the Django Backend and Golang service that will be defined in other Docker Compose files in separate directoriesn (Django Backend and Golang Service).

Creating the Shared Docker Network

Before running the PostgreSQL container, we need to create the shared Docker network manually:

docker network create shared-net

This command ensures that all services across different Compose files can communicate seamlessly.

NOTE: You can only run this command once, for me I can’t run the command again because I already have a running network and got the error message when I tried running it a second time: Error response from daemon: network with name shared-net already exists

Starting the PostgreSQL Container

Navigate to the database/ directory and run:
docker compose up --build

Here, we will see the PostgreSQL container named django_golang_db running on port 5433.

Here, I am using the —build flag so we can see the logs as the container is running.

Verifying PostgreSQL Access

For us to get a list of all running containers, we need to run the first command below, then we can run the second command below to connect and test the database in an interactive bash shell, then the third command to enter our PostgreSQL shell and will be able to run PostgreSQL specific commands in the future when we’re done with setting up the Django side of things:

docker ps
docker exec -it django_golang_db bash
psql -U django_golang_user -d django_golang_db
Enter fullscreen mode Exit fullscreen mode

If successful, you should see the psql prompt:

django_golang_db=#

At this point, our database is ready and accessible for both Django and Golang services via the container hostname django_golang_db over port 5432 (internal Docker port).

Part 2: Integrating Django with External PostgreSQL

In this section, we will dive into setting up Django to use this external PostgreSQL container using a separate compose.yml, Dockerfile, and Nginx reverse proxy.

Now that our PostgreSQL database is up and running on a shared Docker network (shared-net), we’ll connect our Django application to it. The goal here is to make Django our schema source and business logic hub which means Django will be our single source of truth, especially useful for admin interfaces, authentication, and model definitions.

backend/
├── backend/ # Django core Setting (includes settings, urls, etc.)
├── blog/ # Example app
├── manage.py
├── Dockerfile
├── compose.yml
├── entrypoint.sh
├── requirements.txt
├── nginx.conf
├── media/
└── staticfiles/
Enter fullscreen mode Exit fullscreen mode

Follow the commands below to get a working django project with an initial virtual environment which we will not need anymore once our docker set up is complete:

python -m venv venv
source venv/bin/activate
mkdir backend
cd backend
Enter fullscreen mode Exit fullscreen mode

Now we have a working backend directory, let’s create a new requirements.txt file in our backend directory which will have all our dependencies:

backend/requirements.txt

amqp==5.3.1
annotated-types==0.7.0
anyio==4.9.0
asgiref==3.8.1
async-timeout==5.0.1
billiard==4.2.1
celery==5.5.1
certifi==2025.4.26
click==8.2.0
click-didyoumean==0.3.1
click-plugins==1.1.1
click-repl==0.3.0
Deprecated==1.2.18
Django==5.2.1
django-autoslug==1.9.9
django-cors-headers==4.7.0
django-elasticsearch-dsl==7.2.2
django-elasticsearch-dsl-drf==0.22.4
django-nine==0.2.7
djangorestframework==3.16.0
elastic-transport==8.17.1
elasticsearch==7.13.4
elasticsearch-dsl==7.4.0
exceptiongroup==1.3.0
fastapi==0.115.12
gunicorn==23.0.0
h11==0.16.0
idna==3.10
kombu==5.5.3
packaging==25.0
prompt_toolkit==3.0.51
psycopg2-binary==2.9.10
pydantic==2.11.4
pydantic_core==2.33.2
python-dateutil==2.9.0.post0
redis==4.3.4
six==1.17.0
sniffio==1.3.1
sqlparse==0.5.3
starlette==0.46.2
typing-inspection==0.4.0
typing_extensions==4.13.2
tzdata==2025.2
urllib3==1.26.20
uv==0.7.3
uvicorn==0.34.2
vine==5.1.0
wcwidth==0.2.13
wrapt==1.17.2
Enter fullscreen mode Exit fullscreen mode

Now let’s install all dependencies so we can create a new Django project, we can choose to install only Django as that is only what we need to get our Django project running, but for this guide, we will need to install all dependencies:

pip install -r requirements.txt
#OR
pip install Django==5.2.1
Enter fullscreen mode Exit fullscreen mode

Once our required dependencies are installed, the next we have to do is to start a new django project with the command below:
django-admin startproject backend .

We’re using the extra . because we want the whole project to be in our backend folder.

Now we can test the django application with the runserver command to be sure our django project is running manually:
python manage.py runserver

We can visit localhost on port 8000, and we’ll see our django application running there.

Once our django application is running manually, let’s create a new blog app in our django backend. This new blog app will handle our models structure for a simple blog application which our Golang microservice will use.

python manage.py startapp blog

Once this new blog app is created, let’s add it to the backend/backend/settings.py file in the list of INSTALLED_APPS:

INSTALLED_APPS = [
        ...
    'django.contrib.staticfiles',

    # Local apps
    'blog', # New
]
Enter fullscreen mode Exit fullscreen mode

Then in our backend/blog/models.py let’s add the following code below for our models structure:

backend/blog/models.py

from django.db import models


class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    date_created = models.DateTimeField(auto_now_add=True)
    last_updated = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.title
Enter fullscreen mode Exit fullscreen mode

Next we have to update our backend/blog/admin.py file:
backend/blog/admin.py

from django.contrib import admin
from .models import Post

admin.site.register(Post)
Enter fullscreen mode Exit fullscreen mode

Django Docker Compose File Setup

Let’s create a new backend/compose.yml file in our django setup:

backend/compose.yml

services:
  backend:
    build: .
    container_name: backend
    hostname: backend  # this helps DNS resolution
    expose:
      - "8000"
    networks:
      - shared-net
    env_file:
      - .env
    volumes:
      - .:/app
      - static_volume:/app/staticfiles
      - media_volume:/app/media

  nginx:
    image: nginx:latest
    container_name: nginx
    depends_on:
      - backend
    ports:
      - "8000:8000"
    networks:
      - shared-net
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
      - static_volume:/app/staticfiles
      - media_volume:/app/media

volumes:
  static_volume:
  media_volume:

networks:
  shared-net:
    external: true
Enter fullscreen mode Exit fullscreen mode

From our compose.yml file above, we can see we’re setting up two containers for our use case which is the actual backend and nginx setup to handle incoming requests and serve static/media files efficiently.

Here I’m only going to explain the significant external PostgreSQL Database connection configuration:

networks:
  shared-net
Enter fullscreen mode Exit fullscreen mode

This joins the external network shared-net, enabling communication with other containers like nginx or postgres

networks:
  shared-net:
    external: true
Enter fullscreen mode Exit fullscreen mode

This above uses an external Docker network which we have already created before in the first part with the command (docker network create shared-net). We cannot run this command again as it is already working.

Django Dockerfile File Setup

Let’s create a new backend/Dockerfile file in our django backend

setup:
FROM python:3.10

RUN mkdir /app

WORKDIR /app

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# Install uv
RUN pip install --upgrade pip
RUN pip install --upgrade uv

COPY requirements.txt /app/

# Use uv with --system flag to install packages system-wide without virtual env
RUN uv pip install --system --no-cache-dir -r requirements.txt

COPY . /app/

EXPOSE 8000

RUN chmod +x entrypoint.sh

CMD ["./entrypoint.sh"]
Enter fullscreen mode Exit fullscreen mode

This is a basic and straightforward Dockerfile setup for a Django application. From our setup, we see we need an backend/entrypoint.sh file to run all django specific commands.


#!/bin/bash

# Define path to manage.py
MANAGE_PY="/app/manage.py"

# Apply migrations
echo "Applying database migrations"
python $MANAGE_PY migrate --noinput

# Collect static files
echo "Collecting static files"
python $MANAGE_PY collectstatic --noinput

# Start the server - the $DJANGO_MODE is set in the .env file and is either "development" or "production"
if [ "$DJANGO_MODE" = "development" ]; then
    echo "Starting Django development server..."
    exec python $MANAGE_PY runserver 0.0.0.0:8000
else
    echo "Starting Gunicorn server..."
    exec gunicorn backend.wsgi:application --bind 0.0.0.0:8000 --workers 4
fi
Enter fullscreen mode Exit fullscreen mode

Once we have this entrypoint.sh file in place, we need to make it executable, run the command below to make it executable:
chmod +x backend/entrypoint.sh

Django Database Configuration

Next we need to do is replace the DATABASES section setup in our backend/backend/settings.py file to use the shared PostgreSQL container instead of the default sqlite3 database.

backend/backend/settings.py


DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'django_golang_db',
        'USER': 'django_golang_user',
        'PASSWORD': 'django_golang_password',
        'HOST': 'django_golang_db',  # this must match Postgres container name
        'PORT': '5432',
    }
}
Enter fullscreen mode Exit fullscreen mode

Next we have to do in the backend/backend/settings.py file is to set our STATIC_ROOT, add this block of code below beneath our STATIC_URL:

import os
...
STATIC_URL = 'static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
Enter fullscreen mode Exit fullscreen mode

Last thing we need to setup in our backend/backend/settings.py file is the CSRF_TRUSTED_ORIGINS:

BACKEND_URLS = [
    # Our local IP and port used by Nginx
    "http://127.0.0.1:8000",
    "http://localhost:8000",
]

CSRF_TRUSTED_ORIGINS = BACKEND_URLS
Enter fullscreen mode Exit fullscreen mode

Next thing we need to set up is our backend/nginx.conf file which will handle all incoming requests:

backend/nginx.conf

server {
    listen 8000;

    location / {
        proxy_pass http://backend:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /static/ {
        alias /app/staticfiles/;
        expires 30d;
    }

    location /media/ {
        alias /app/media/;
    }
}
Enter fullscreen mode Exit fullscreen mode

Running the Django Container

From the backend/ directory, let’s run the command below to build and run our Django container and we will see our application running on localhost on port 8000:
docker compose up --build

Now that our django application is running, next we need to do is open our django container in an interactive bash shell run the following commands to perform some migrations.

docker ps
docker compose exec -it backend bash
Enter fullscreen mode Exit fullscreen mode

Once you run the last command above, you’ll see a shell like this below, it means you are in the interactive shell:
root@backend:/app#

Great job, now we’re in the interactive bash shell, we can run the following commands

python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser
Enter fullscreen mode Exit fullscreen mode

Validating DB Connection

Now this is all the setup we have to do code wise for our django backend and we now have our superuser, we can log in to the django admin and add some blog data which we will use for our Golang microservice in the next section.

Before we proceed to the next section, let’s open our postgresql in an interactive bash shell to see our whole database schema and structure.

To validate that Django is writing to PostgreSQL correctly:

  1. Enter the PostgreSQ Database container:
    docker exec -it django_golang_db bash

  2. Connect with psql:
    psql -U django_golang_user -d django_golang_db

  3. Check tables with this sql commands below:
    \dt

In the \dt command we just ran, we’ll see that it lists all the tables in our PostgreSQL database like this below:

\dt
                        List of relations
 Schema |            Name            | Type  |       Owner        
--------+----------------------------+-------+--------------------
 public | auth_group                 | table | django_golang_user
 public | auth_group_permissions     | table | django_golang_user
 public | auth_permission            | table | django_golang_user
 public | auth_user                  | table | django_golang_user
 public | auth_user_groups           | table | django_golang_user
 public | auth_user_user_permissions | table | django_golang_user
 public | blog_post                  | table | django_golang_user
 public | django_admin_log           | table | django_golang_user
 public | django_content_type        | table | django_golang_user
 public | django_migrations          | table | django_golang_user
 public | django_session             | table | django_golang_user
(11 rows)
Enter fullscreen mode Exit fullscreen mode

We see that we have all the default Django tables created when we run the python manage.py migrate command for the first time in a Django application and our blog_post table in the blog app for the Post models instance we created earlier.

Next let’s view all the columns in the blog_post table, we’ll use the command below:
\d blog_post

\d blog_post
                                     Table "public.blog_post"
    Column    |           Type           | Collation | Nullable |             Default              
--------------+--------------------------+-----------+----------+----------------------------------
 id           | bigint                   |           | not null | generated by default as identity
 title        | character varying(200)   |           | not null | 
 content      | text                     |           | not null | 
 date_created | timestamp with time zone |           | not null | 
 last_updated | timestamp with time zone |           | not null | 
Indexes:
    "blog_post_pkey" PRIMARY KEY, btree (id)
Enter fullscreen mode Exit fullscreen mode

At this point, everything works perfectly both on our Database set up and Django setup which now brings us to the final part of this guide.

Part 3: Integrating Golang with PostgreSQL

In this part, we’ll connect our Golang backend to the same PostgreSQL database container that our Django app uses. This setup allows Django to manage database models and migrations, while Golang focuses on high-performance APIs using raw SQL for speed and control.

Golang Project Structure:

golang/
├── cmd/
│   └── api/
│       └── main.go
├── internal/
│   ├── blog/
│   │   ├── handler.go
│   │   ├── models.go
│   │   └── repository.go
│   ├── db/
│   │   └── postgres.go
│   └── users/
│       ├── handler.go
│       ├── model.go
│       └── repository.go
├── pkg/
│   └── utils/
│       └── response.go
├── Dockerfile
├── compose.yml
├── go.mod
├── go.sum
└── .air.toml
Enter fullscreen mode Exit fullscreen mode

Step 1: Initialize the Go Module

Navigate to the golang/ directory and initialize the module. The name should reflect your import paths.


cd golang
go mod init api
Enter fullscreen mode Exit fullscreen mode

This creates the go.mod file.


Step 2: Install Required Dependencies

Install the required packages using go get:

go get github.com/gorilla/mux
go get github.com/lib/pq
Enter fullscreen mode Exit fullscreen mode

This generates the appropriate entries in go.sum, such as:

github.com/gorilla/mux v1.8.1
github.com/lib/pq v1.10.9
Enter fullscreen mode Exit fullscreen mode

Step 3: Create the Main Entry File

Create golang/cmd/api/main.go with the following content:

package main

import (
    "api/internal/blog"
    "api/internal/db"
    "api/internal/users"
    "log"
    "net/http"

    "github.com/gorilla/mux"
)

func main() {
    database, err := db.ConnectDB()
    if err != nil {
        log.Fatal(err)
    }

    router := mux.NewRouter()

    users.RegisterRoutes(router, database)
    blog.RegisterRoutes(router, database)

    log.Println("Server is running on port 8080...")
    log.Fatal(http.ListenAndServe(":8080", router))
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Database Connection

Create golang/internal/db/postgres.go:

package db

import (
    "database/sql"
    "fmt"

    _ "github.com/lib/pq"
)

func ConnectDB() (*sql.DB, error) {
    host := "django_golang_db"
    port := 5432
    user := "django_golang_user"
    password := "django_golang_password"
    dbname := "django_golang_db"

    psqlInfo := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
        host, port, user, password, dbname)

    db, err := sql.Open("postgres", psqlInfo)
    if err != nil {
        return nil, err
    }
    return db, db.Ping()
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Code Implementation

At this point we will handle the implementation of users and blog in both golang/internal/users and golang/internal/blog respectively

golang/internal/users/model.go

package users

import "time"

type CompleteUsersModel struct {
    ID          int       `json:"id"`
    LastLogin   time.Time `json:"last_login"`
    IsSuperuser bool      `json:"is_superuser"`
    Username    string    `json:"username"`
    FirstName   string    `json:"first_name"`
    LastName    string    `json:"last_name"`
    Email       string    `json:"email"`
    IsStaff     bool      `json:"is_staff"`
    IsActive    bool      `json:"is_active"`
    DateJoined  time.Time `json:"date_joined"`
    Password    string    `json:"password"`
}

type GetAllUsersModel struct {
    ID        int    `json:"id"`
    Email     string `json:"email"`
    FirstName string `json:"first_name"`
    LastName  string `json:"last_name"`
    Username  string `json:"username"`
    IsActive  bool   `json:"is_active"`
}
Enter fullscreen mode Exit fullscreen mode

golang/internal/users/repository.go

package users

import "database/sql"

func GetAllUsers(db *sql.DB) ([]GetAllUsersModel, error) {
    rows, err := db.Query("SELECT id, email, first_name, last_name, username, is_active FROM auth_user")
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var users []GetAllUsersModel
    for rows.Next() {
        var u GetAllUsersModel
        if err := rows.Scan(&u.ID, &u.Email, &u.FirstName, &u.LastName, &u.Username, &u.IsActive); err != nil {
            return nil, err
        }
        users = append(users, u)
    }
    return users, nil
}
Enter fullscreen mode Exit fullscreen mode

golang/internal/users/handler.go

package users

import (
    "api/pkg/utils"
    "database/sql"
    "fmt"
    "net/http"

    "github.com/gorilla/mux"
)

func RegisterRoutes(r *mux.Router, db *sql.DB) {
    r.HandleFunc("/users", getAllUsersHandler(db)).Methods("GET")
}

func getAllUsersHandler(db *sql.DB) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        fmt.Println("Getting all usersD")
        users, err := GetAllUsers(db)
        if err != nil {
            utils.WriteJSONError(w, http.StatusInternalServerError, "Error fetching users", err.Error())
            return
        }
        utils.WriteJSONSuccess(w, http.StatusOK, "Users fetched successfully", users)
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we have all the code for our users functionality, next we need to do is create our response pkg utility in
golang/pkg/utils/response.go

package utils

import (
    "encoding/json"
    "net/http"
)

// This function is used to write a JSON error response to the client, will
// be called where there is an error in all api requests
func WriteJSONError(w http.ResponseWriter, status int, message string, details interface{}) {
    w.WriteHeader(status)

    resp := map[string]interface{}{
        "message": message,
    }

    if details != nil {
        resp["error"] = details
    }

    json.NewEncoder(w).Encode(resp)
}

type SuccessResponse struct {
    Message string      `json:"message"`
    Data    interface{} `json:"data,omitempty"`
}

// This function is used to write a JSON success response to the client, will
// be called where there is an error in all api requests
func WriteJSONSuccess(w http.ResponseWriter, status int, message string, data interface{}) {
    w.WriteHeader(status)
    resp := SuccessResponse{
        Message: message,
        Data:    data,
    }
    json.NewEncoder(w).Encode(resp)
}
Enter fullscreen mode Exit fullscreen mode

Now the last setup is our blog application we need to setup:

golang/internal/blog/model.go

package blog

import "time"

type CompleteBlogModel struct {
    ID          int64     `json:"id"`
    Title       string    `json:"title"`
    Content     string    `json:"content"`
    DateCreated time.Time `json:"date_created"`
    LastUpdated time.Time `json:"last_updated"`
}

type BlogCreateModel struct {
    ID          int64     `json:"id"`
    Title       string    `json:"title"`
    Content     string    `json:"content"`
    DateCreated time.Time `json:"date_created"`
    LastUpdated time.Time `json:"last_updated"`
}

func (p *BlogCreateModel) Validate() []string {
    var errors []string
    if p.Title == "" {
        errors = append(errors, "title is required")
    }
    if p.Content == "" {
        errors = append(errors, "content is required")
    }
    return errors
}

type GetAllBlogModel struct {
    ID          int64     `json:"id"`
    Title       string    `json:"title"`
    Content     string    `json:"content"`
    DateCreated time.Time `json:"date_created"`
    LastUpdated time.Time `json:"last_updated"`
}

type IndividualPostModel struct {
    ID          int64     `json:"id"`
    Title       string    `json:"title"`
    Content     string    `json:"content"`
    DateCreated time.Time `json:"date_created"`
    LastUpdated time.Time `json:"last_updated"`
}

type UpdatePostModel struct {
    ID      int64  `json:"id"`
    Title   string `json:"title"`
    Content string `json:"content"`
}

func (p *UpdatePostModel) Validate() []string {
    var errors []string
    if p.Title == "" {
        errors = append(errors, "title is required")
    }
    if p.Content == "" {
        errors = append(errors, "content is required")
    }
    return errors
}

Enter fullscreen mode Exit fullscreen mode

golang/internal/blog/repository.go

package blog

import "database/sql"

func GetAllBlogs(db *sql.DB) ([]GetAllBlogModel, error) {
    rows, err := db.Query("SELECT id, title, content, date_created, last_updated FROM blog_post")
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var blogs []GetAllBlogModel
    for rows.Next() {
        var u GetAllBlogModel
        if err := rows.Scan(&u.ID, &u.Title, &u.Content, &u.DateCreated, &u.LastUpdated); err != nil {
            return nil, err
        }
        blogs = append(blogs, u)
    }
    return blogs, nil
}

func GetPostByID(db *sql.DB, id string) (IndividualPostModel, error) {
    var p IndividualPostModel
    err := db.QueryRow("SELECT id, title, content, date_created, last_updated FROM blog_post WHERE id = $1", id).Scan(&p.ID, &p.Title, &p.Content, &p.DateCreated, &p.LastUpdated)
    if err != nil {
        return p, err
    }
    return p, nil
}

func UpdatePost(db *sql.DB, id string, p *UpdatePostModel) (bool, error) {
    result, err := db.Exec("UPDATE blog_post SET title = $1, content = $2 WHERE id = $3", p.Title, p.Content, id)
    if err != nil {
        return false, err
    }
    rowsAffected, err := result.RowsAffected()
    if err != nil {
        return false, err
    }
    return rowsAffected > 0, nil
}
Enter fullscreen mode Exit fullscreen mode

golang/internal/blog/handler.go

package blog

import (
    "api/pkg/utils"
    "database/sql"
    "encoding/json"
    "fmt"
    "net/http"

    "github.com/gorilla/mux"
)

func RegisterRoutes(r *mux.Router, db *sql.DB) {
    r.HandleFunc("/posts", getAllBlogsHandler(db)).Methods("GET")
    r.HandleFunc("/posts/{id}", getPostByIDHandler(db)).Methods("GET")
    r.HandleFunc("/posts/{id}", updatePostHandler(db)).Methods("PUT")
}

func getAllBlogsHandler(db *sql.DB) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        fmt.Println("Getting all usersD")
        blogs, err := GetAllBlogs(db)
        if err != nil {
            utils.WriteJSONError(w, http.StatusInternalServerError, "Error fetching users", err.Error())
            return
        }
        utils.WriteJSONSuccess(w, http.StatusOK, "Blogs fetched successfully", blogs)
    }
}

func getPostByIDHandler(db *sql.DB) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        id := mux.Vars(r)["id"]
        post, err := GetPostByID(db, id)
        if err != nil {
            utils.WriteJSONError(w, http.StatusNotFound, "Post not found", nil)
            return
        }
        utils.WriteJSONSuccess(w, http.StatusOK, "Post fetched successfully", post)
    }
}

func updatePostHandler(db *sql.DB) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        id := mux.Vars(r)["id"]
        var p UpdatePostModel
        if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
            utils.WriteJSONError(w, http.StatusBadRequest, "Invalid request body", err.Error())
            return
        }
        updated, err := UpdatePost(db, id, &p)
        if err != nil {
            utils.WriteJSONError(w, http.StatusInternalServerError, "Error updating post", err.Error())
            return
        }
        utils.WriteJSONSuccess(w, http.StatusOK, "Post updated successfully", updated)
    }
}
Enter fullscreen mode Exit fullscreen mode

Dockerize the Golang Microservice Application

Golang Dockerfile:
golang/Dockerfile

FROM golang:1.23.4-alpine

WORKDIR /app

# Install git for downloading Air
RUN apk add --no-cache git

# Install Air
RUN go install github.com/air-verse/air@latest


COPY go.mod ./
COPY go.sum ./
RUN go mod download

COPY . .
COPY .air.toml . 

EXPOSE 8080

CMD ["air", "-c", ".air.toml"]
Enter fullscreen mode Exit fullscreen mode

Docker Compose File:

golang/compose.yml

services:
  api:
    build: .
    container_name: golang_api
    volumes:
      - .:/app
    networks:
      - shared-net
    ports:
      - "8080:8080"

networks:
  shared-net:
    external: true
Enter fullscreen mode Exit fullscreen mode

We’re using the same external Docker network shared-net so that the Golang container can reach PostgreSQL using django_golang_db as host.

Air Library Initialization:

Lastly we need to initialize our Air library we installed installed earlier which will be used for reloading our server (go install github.com/air-verse/air@latest), simply run this command below in your Go project root:

air init

We now have an initialized .air.toml file created in our golang root directory. Replace the generated code with this below:

golang/.air.toml

# .air.toml
# Configures Air to watch your app and build the actual entry point in cmd/api

root = "."
tmp_dir = "tmp"

[build]
  cmd = "go build -buildvcs=false -o ./tmp/main ./cmd/api"
  bin = "tmp/main"
  include_ext = ["go"]
  exclude_dir = ["tmp", "vendor"]
  delay = 1000
  log = "air.log"
  send_interrupt = true
Enter fullscreen mode Exit fullscreen mode

We’re almost done, now we can run and build our golang microservice with the command below:

docker compose up --build

Once our golang microservice is running, we’ll see it running on port 8080. At this point we can consume the API endpoints

/posts [GET] → Gets the list of all Blog posts

/posts/{id} [GET] → Gets the post object’s data detail

/posts/{id} [PUT] → Update the post object’s data details

/users [GET] → Gets the list of all users

Codebase can be found here:
https://github.com/Arthurobo/-django-golang-postgresql.git

Top comments (0)