Complete Guide: PostgreSQL & pgAdmin Docker Setup for Go Developers
Learn how to set up PostgreSQL 17 and pgAdmin in Docker containers while running your Go application locally. This comprehensive guide includes Docker Compose configuration, complete Go code examples with pgx/v5, connection pooling best practices, and a full Docker commands cheatsheet. Perfect for developers who want isolated, portable database environments without containerizing their entire application.
Complete Guide: PostgreSQL & pgAdmin Docker Setup for Go Developers
Table of Contents
- Overview
- Prerequisites
- Project Setup
- Understanding the Docker Architecture
- Configuration Files Explained
- [Docker Compose Configuration](#docker compose-configuration)
- Starting the Services
- Go Project Structure
- GORM Database Setup
- Building REST API with Gin
- Database Migrations
- Complete Implementation
- Essential Docker Commands
- Testing the API
- Best Practices
- Troubleshooting
Overview
This guide demonstrates how to build a production-ready REST API with all services containerized in Docker, including:
- Gin Gonic - High-performance HTTP web framework
- GORM - Developer-friendly ORM for Go
- PostgreSQL 16 - Database running in Docker
- pgAdmin 4 - Database management tool in Docker
- Your Go application - Also running in Docker for consistency
Why run Go in Docker?
Running your Go application in Docker alongside PostgreSQL solves several critical issues:
- Network connectivity: Containers on the same Docker network can communicate using service names (e.g.,
postgres) instead oflocalhost, eliminating connection issues - Environment consistency: Development environment matches production exactly
- Easy onboarding: New team members can start with a single
docker compose upcommand - Isolation: No conflicts with other projects or system services
- Portability: Works identically on Windows, macOS, and Linux
Prerequisites
Required Software:
- Docker & Docker Compose (Docker Desktop includes both)
- Go 1.24 or higher (for local development/testing)
- Your favorite code editor (VS Code, GoLand, etc.)
Verify installations:
docker --version
docker compose --version
go versionProject Setup
Step 1: Create Project Structure
mkdir go-tracker-api
cd go-tracker-apiYour final project structure:
go-tracker-api/
├── docker compose.yml # Orchestrates all services
├── Dockerfile.dev # Development Docker image
├── Dockerfile.prod # Production Docker image
├── .env # Environment variables
├── .gitignore # Files to exclude from Git
├── go.mod # Go module dependencies
├── go.sum # Dependency checksums
├── main.go # Application entry point
├── initializers/
│ ├── database.go # Database connection logic
│ └── loadEnvVariables.go # Environment loader
├── models/ # Data models
├── controllers/ # Request handlers
├── routes/ # API routes
├── middlewares/ # Custom middleware
├── scripts/
│ ├── migrate-gorm.go # Migration script
│ └── migrate.sh # Shell script to run migrations
└── postgres-data/ # PostgreSQL data (auto-created, gitignored)
Step 2: Create Configuration Files
Create .gitignore:
# .gitignore
postgres-data/
pgadmin-data/
.env
*.log
tmp/Why we need .gitignore:
postgres-data/contains database files - shouldn't be in version control.envcontains secrets - must never be committedtmp/contains temporary build files*.logfiles contain runtime logs
Understanding the Docker Architecture
When you run your application in Docker, the architecture looks like this:
┌─────────────────────────────────────────┐
│ Docker Network: app-network │
│ │
│ ┌──────────────┐ ┌─────────────────┐ │
│ │ pgAdmin │ │ PostgreSQL │ │
│ │ Port: 5050 │ │ Port: 5432 │ │
│ │ │ │ │ │
│ └──────────────┘ └─────────────────┘ │
│ │ ▲ │
│ │ │ │
│ └────────┬───────────┘ │
│ │ │
│ ┌────────▼──────────┐ │
│ │ Go App │ │
│ │ Port: 8080 │ │
│ │ (go-tracker-api)│ │
│ └───────────────────┘ │
│ │ │
└──────────────────┼──────────────────────┘
│
Host Port: 8080
(http://localhost:8080)
Key points:
- All containers are on the same Docker network (
app-network) - Containers communicate using service names:
postgres,pgadmin,app - Your Go app connects to PostgreSQL using
host=postgres(notlocalhost) - Only necessary ports are exposed to your host machine
Configuration Files Explained
1. .env File
Create .env file:
# .env
# PostgreSQL Configuration
POSTGRES_USER=postgres
POSTGRES_PASSWORD=titanic101
POSTGRES_DB=trackerDB
POSTGRES_PORT=5432
# pgAdmin Configuration
PGADMIN_EMAIL=admin@admin.com
PGADMIN_PASSWORD=Admin@2025
PGADMIN_PORT=5050
# Application
PORT=8080
APP_PORT=8080
GIN_MODE=debug
SECRET_KEY=w9hXDwVXGKnfK9k4kbPNAfxweEdPkc5uhYaU1S8qMNc=Environment Variables Explained:
| Variable | Purpose | Example Value |
|---|---|---|
POSTGRES_USER | PostgreSQL username | postgres |
POSTGRES_PASSWORD | PostgreSQL password (change in production!) | titanic101 |
POSTGRES_DB | Database name that will be created | trackerDB |
POSTGRES_PORT | Port to expose PostgreSQL on host | 5432 |
PGADMIN_EMAIL | Login email for pgAdmin web interface | admin@admin.com |
PGADMIN_PASSWORD | Login password for pgAdmin | Admin@2025 |
PGADMIN_PORT | Port to access pgAdmin web interface | 5050 |
PORT | Internal port Go app listens on | 8080 |
APP_PORT | External port mapped to host | 8080 |
GIN_MODE | Gin framework mode (debug or release) | debug |
SECRET_KEY | Secret key for JWT/sessions (generate unique key!) | Random string |
⚠️ Security Note: Always use strong, unique passwords in production and never commit .env to version control!
2. Dockerfile.dev - Development Container
Create Dockerfile.dev:
FROM golang:1.25-alpine
WORKDIR /app
# Copy go.mod and go.sum
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
EXPOSE 8080
# Use go run which will recompile on restart
CMD ["go", "run", "main.go"]Why we need this file:
- Defines how to build the Docker image for development
- Uses
go runfor quick iterations (no need to rebuild binary) - Lightweight Alpine Linux base image keeps container size small
WORKDIR /appsets working directory inside containerEXPOSE 8080documents which port the app uses (informational)CMD ["go", "run", "main.go"]runs the application
Line-by-line breakdown:
FROM golang:1.25-alpine: Base image with Go 1.25 on Alpine LinuxWORKDIR /app: All subsequent commands run from/appdirectoryCOPY go.mod go.sum ./: Copy dependency files first (Docker layer caching)RUN go mod download: Download dependencies (cached if files don't change)COPY . .: Copy all source code into containerEXPOSE 8080: Metadata - doesn't actually open portCMD ["go", "run", "main.go"]: Command to run when container starts
3. Dockerfile.prod - Production Container
Create Dockerfile.prod:
# Build stage
FROM golang:1.25-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main .
# Run stage
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]Why we need this file:
- Multi-stage build creates smaller production images
- First stage compiles the Go binary
- Second stage only includes the compiled binary (no Go toolchain)
- Results in ~10-15MB image vs ~300MB+ with full Go environment
CGO_ENABLED=0creates static binary with no external dependenciesca-certificatesneeded for HTTPS requests
Production vs Development:
- Development: Fast startup, easy debugging, larger image
- Production: Optimized size, faster deployment, more secure
4. docker compose.yml - Service Orchestration
Create docker compose.yml:
services:
postgres:
image: postgres:16-alpine
container_name: postgres-traceable-db
restart: unless-stopped
env_file:
- .env
environment:
POSTGRES_HOST_AUTH_METHOD: md5
ports:
- "${POSTGRES_PORT:-5432}:5432"
volumes:
- ./postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d trackerDB"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
pgadmin:
image: dpage/pgadmin4:latest
container_name: pgadmin
restart: unless-stopped
env_file:
- .env
environment:
PGADMIN_CONFIG_SERVER_MODE: "False"
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: "False"
ports:
- "${PGADMIN_PORT:-5050}:80"
volumes:
- pgadmin-data:/var/lib/pgadmin
depends_on:
postgres:
condition: service_healthy
networks:
- app-network
app:
build:
context: .
dockerfile: Dockerfile.dev
container_name: go-tracker-api
env_file:
- .env
environment:
POSTGRES_HOST: postgres # This is crucial! Use service name, not localhost
ports:
- "${APP_PORT:-8080}:8080"
volumes:
- .:/app # Mount source code for hot reload on restart
depends_on:
postgres:
condition: service_healthy
networks:
- app-network
networks:
app-network:
driver: bridge
volumes:
pgadmin-data:Docker Compose Settings Explained:
PostgreSQL Service:
image: postgres:16-alpine: Official PostgreSQL 16 on Alpine Linuxcontainer_name: Custom name for easy referencerestart: unless-stopped: Auto-restart on failure (except manual stops)env_file: Load variables from.envfilePOSTGRES_HOST_AUTH_METHOD: md5: Password authentication methodports: Maps container port 5432 to host port 5432volumes: Persists database data to./postgres-datadirectoryhealthcheck: Ensures PostgreSQL is ready before starting dependent services- Runs
pg_isreadycommand every 10 seconds - Waits 5 seconds for response
- Retries 5 times before marking unhealthy
- Runs
networks: Joins theapp-networkfor inter-container communication
pgAdmin Service:
depends_onwithcondition: service_healthy: Waits for PostgreSQL to be healthyPGADMIN_CONFIG_SERVER_MODE: "False": Runs in desktop mode (no login required for initial setup)- Named volume
pgadmin-data: Persists pgAdmin configuration
App Service:
build: Builds fromDockerfile.devin current directoryenvironment: POSTGRES_HOST: postgres: Critical! Overrides.envto use Docker service namevolumes: - .:/app: Mounts current directory into container at/app- Changes to code on host immediately reflect in container
- Restart container to see changes:
docker compose restart app
Networks:
app-network: Custom bridge network allows containers to find each other by service name- Without this, containers couldn't communicate
Volumes:
- Named volumes (
pgadmin-data) are managed by Docker - Bind mounts (
./postgres-data) use host filesystem
Starting the Services
Essential Docker Commands
These commands are your daily workflow tools. Understanding them is crucial:
1. Stop Current Containers
docker compose downWhat this does:
- Stops all running containers defined in
docker compose.yml - Removes the containers (but keeps volumes/data)
- Removes the network
- Does NOT delete: Database data, images, or volumes
When to use:
- Before making changes to
docker compose.yml - When switching between projects
- Before running
docker compose upagain
2. Remove PostgreSQL Data Directory
rm -rf postgres-dataWhat this does:
- Deletes all PostgreSQL database files
- Forces a fresh database on next startup
- Warning: This is destructive! All data is lost!
When to use:
- Database is corrupted
- Need to reset database to clean state
- Testing migrations from scratch
- Switching between major PostgreSQL versions
Windows equivalent:
rmdir /s /q postgres-data3. Start Services with Build
docker compose up --build -dWhat this does:
up: Starts all services--build: Rebuilds images even if they exist (ensures latest code)-d: Detached mode (runs in background)
When to use:
- After changing Dockerfile
- After
git pullwith new dependencies - First time starting project
- When you suspect image is outdated
Without --build:
docker compose up -d # Uses cached images4. Watch Logs
docker compose logs -f appWhat this does:
logs: Shows stdout/stderr from containers-f: Follow mode (liketail -f), shows new logs in real-timeapp: Only show logs fromappservice
Variations:
# All services
docker compose logs -f
# Specific service
docker compose logs -f postgres
# Last 100 lines
docker compose logs --tail=100 app
# Since specific time
docker compose logs --since 2025-01-01T00:00:00 appWhen to use:
- Debugging startup issues
- Monitoring application output
- Watching for errors
- Checking database connection status
5. Restart After Code Changes
docker compose restart appWhat this does:
- Stops the
appcontainer - Starts it again
- Preserves volumes and network
- Much faster than
down+up
Why this works:
- Source code is mounted via volume (
.:/app) - Container sees file changes immediately
- Restart picks up changes (since we use
go run)
When to use:
- After editing Go files
- After changing
.envvariables - When application is stuck
- Testing configuration changes
Alternative for all services:
docker compose restart6. Complete Workflow Example
# Clean start (fresh database)
docker compose down
rm -rf postgres-data
docker compose up --build -d
docker compose logs -f app
# Make code changes, then:
docker compose restart app
# Check if services are running
docker compose ps
# Stop everything when done
docker compose downStep 1: Initial Startup
# Start all services
docker compose up --build -d
# Verify containers are running
docker compose ps
# Check logs
docker compose logs -fExpected output:
✔ Container postgres-traceable-db Healthy
✔ Container go-tracker-api Started
✔ Container pgadmin Started
If you see errors:
# Check specific service logs
docker compose logs postgres
docker compose logs app
# Common fix: Remove old data
docker compose down
rm -rf postgres-data
docker compose up --build -dStep 2: Access pgAdmin
Open browser: http://localhost:5050
Login with:
- Email:
admin@admin.com(from.env) - Password:
Admin@2025(from.env)
Add PostgreSQL Server:
- Click "Add New Server"
- General Tab:
- Name =
Tracker Database(any name you want)
- Name =
- Connection Tab:
- Host:
postgres(NOTlocalhost! - this is the Docker service name) - Port:
5432 - Maintenance database:
postgres - Username:
postgres - Password:
titanic101
- Host:
- Check "Save password"
- Click "Save"
Why postgres instead of localhost?
- pgAdmin runs inside Docker
- Inside Docker,
localhostrefers to the pgAdmin container itself postgresis the service name Docker DNS resolves to the PostgreSQL container
Go Project Structure
Step 1: Initialize Go Module
go mod init github.com/yourusername/go-tracker-apiStep 2: Install Dependencies
go get -u github.com/gin-gonic/gin
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
go get -u github.com/joho/godotenvGORM Database Setup
Create initializers/database.go:
package initializers
import (
"fmt"
"log"
"os"
"time"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
var DB *gorm.DB
func ConnectToDB() {
var err error
// Build DSN from environment variables
dsn := fmt.Sprintf(
"host=%s user=%s password=%s dbname=%s port=%s sslmode=disable",
getEnv("POSTGRES_HOST", "localhost"),
getEnv("POSTGRES_USER", "postgres"),
getEnv("POSTGRES_PASSWORD", "titanic101"),
getEnv("POSTGRES_DB", "trackerDB"),
getEnv("POSTGRES_PORT", "5432"),
)
log.Printf("Connecting to: host=%s db=%s",
getEnv("POSTGRES_HOST", "localhost"),
getEnv("POSTGRES_DB", "trackerDB"))
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("Failed to connect to the DB:", err)
}
// Test the connection
sqlDB, err := DB.DB()
if err != nil {
log.Fatal("Failed to get DB instance:", err)
}
// Configure connection pool
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour)
if err := sqlDB.Ping(); err != nil {
log.Fatal("Failed to ping database:", err)
}
fmt.Println("✅ Successfully connected to database!")
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}Create initializers/loadEnvVariables.go:
package initializers
import (
"fmt"
"log"
"os"
"github.com/joho/godotenv"
)
func LoadEnvVariables() {
err := godotenv.Load()
if err != nil {
log.Println("No .env file found, using environment variables from system")
}
// Verify we have the required environment variables
fmt.Printf("POSTGRES_HOST: '%s'\n", os.Getenv("POSTGRES_HOST"))
fmt.Printf("POSTGRES_USER: '%s'\n", os.Getenv("POSTGRES_USER"))
fmt.Printf("POSTGRES_PASSWORD: '%s'\n", os.Getenv("POSTGRES_PASSWORD"))
fmt.Printf("POSTGRES_DB: '%s'\n", os.Getenv("POSTGRES_DB"))
}Why separate these functions?
LoadEnvVariables(): Loads.envfile or uses Docker environment variablesConnectToDB(): Establishes database connection with proper error handling- Separation of concerns makes code more maintainable and testable
Database Migrations
Why We Need Migrations
Migrations manage database schema changes over time:
- Create/modify tables
- Add/remove columns
- Create indexes
- Seed initial data
Create scripts/migrate-gorm.go:
package main
import (
"fmt"
"log"
"github.com/MUKE-coder/go-tracker-api/initializers"
"github.com/MUKE-coder/go-tracker-api/models"
)
func main() {
// Load environment variables
initializers.LoadEnvVariables()
// Connect to database
initializers.ConnectToDB()
// Auto migrate all models
err := initializers.DB.AutoMigrate(
&models.User{},
&models.Customer{},
&models.Supplier{},
&models.Farmer{},
&models.Order{},
&models.Batch{},
&models.BatchFarmer{},
&models.BatchSupplier{},
&models.TraceabilityReport{},
)
if err != nil {
log.Fatal("Migration failed:", err)
}
fmt.Println("✅ Database migration completed successfully!")
}Why we need this file:
- Separate executable for running migrations
- Can be run independently of main application
- Useful for CI/CD pipelines
- GORM's
AutoMigratecreates/updates tables based on struct definitions
Create scripts/migrate.sh:
#!/bin/bash
echo "Running database migrations..."
docker exec go-tracker-api go run ./scripts/migrate-gorm.goWhy we need this file:
- Shell script wrapper for easy execution
- Runs migration inside the Docker container (where database connection works)
docker execexecutes command in running container- Easier than typing full docker exec command each time
Make Script Executable:
chmod +x scripts/migrate.shWhat chmod +x does:
- Adds execute permission to the file
- Without this, you'd get "Permission denied" error
- Only needed once (permission is preserved in Git)
Run Migrations:
./scripts/migrate.shExpected output:
Running database migrations...
POSTGRES_HOST: 'postgres'
POSTGRES_USER: 'postgres'
POSTGRES_PASSWORD: 'titanic101'
POSTGRES_DB: 'trackerDB'
Connecting to: host=postgres db=trackerDB
✅ Successfully connected to database!
✅ Database migration completed successfully!
When to run migrations:
- After adding new models
- After changing model fields
- After pulling code with model changes
- When setting up fresh database
Complete Implementation
Create main.go:
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
"github.com/MUKE-coder/go-tracker-api/initializers"
"github.com/MUKE-coder/go-tracker-api/models"
"github.com/MUKE-coder/go-tracker-api/routes"
)
func init() {
initializers.LoadEnvVariables()
initializers.ConnectToDB()
}
func main() {
// Auto-migrate database schema
if err := initializers.DB.AutoMigrate(
&models.User{},
&models.Customer{},
&models.Supplier{},
&models.Farmer{},
&models.Order{},
&models.Batch{},
&models.BatchFarmer{},
); err != nil {
log.Fatalf("Failed to migrate database: %v", err)
}
log.Println("✅ Database migration completed")
// Set Gin mode
ginMode := os.Getenv("GIN_MODE")
if ginMode == "" {
ginMode = gin.DebugMode
}
gin.SetMode(ginMode)
// Create Gin router
router := gin.Default()
// Setup routes
routes.SetupRoutes(router)
// Get port from environment
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
// Create HTTP server
srv := &http.Server{
Addr: fmt.Sprintf(":%s", port),
Handler: router,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20, // 1 MB
}
// Start server in a goroutine
go func() {
log.Printf("🚀 Server starting on port %s", port)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Failed to start server: %v", err)
}
}()
// Wait for interrupt signal for graceful shutdown
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("🛑 Shutting down server...")
// Graceful shutdown with 5 second timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("Server forced to shutdown: %v", err)
}
log.Println("✅ Server exited")
}Essential Docker Commands
Daily Workflow Commands
# 1. Clean start (fresh database)
docker compose down # Stop all services
rm -rf postgres-data # Delete database files
docker compose up --build -d # Start with fresh build
# 2. Watch logs (separate terminal recommended)
docker compose logs -f app # Follow app logs
docker compose logs -f # Follow all logs
# 3. Make code changes, then restart
docker compose restart app # Quick restart to see changes
# 4. Check service status
docker compose ps # List running containers
# 5. Stop everything
docker compose down # Clean shutdownWhy Each Command Matters
docker compose down
- Stops containers gracefully
- Removes containers (but preserves volumes)
- Clean slate for next startup
- Use before structural changes
rm -rf postgres-data
- Nuclear option for database issues
- Forces PostgreSQL to initialize fresh
- Solves corruption issues
- Warning: Deletes all data!
docker compose up --build -d
--build: Rebuilds images (includes code changes)-d: Runs in background (detached mode)- Without
--build, uses cached images - Essential after code changes
docker compose logs -f app
- Real-time log viewing
-ffollows new output (liketail -f)- Filter by service name (
app,postgres) - Critical for debugging
docker compose restart app
- Fastest way to see code changes
- No rebuild needed (volume mounted)
- Preserves network and database connection
- Use after editing Go files
chmod +x scripts/migrate.sh
- Makes shell scripts executable
- Unix/Linux/macOS requirement
- Only needed once per file
- Prevents "Permission denied" errors
./scripts/migrate.sh
- Runs migration script
./means current directory- Executes inside running container
- Safe way to update database schema
Debugging Commands
# Check if containers are running
docker compose ps
# Inspect specific service
docker logs go-tracker-api
docker logs postgres-traceable-db
# Enter container shell
docker exec -it go-tracker-api sh
docker exec -it postgres-traceable-db sh
# Check database directly
docker exec -it postgres-traceable-db psql -U postgres -d trackerDB
# View resource usage
docker stats
# Remove everything (including volumes)
docker compose down -v # DANGER: Deletes data!PostgreSQL Specific Commands
# Connect to PostgreSQL
docker exec -it postgres-traceable-db psql -U postgres -d trackerDB
# Inside psql:
\dt # List tables
\d users # Describe users table
\l # List databases
\q # Quit
# Run SQL from host
docker exec -it postgres-traceable-db psql -U postgres -d trackerDB -c "SELECT * FROM users;"
# Backup database
docker exec postgres-traceable-db pg_dump -U postgres trackerDB > backup.sql
# Restore database
docker exec -i postgres-traceable-db psql -U postgres trackerDB < backup.sqlTesting the API
Using cURL
# Health check
curl http://localhost:8080/health
# Create user
curl -X POST http://localhost:8080/api/v1/users \
-H "Content-Type: application/json" \
-d '{
"name": "John Doe",
"email": "john@example.com"
}'
# Get all users
curl http://localhost:8080/api/v1/users
# Get single user
curl http://localhost:8080/api/v1/users/1
# Update user
curl -X PUT http://localhost:8080/api/v1/users/1 \
-H "Content-Type: application/json" \
-d '{"name": "Jane Doe"}'
# Delete user
curl -X DELETE http://localhost:8080/api/v1/users/1Best Practices
1. Use Strong Passwords
Never use default passwords in production:
# Generate secure password
openssl rand -base64 32
# Use in .env
POSTGRES_PASSWORD=<generated-password>2. Separate Development and Production Configs
# .env.development
GIN_MODE=debug
POSTGRES_PASSWORD=development_password
# .env.production
GIN_MODE=release
POSTGRES_PASSWORD=<strong-password>3. Regular Backups
# Backup script
#!/bin/bash
DATE=$(date +%Y%m%d_%H%M%S)
docker exec postgres-traceable-db pg_dump -U postgres trackerDB > backup_$DATE.sql4. Monitor Logs
# Keep logs organized by date
docker compose logs --tail=1000 app > logs_$(date +%Y%m%d).log5. Health Checks
Always implement health check endpoints:
router.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{
"status": "ok",
"database": "connected",
"timestamp": time.Now(),
})
})6. Environment-Specific Configuration
Use different Docker Compose files:
# Development
docker compose -f docker compose.dev.yml up
# Production
docker compose -f docker compose.prod.yml upTroubleshooting
Problem 1: "Connection refused" to PostgreSQL
Symptoms:
Failed to connect to database: dial tcp 127.0.0.1:5432: connect: connection refused
Solution:
# 1. Check if PostgreSQL container is running
docker compose ps
# 2. Check PostgreSQL logs
docker compose logs postgres
# 3. Ensure you're using 'postgres' as host, not 'localhost'
# In docker compose.yml, verify:
environment:
POSTGRES_HOST: postgres
# 4. Restart services
docker compose restartWhy this happens:
- Using
localhostinstead ofpostgresservice name - PostgreSQL container not healthy yet
- Network configuration issues
Problem 2: "Database is unhealthy"
Symptoms:
Container postgres-traceable-db is unhealthy
dependency failed to start
Solution:
# 1. Check logs
docker logs postgres-traceable-db
# 2. Remove corrupted data
docker compose down
rm -rf postgres-data
# 3. Fresh start
docker compose up -d
# 4. If still failing, check disk space
df -hCommon causes:
- Corrupted database files
- Insufficient disk space
- Port 5432 already in use
- Incorrect credentials in healthcheck
Problem 3: "Permission denied" running migrate.sh
Symptoms:
bash: ./scripts/migrate.sh: Permission denied
Solution:
# Make script executable
chmod +x scripts/migrate.sh
# Verify permissions
ls -la scripts/migrate.sh
# Should show: -rwxr-xr-xProblem 4: Changes not reflecting after restart
Symptoms:
- Code changes don't appear after
docker compose restart app
Solution:
# 1. Verify volume mount in docker compose.yml
volumes:
- .:/app # This must be present
# 2. Rebuild if Dockerfile changed
docker compose up --build -d
# 3. Check if file is actually changed inside container
docker exec go-tracker-api cat main.go | head -20Problem 5: "Port already in use"
Symptoms:
Error starting userland proxy: listen tcp 0.0.0.0:8080: bind: address already in use
Solution:
# Find what's using the port (macOS/Linux)
lsof -i :8080
# Windows
netstat -ano | findstr :8080
# Kill the process or change port in .env
APP_PORT=8081Problem 6: Container keeps restarting
Symptoms:
docker compose ps
# Shows container with status "Restarting"
Solution:
# Check logs for error
docker compose logs --tail=50 app
# Common issues:
# - Syntax error in Go code
# - Missing environment variables
# - Database connection failing
# Fix code and rebuild
docker compose up --build -dProblem 7: "No .env file found" but variables still empty
Symptoms:
POSTGRES_HOST: ''
POSTGRES_USER: ''
Solution:
# 1. Verify .env exists
ls -la .env
# 2. Check docker compose.yml has env_file
env_file:
- .env
# 3. Restart services after .env changes
docker compose down
docker compose up -d
# 4. Verify variables inside container
docker exec go-tracker-api env | grep POSTGRESProblem 8: Migration fails
Symptoms:
Migration failed: relation "users" already exists
Solution:
# Option 1: Drop and recreate (development only!)
docker exec -it postgres-traceable-db psql -U postgres -d trackerDB -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
# Option 2: Let GORM handle it (safer)
# GORM's AutoMigrate is idempotent - it won't fail on existing tables
# Option 3: Fresh database
docker compose down
rm -rf postgres-data
docker compose up -d
./scripts/migrate.shProblem 9: pgAdmin can't connect to PostgreSQL
Symptoms:
- pgAdmin shows "Unable to connect to server"
Solution:
Connection settings must be:
- Host:
postgres(NOTlocalhost, NOTpostgres-traceable-db) - Port:
5432 - Username: From
.envfile - Password: From
.envfile - Database:
trackerDB(or your database name)
Why:
- pgAdmin runs in Docker, so it uses internal network
- Service name is
postgresas defined in docker compose.yml - Container name (
postgres-traceable-db) doesn't work for DNS
Problem 10: Docker out of disk space
Symptoms:
no space left on device
Solution:
# Check disk usage
docker system df
# Clean up unused resources
docker system prune -a
# Remove specific items
docker volume prune
docker image prune -a
# Nuclear option (careful!)
docker system prune -a --volumesAdvanced Tips
Hot Reload Without Manual Restart
While the current setup requires manual restart, you can add Air for automatic reload:
Update Dockerfile.dev:
FROM golang:1.25-alpine
WORKDIR /app
# Install Air
RUN go install github.com/air-verse/air@latest
COPY go.mod go.sum ./
RUN go mod download
COPY . .
EXPOSE 8080
CMD ["air", "-c", ".air.toml"]Create .air.toml:
root = "."
tmp_dir = "tmp"
[build]
cmd = "go build -o ./tmp/main ."
bin = "tmp/main"
include_ext = ["go", "tpl", "tmpl", "html"]
exclude_dir = ["assets", "tmp", "vendor", "postgres-data"]
delay = 1000Now changes auto-reload without manual restart!
Database Connection Pooling
Already configured in database.go, but here's why each setting matters:
sqlDB.SetMaxIdleConns(10) // Keep 10 connections ready
sqlDB.SetMaxOpenConns(100) // Max 100 concurrent connections
sqlDB.SetConnMaxLifetime(time.Hour) // Recycle connections after 1 hourTuning guidelines:
- Low traffic: MaxIdleConns=5, MaxOpenConns=25
- Medium traffic: MaxIdleConns=10, MaxOpenConns=100
- High traffic: MaxIdleConns=20, MaxOpenConns=200
Multiple Environments
Development:
docker compose -f docker compose.yml upProduction:
docker compose -f docker compose.prod.yml upTesting:
docker compose -f docker compose.test.yml upDebugging Inside Container
# Enter container shell
docker exec -it go-tracker-api sh
# Check environment variables
env | grep POSTGRES
# Test database connection
apk add postgresql-client
psql -h postgres -U postgres -d trackerDB
# Check file contents
cat main.go
# Check running processes
ps auxProduction Deployment Checklist
Before deploying to production:
- Change all default passwords in
.env - Set
GIN_MODE=release - Use
Dockerfile.prodfor smaller images - Enable SSL/TLS (
sslmode=require) - Set up automatic backups
- Configure log rotation
- Set up monitoring (Prometheus, Grafana)
- Use secrets management (Vault, AWS Secrets Manager)
- Configure firewall rules
- Set up CI/CD pipeline
- Add rate limiting
- Enable CORS properly
- Add authentication/authorization
- Set up load balancer
- Configure auto-scaling
Quick Reference Card
Everyday Commands
# Start project
docker compose up -d
# Stop project
docker compose down
# View logs
docker compose logs -f app
# Restart after changes
docker compose restart app
# Run migrations
./scripts/migrate.sh
# Fresh start (deletes data!)
docker compose down && rm -rf postgres-data && docker compose up --build -dWhen Things Break
# Check what's running
docker compose ps
# Check logs
docker compose logs postgres
docker compose logs app
# Full restart
docker compose restart
# Nuclear option
docker compose down -v
rm -rf postgres-data
docker compose up --build -dConclusion
You now have a complete Docker-based development environment with:
- PostgreSQL 16 running in an isolated container
- pgAdmin for visual database management
- Your Go application containerized for consistency
- Automatic networking between containers
- Persistent data via Docker volumes
- Easy migrations with shell scripts
- Quick restart workflow for development
Key Takeaways
- Docker containers communicate by service name, not localhost
- Volume mounts let you edit code and restart to see changes
docker compose down && rm -rf postgres-datafixes most database issuesdocker compose restart appis your friend during development- Always use
.envfiles for configuration, never hardcode
Next Steps
- Add authentication with JWT tokens
- Implement role-based access control
- Set up automated testing
- Add API documentation with Swagger
- Configure CI/CD pipeline
- Implement caching with Redis
- Add monitoring and alerting
- Set up staging environment
Additional Resources
- Docker Documentation: https://docs.docker.com/
- Docker Compose Reference: https://docs.docker.com/compose/
- Gin Framework: https://gin-gonic.com/docs/
- GORM Documentation: https://gorm.io/docs/
- PostgreSQL Docs: https://www.postgresql.org/docs/
- Go Best Practices: https://go.dev/doc/effective_go
Need help? Check logs first with docker compose logs -f, then review the Troubleshooting section. Most issues are solved by docker compose down, rm -rf postgres-data, and docker compose up --build -d.
Happy coding!

