Get GORM working with Postgres in Docker
This was challenging because even though my Go web service depended on the
PostgreSQL db service in docker compose, I still needed to add some extra
clauses to get these two technologies to synchronize. The trick was adding a
block to the db service to make sure postgres was up, and adding more metadata
to the depends_on
block in the web service to coordinate with the db service
about when it was healthy and ready.
Docker Compose Setup
A lot of this is normal, we’re picking the latest postgres image, we’re picking
simple values for the environment variables. The interesting thing is the
healthcheck
block on the db service, and the depends_on
block on the web
service. The web service waits until the db is in the service_healthy
state.
The arguments in the healthcheck
block are -U
which should match
POSTGRES_USER
, and -d
which should match POSTGRES_DB
. I tried using
string interpolation here, but it didn’t work right away, and rather than go
down a string interpolation rabbit hole, I just hardcoded those values for now.
services:
db:
image: postgres:latest
restart: always
environment:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
interval: 10s
retries: 5
start_period: 30s
timeout: 10s
volumes:
- pgdata:/var/lib/postgresql/data
web:
build: .
environment:
GO_PORT: 8080
ports:
- "8080:8080"
depends_on:
db:
condition: service_healthy
restart: true
links:
- db
volumes:
pgdata:
Accessing Postgres from GORM
Setting up GORM was the easier part of this. Here is my database service code.
package service
import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
type DbService struct {
DB *gorm.DB
}
func NewDbService() DbService {
dsn := "host=db user=postgres password=password dbname=postgres port=5432 sslmode=disable connect_timeout=5 TimeZone=America/Denver"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
return DbService{DB: db}
}
And then the main entrypoint looks like this. In the init()
function we set
up the database object, and then share that with the various services that
interact directly with the database, and then share the services with the
handlers that accept requests, and return Templ
components to the client.
package main
import (
"os"
"notesApp/handler"
"notesApp/model"
"notesApp/service"
"github.com/labstack/echo/v4"
)
var dbService service.DbService
func init() {
dbService = service.NewDbService()
err := dbService.DB.AutoMigrate(&model.Note{})
if err != nil {
panic("failed to migrate database")
}
}
func main() {
app := echo.New()
app.Static("/static", "static")
noteService := service.NoteService{DbService: dbService}
noteHandler := handler.NoteHandler{NoteService: noteService}
basicHandler := handler.BasicHandler{NoteService: noteService}
app.GET("/", basicHandler.ShowHome)
app.GET("/write", basicHandler.ShowWrite)
app.GET("/notes/:id", noteHandler.ShowNote)
app.POST("/notes", noteHandler.CreateNote)
port := os.Getenv("GO_PORT")
app.Logger.Fatal(app.Start(":" + port))
}