Contents

Building Infrastructure with Docker — Part 1: PostgreSQL

🗄️ Building Infrastructure with Docker — Part 1

PostgreSQL: A Reusable, Independent Database Module

Welcome to Part 1 of the Building Infrastructure with Docker series. In Part-0, we set the foundation: one mono-repo, multiple fully independent modules, each engineered with deterministic behaviour, pinned container images, self-contained docs, health checks, and Makefile-driven execution.

We now begin the first real module in the series — PostgreSQL.

This module is intentionally simple but extremely important: every other module in this series that interacts with a relational database or requires CDC (Change Data Capture using Debezium later) will rely on the principles we establish here.

Our goal is not to “run PostgreSQL in Docker.” Our goal is to engineer a reusable infrastructure component you can plug into any project.


🎯 What We Are Building in Part 1

This module provides a standalone PostgreSQL instance inside the repository under:

infra-docker-series/modules/postgres/

Once built, this module:

  • Runs independently using make up
  • Provides health checks using pg_isready
  • Stores data consistently under docker-volume/data/
  • Uses pinned Docker image versions (no latest)
  • Exposes a simple smoke test (SELECT 1)
  • Can be reused across any other project you build

If you learn the pattern here, every future module will feel natural.


1. 🧱 Folder Structure — Postgres Module

Inside the mono-repo, the PostgreSQL module lives here:

modules/
  postgres/
    docs/
      README.md
      requirements.md
      design-intent.md
      diagrams/
    infra/
      docker-compose.yml
    docker-volume/
      data/
    scripts/
      test_health.sh
    .env.example
    Makefile
    Jenkinsfile

This layout is the canonical scaffold for the entire series. Kafka, Mosquitto, Keycloak, Prometheus, and all future parts will follow this identical pattern.


🧩 The Architecture (Simple but as-is)

  • Data is bind-mounted locally (not named volumes)
  • A dedicated Docker network postgres_net isolates traffic
  • Everything is driven by environment variables loaded from .env

This keeps it production-minded while still very easy to work with.

2. PostgreSQL module files

All paths below are relative to: infra-docker-series/modules/postgres/


2.1 modules/postgres/.env.example

# Postgres container settings
POSTGRES_CONTAINER_NAME=infra_postgres
POSTGRES_DB=app_db
POSTGRES_USER=app_user
POSTGRES_PASSWORD=change_me_locally
POSTGRES_PORT=5432

# Image version pin
POSTGRES_VERSION=15

You will copy this to .env manually and adjust POSTGRES_PASSWORD etc. .env should be gitignored (already covered by root .gitignore).


2.2 modules/postgres/infra/docker-compose.yml

version: "3.9"

services:
  postgres:
    image: postgres:${POSTGRES_VERSION}
    container_name: ${POSTGRES_CONTAINER_NAME}
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      PGDATA: /var/lib/postgresql/data/pgdata
    ports:
      - "${POSTGRES_PORT}:5432"
    volumes:
      - ../docker-volume/data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 15s
    networks:
      - postgres_net

networks:
  postgres_net:
    name: postgres_net
    driver: bridge

Notes:

  • Volume path ../docker-volume/data is relative to infra/, mapping to docker-volume/data at module root.
  • Image pinned via POSTGRES_VERSION env, defaulting to 15.

2.3 modules/postgres/Makefile (module-level)

# PostgreSQL module Makefile

COMPOSE_FILE := infra/docker-compose.yml
DOCKER_COMPOSE := docker compose -f $(COMPOSE_FILE)
ENV_FILE ?= .env

.PHONY: help init up down logs test clean psql

help:
	@echo "PostgreSQL module"
	@echo "Targets:"
	@echo "  init  - prepare env and folders"
	@echo "  up    - start postgres container"
	@echo "  down  - stop postgres container"
	@echo "  logs  - tail container logs"
	@echo "  test  - run health/smoke test"
	@echo "  clean - stop and remove data after confirmation"
	@echo "  psql  - open psql shell into the database"

init:
	@if [ ! -f "$(ENV_FILE)" ]; then \
	  echo "Creating $(ENV_FILE) from .env.example"; \
	  cp .env.example $(ENV_FILE); \
	else \
	  echo "$(ENV_FILE) already exists, skipping copy."; \
	fi
	@mkdir -p docker-volume/data
	@echo "Pulling images..."
	@$(DOCKER_COMPOSE) --env-file $(ENV_FILE) pull

up:
	@$(DOCKER_COMPOSE) --env-file $(ENV_FILE) up -d

down:
	@$(DOCKER_COMPOSE) --env-file $(ENV_FILE) down

logs:
	@$(DOCKER_COMPOSE) --env-file $(ENV_FILE) logs -f

test:
	@./scripts/test_health.sh $(ENV_FILE)

clean:
	@read -p "This will stop the container and DELETE docker-volume/data. Continue? (y/N) " ans; \
	if [ "$$ans" = "y" ] || [ "$$ans" = "Y" ]; then \
	  $(DOCKER_COMPOSE) --env-file $(ENV_FILE) down; \
	  rm -rf docker-volume/data; \
	  echo "Data directory removed."; \
	else \
	  echo "Aborted."; \
	fi

psql:
	@./scripts/test_health.sh $(ENV_FILE) --psql

2.4 modules/postgres/scripts/test_health.sh

#!/usr/bin/env bash
set -euo pipefail

ENV_FILE="${1:-.env}"
MODE="${2:-health}"

if [ ! -f "$ENV_FILE" ]; then
  echo "Env file '$ENV_FILE' not found. Run 'make init' first."
  exit 1
fi

# shellcheck disable=SC2046
export $(grep -v '^\s*#' "$ENV_FILE" | xargs)

CONTAINER_NAME="${POSTGRES_CONTAINER_NAME:-infra_postgres}"

echo "Using container: ${CONTAINER_NAME}"
echo "Checking health status..."

HEALTH_STATUS=$(docker inspect \
  --format='{{.State.Health.Status}}' \
  "${CONTAINER_NAME}" 2>/dev/null || echo "not_found")

if [ "$HEALTH_STATUS" != "healthy" ]; then
  echo "Container health status: ${HEALTH_STATUS}"
  echo "Hint: check logs with 'make logs' or 'docker logs ${CONTAINER_NAME}'"
  exit 1
fi

echo "Container is healthy."

if [ "$MODE" = "--psql" ]; then
  echo "Opening psql shell..."
  docker exec -it "${CONTAINER_NAME}" \
    psql -U "${POSTGRES_USER}" -d "${POSTGRES_DB}"
  exit 0
fi

echo "Running smoke test: SELECT 1;"

docker exec -i "${CONTAINER_NAME}" \
  psql -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" \
  -c "SELECT 1;" >/dev/null

echo "Smoke test passed."

Make sure to chmod +x modules/postgres/scripts/test_health.sh.


2.5 modules/postgres/docs/README.md

# PostgreSQL Infrastructure Module

This module provides a reusable PostgreSQL instance running in Docker with:

- Pinned PostgreSQL version (configured via `.env`).
- Local bind-mounted storage under `docker-volume/data`.
- Healthcheck configuration.
- Makefile-driven lifecycle (`init`, `up`, `test`, `down`, `clean`).
- Simple smoke test using `psql` (`SELECT 1`).

## Quick Start

```bash
# From repo root
cd modules/postgres

# Prepare env and pull images
make init

# Start PostgreSQL
make up

# Run health and smoke test
make test

# Optional: open psql shell into the DB
make psql

# Stop PostgreSQL
make down

Data is stored under modules/postgres/docker-volume/data and persists across restarts. Use make clean to remove it after confirmation.

2.6 modules/postgres/docs/requirements.md

# PostgreSQL Module – Requirements

## Business / Functional Intent

- Provide a standalone PostgreSQL instance that can be:
  - Started and stopped via Makefile and Docker Compose.
  - Reused across multiple application projects.
  - Used as the source database for future modules (e.g., Debezium CDC).

- Keep the module self-contained so it can be cloned and used without the rest of the series.

## Non-Functional Requirements

- Deterministic setup:
  - Explicit PostgreSQL image version (no `latest`).
  - Single, documented port with override capability via `.env`.

- Persistence:
  - Data stored under `docker-volume/data` inside the module.
  - Safe cleanup via `make clean` only after explicit confirmation.

- Observability:
  - Docker healthcheck using `pg_isready`.
  - Smoke test using `psql` (`SELECT 1`).

- Security:
  - Credentials defined in `.env` (not committed).
  - `.env.example` provided for reference.

- Portability:
  - No external dependencies beyond Docker and Docker Compose.
  - No hard-coded host paths; everything relative to the module root.

2.7 modules/postgres/docs/design-intent.md

# PostgreSQL Module – Design Intent

## Overview

This module provides a single PostgreSQL instance intended as:

- A reusable DB for local development.
- A base for CDC experiments (e.g., with Debezium in another module).
- A template for how other infrastructure modules in this series are structured.

## Architecture

```mermaid
flowchart LR
    dev[Developer] --> docker[Docker Engine]
    docker --> compose[Docker Compose]
    compose --> pg[PostgreSQL Container]
    pg --> vol[docker-volume data]
  • docker-compose.yml defines a single postgres service.
  • Data directory is bind-mounted from docker-volume/data.
  • A dedicated Docker bridge network postgres_net isolates traffic.

2.8 modules/postgres/Jenkinsfile (skeleton)

pipeline {
    agent any

    environment {
        MODULE_DIR = "modules/postgres"
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Init') {
            steps {
                dir(MODULE_DIR) {
                    sh 'make init'
                }
            }
        }

        stage('Up') {
            steps {
                dir(MODULE_DIR) {
                    sh 'make up'
                }
            }
        }

        stage('Test') {
            steps {
                dir(MODULE_DIR) {
                    sh 'make test'
                }
            }
        }

        stage('Down') {
            steps {
                dir(MODULE_DIR) {
                    sh 'make down'
                }
            }
        }
    }

    post {
        always {
            script {
                try {
                    dir(MODULE_DIR) {
                        sh 'make down || true'
                    }
                } catch (err) {
                    echo "Cleanup failed: ${err}"
                }
            }
        }
    }
}

This is just a reference Jenkins skeleton file, not yet complete. leaving Jenkins out of scope for this series.

3.⚙️ Core Configuration

The PostgreSQL behaviour is driven by .env.example, which you copy into .env:

POSTGRES_DB=app_db
POSTGRES_USER=app_user
POSTGRES_PASSWORD=change_me_locally
POSTGRES_PORT=5432
POSTGRES_VERSION=15

Why this matters:

  • You can change the port without editing the compose file.
  • You can rotate credentials without committing secrets.
  • You can upgrade PostgreSQL by adjusting a single version variable.

This reflects real-world configuration hygiene.


4. 🐘 docker-compose.yml — Pinned, Predictable, Safe

The module’s docker-compose.yml pins PostgreSQL to a specific version:

image: postgres:${POSTGRES_VERSION}

and sets a solid health check:

healthcheck:
  test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
  interval: 10s
  timeout: 5s
  retries: 5
  start_period: 15s

The container is only considered ready when PostgreSQL is truly accepting connections.

This tiny decision prevents unpredictable behavior in downstream systems—something every engineer learns the hard way at least once.


5. 🔧 Makefile-Driven Lifecycle

Every module in this series uses the same Makefile contract:

Command What It Does
make init Creates .env, prepares folders, pulls images
make up Starts the PostgreSQL container
make test Health check + smoke test (SELECT 1)
make down Stops the service
make clean Stops service + removes data (with confirmation)
make psql Opens an interactive shell inside PostgreSQL

You can run it inside the module:

cd modules/postgres
make init
make up
make test
make psql
make down

Or from the root of the repo:

make init MODULE=postgres
make up MODULE=postgres
make test MODULE=postgres

This uniform experience across all modules is the core training aspect of this series.


6. 🩺 Smoke Test: Trust, but Verify

After make up, you run:

make test

This executes test_health.sh which:

  1. Checks Docker health status → must be healthy
  2. Runs:
SELECT 1;

via psql inside the container.

If both succeed, your PostgreSQL module is considered operational.

If either fails, logs are suggested:

make logs

This one minute test ensures your infra module isn’t just “running”… …it’s actually working.


7. 💾 Persistence Strategy

Your data lives here:

modules/postgres/docker-volume/data/

This folder:

  • is bind-mounted (not named volume)
  • is owned by this module only
  • is always safe to delete manually if needed
  • is ignored from Git by default

This approach:

  • Lets you snapshot/copy/backup easily
  • Ensures no global Docker volumes pollute your system
  • Keeps module state self-contained

Exactly how enterprise infra-as-code teams work.


8. 🧪 Real-World Deployment Notes (Quick)

Even though this module runs locally:

  • The same Docker Compose file becomes your base for Kubernetes StatefulSets.
  • .env values map to Kubernetes Secrets and ConfigMaps.
  • docker-volume/data maps to PersistentVolumeClaims.
  • Healthchecks translate to liveness/readiness probes.

This is deliberate. This series trains you for real production patterns, not just local demos.


9. ⚠️ Common Pitfalls (and How We Avoid Them)

Pitfall How This Module Prevents It
Using latest image Version pinned in .env.example
Global Docker volumes Local bind-mount inside module only
Weak health checks pg_isready validates actual DB readiness
Secrets committed to git .env ignored, .env.example only
Data loss on tear-down make clean requires explicit confirmation
Non-repeatable setup Deterministic Makefile + explicit design files

Consistency in environment setup prevents 90% of infra headaches.


10. 📘 Summary of Part 1

By completing this module, you now have:

  • A fully reusable Postgres infra unit
  • Pinned version
  • Deterministic runtime
  • Isolated storage
  • Health checks
  • Makefile-driven execution
  • Aligned folder structure for all upcoming modules

This is your first building block in the larger infrastructure library we are constructing.

You can take this modules/postgres folder and drop it into any other project you build — it will behave exactly the same.

Coming Up Next — Part 2: Kafka + Zookeeper

In the next part, we’ll build:

  • A pinned Kafka 3.4.x + Zookeeper 3.6.x module
  • With health checks
  • With a topic creation smoke test
  • With producer and consumer validation
  • And the same canonical scaffold structure

Kafka is the backbone for many downstream modules (Debezium, telemetry pipelines, distributed tracing), so it’s the natural next step.


🔗 Project Repo: https://github.com/KathiravanMuthaiah/infrastructureWithDocker


Building Infrastructure with Docker Series: post links

🔗 Building Infrastructure with Docker — Part0:

🔗 Building Infrastructure with Docker — Part2:

Technically authored by me, accelerated with insights from ChatGPT by OpenAI.” Refer: Leverage ChatGPT

Happy Learning