Files
2026-02-17 17:30:45 -05:00

18 KiB

PRD: Infrastructure & Deployment

Scope: Docker Compose, NATS, MinIO, PostgreSQL, nginx, service topology Status: v1 | Last updated: 2026-02-17


1. Overview

Infrastructure configuration for dev, test, and production environments. All environments use the same backing services (PostgreSQL, NATS, MinIO) — only configuration differs.

2. Service Ports

2.1 Application Services (Dev)

Service Port Protocol
Auth Gateway 3000 HTTP
API 3001 HTTP
Web SM 3002 HTTP
TUI SM 3003 HTTP

2.2 Infrastructure Services (Dev)

Service Port(s) Protocol
PostgreSQL 5432 TCP
NATS 4222 (client), 8222 (monitoring) TCP, HTTP
MinIO 9000 (API), 9001 (console) HTTP

2.3 Production

Service Port Exposed
nginx 80, 443 External
Auth Gateway 3000 Internal only
API 3001 Internal only
Web SM 3002 Internal only
TUI SM 3003 Internal only
PostgreSQL 5432 Internal only
NATS 4222 Internal only
MinIO 9000 Internal only

3. Docker Compose — Development

File: docker-compose.dev.yml

Runs infrastructure services only. Clojure services run locally via REPL.

# docker-compose.dev.yml
services:
  postgres:
    image: postgres:16-alpine
    ports:
      - "5432:5432"
    environment:
      POSTGRES_DB: ajet_chat
      POSTGRES_USER: ajet
      POSTGRES_PASSWORD: ajet_dev
    volumes:
      - pgdata_dev:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ajet -d ajet_chat"]
      interval: 5s
      timeout: 3s
      retries: 5

  nats:
    image: nats:2.10-alpine
    ports:
      - "4222:4222"
      - "8222:8222"
    command: ["--js", "--sd", "/data", "-m", "8222"]
    volumes:
      - natsdata_dev:/data
    healthcheck:
      test: ["CMD", "nats-server", "--signal", "ldm"]
      interval: 5s
      timeout: 3s
      retries: 5

  minio:
    image: minio/minio:latest
    ports:
      - "9000:9000"
      - "9001:9001"
    environment:
      MINIO_ROOT_USER: minioadmin
      MINIO_ROOT_PASSWORD: minioadmin
    command: server /data --console-address ":9001"
    volumes:
      - miniodata_dev:/data
    healthcheck:
      test: ["CMD", "mc", "ready", "local"]
      interval: 5s
      timeout: 3s
      retries: 5

  minio-init:
    image: minio/mc:latest
    depends_on:
      minio:
        condition: service_healthy
    entrypoint: >
      /bin/sh -c "
      mc alias set local http://minio:9000 minioadmin minioadmin;
      mc mb --ignore-existing local/ajet-chat;
      "

volumes:
  pgdata_dev:
  natsdata_dev:
  miniodata_dev:

Usage:

docker compose -f docker-compose.dev.yml up -d
# Then start Clojure services via REPL
clj -A:dev:api:web-sm:tui-sm:auth-gw

4. Docker Compose — Test

File: docker-compose.test.yml

Fresh database per run. Separate ports to avoid conflicts with dev.

# docker-compose.test.yml
services:
  postgres-test:
    image: postgres:16-alpine
    ports:
      - "5433:5432"
    environment:
      POSTGRES_DB: ajet_chat_test
      POSTGRES_USER: ajet
      POSTGRES_PASSWORD: ajet_test
    tmpfs:
      - /var/lib/postgresql/data    # ephemeral — fresh DB each run
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ajet -d ajet_chat_test"]
      interval: 3s
      timeout: 2s
      retries: 10

  nats-test:
    image: nats:2.10-alpine
    ports:
      - "4223:4222"
    command: ["--js"]               # JetStream enabled, no persistent storage
    healthcheck:
      test: ["CMD", "nats-server", "--signal", "ldm"]
      interval: 3s
      timeout: 2s
      retries: 10

  minio-test:
    image: minio/minio:latest
    ports:
      - "9002:9000"
    environment:
      MINIO_ROOT_USER: minioadmin
      MINIO_ROOT_PASSWORD: minioadmin
    command: server /data
    tmpfs:
      - /data                        # ephemeral — no persistent files
    healthcheck:
      test: ["CMD", "mc", "ready", "local"]
      interval: 3s
      timeout: 2s
      retries: 10

  minio-test-init:
    image: minio/mc:latest
    depends_on:
      minio-test:
        condition: service_healthy
    entrypoint: >
      /bin/sh -c "
      mc alias set local http://minio-test:9000 minioadmin minioadmin;
      mc mb --ignore-existing local/ajet-chat;
      "

  # E2E profile — adds application service containers
  auth-gw:
    profiles: ["e2e"]
    build:
      context: .
      dockerfile: auth-gw/Dockerfile
    ports:
      - "3100:3000"
    environment:
      AJET_DB_HOST: postgres-test
      AJET_DB_PORT: 5432
      AJET_DB_DBNAME: ajet_chat_test
      AJET_DB_USER: ajet
      AJET_DB_PASSWORD: ajet_test
      AJET_SERVICES_API_HOST: api
      AJET_SERVICES_WEB_SM_HOST: web-sm
      AJET_SERVICES_TUI_SM_HOST: tui-sm
    depends_on:
      postgres-test:
        condition: service_healthy

  api:
    profiles: ["e2e"]
    build:
      context: .
      dockerfile: api/Dockerfile
    environment:
      AJET_DB_HOST: postgres-test
      AJET_DB_PORT: 5432
      AJET_DB_DBNAME: ajet_chat_test
      AJET_DB_USER: ajet
      AJET_DB_PASSWORD: ajet_test
      AJET_NATS_URL: nats://nats-test:4222
      AJET_MINIO_ENDPOINT: http://minio-test:9000
    depends_on:
      postgres-test:
        condition: service_healthy
      nats-test:
        condition: service_healthy
      minio-test-init:
        condition: service_completed_successfully

  web-sm:
    profiles: ["e2e"]
    build:
      context: .
      dockerfile: web-sm/Dockerfile
    environment:
      AJET_API_BASE_URL: http://api:3001
      AJET_NATS_URL: nats://nats-test:4222
    depends_on:
      nats-test:
        condition: service_healthy

  tui-sm:
    profiles: ["e2e"]
    build:
      context: .
      dockerfile: tui-sm/Dockerfile
    environment:
      AJET_API_BASE_URL: http://api:3001
      AJET_NATS_URL: nats://nats-test:4222
    depends_on:
      nats-test:
        condition: service_healthy

Usage:

# Unit + integration tests
docker compose -f docker-compose.test.yml up -d
clj -M:test/unit
clj -M:test/integration

# E2E tests (full stack)
docker compose -f docker-compose.test.yml --profile e2e up -d --build
clj -M:test/e2e

# Teardown
docker compose -f docker-compose.test.yml --profile e2e down -v

5. Docker Compose — Production

File: docker-compose.yml

Full stack with nginx TLS termination.

# docker-compose.yml
services:
  nginx:
    image: nginx:1.27-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/certs:/etc/nginx/certs:ro
    depends_on:
      - auth-gw
    restart: unless-stopped

  auth-gw:
    build:
      context: .
      dockerfile: auth-gw/Dockerfile
    environment:
      AJET_DB_HOST: postgres
      AJET_DB_PASSWORD: ${AJET_DB_PASSWORD}
      AJET_OAUTH_GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID}
      AJET_OAUTH_GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET}
      AJET_SERVICES_API_HOST: api
      AJET_SERVICES_WEB_SM_HOST: web-sm
      AJET_SERVICES_TUI_SM_HOST: tui-sm
    depends_on:
      postgres:
        condition: service_healthy
    restart: unless-stopped

  api:
    build:
      context: .
      dockerfile: api/Dockerfile
    environment:
      AJET_DB_HOST: postgres
      AJET_DB_PASSWORD: ${AJET_DB_PASSWORD}
      AJET_NATS_URL: nats://nats:4222
      AJET_MINIO_ENDPOINT: http://minio:9000
      AJET_MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
      AJET_MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
    depends_on:
      postgres:
        condition: service_healthy
      nats:
        condition: service_healthy
      minio:
        condition: service_healthy
    restart: unless-stopped

  web-sm:
    build:
      context: .
      dockerfile: web-sm/Dockerfile
    environment:
      AJET_API_BASE_URL: http://api:3001
      AJET_NATS_URL: nats://nats:4222
    depends_on:
      nats:
        condition: service_healthy
    restart: unless-stopped

  tui-sm:
    build:
      context: .
      dockerfile: tui-sm/Dockerfile
    environment:
      AJET_API_BASE_URL: http://api:3001
      AJET_NATS_URL: nats://nats:4222
    depends_on:
      nats:
        condition: service_healthy
    restart: unless-stopped

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: ajet_chat
      POSTGRES_USER: ajet
      POSTGRES_PASSWORD: ${AJET_DB_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ajet -d ajet_chat"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  nats:
    image: nats:2.10-alpine
    command: ["--js", "--sd", "/data", "-m", "8222"]
    volumes:
      - natsdata:/data
    healthcheck:
      test: ["CMD", "nats-server", "--signal", "ldm"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  minio:
    image: minio/minio:latest
    environment:
      MINIO_ROOT_USER: ${MINIO_ACCESS_KEY}
      MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY}
    command: server /data
    volumes:
      - miniodata:/data
    healthcheck:
      test: ["CMD", "mc", "ready", "local"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

volumes:
  pgdata:
  natsdata:
  miniodata:

Environment file (.env):

AJET_DB_PASSWORD=<strong-random-password>
GITHUB_CLIENT_ID=<from-github-oauth-app>          # seed only — migrated to DB on first start
GITHUB_CLIENT_SECRET=<from-github-oauth-app>       # seed only — migrated to DB on first start
MINIO_ACCESS_KEY=<random-access-key>
MINIO_SECRET_KEY=<random-secret-key>

Note: OAuth credentials in .env are auto-migrated to the oauth_providers DB table on first startup. After that, manage OAuth providers via the admin setup wizard or the admin API. You can also skip these env vars entirely and configure providers through the setup wizard on first deployment.


6. nginx Configuration (Production)

File: nginx/nginx.conf

worker_processes auto;

events {
    worker_connections 4096;
}

http {
    upstream auth_gw {
        server auth-gw:3000;
    }

    # Redirect HTTP → HTTPS
    server {
        listen 80;
        server_name chat.example.com;
        return 301 https://$host$request_uri;
    }

    server {
        listen 443 ssl http2;
        server_name chat.example.com;

        ssl_certificate     /etc/nginx/certs/fullchain.pem;
        ssl_certificate_key /etc/nginx/certs/privkey.pem;
        ssl_protocols       TLSv1.2 TLSv1.3;

        # Proxy all traffic to Auth Gateway
        location / {
            proxy_pass http://auth_gw;
            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;

            # SSE support — disable buffering
            proxy_buffering off;
            proxy_cache off;
            proxy_read_timeout 86400s;   # 24h for SSE connections
            proxy_send_timeout 86400s;

            # WebSocket support (future, for voice)
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
        }

        # Static file size limit
        client_max_body_size 10m;
    }
}

7. NATS JetStream Configuration

7.1 Stream Setup

Created automatically by the API service on startup:

;; Stream: ajet-events
{:name        "ajet-events"
 :subjects    ["chat.events.>"
               "chat.dm.>"
               "chat.typing.>"
               "chat.presence.>"
               "chat.notifications.>"
               "chat.audit"]
 :retention   :limits           ;; retain by limits (not interest or work-queue)
 :max-age     86400000000000    ;; 24 hours in nanoseconds
 :max-bytes   1073741824        ;; 1 GB
 :storage     :file
 :replicas    1                 ;; single node for v1
 :discard     :old}             ;; discard oldest when limits hit

7.2 Consumer Setup

Each session manager creates a durable consumer on connect:

;; Consumer: per-user, created by SM on SSE connection
{:durable-name  "sm-{service}-{user-id}"   ;; e.g. "sm-web-abc123"
 :filter-subjects ["chat.events.{community-id}"
                   "chat.dm.{channel-id-1}"
                   "chat.dm.{channel-id-2}"
                   "chat.notifications.{user-id}"]
 :deliver-policy :by-start-time             ;; on reconnect: from last-event-id time
 :ack-policy     :none}                     ;; no ack needed for real-time delivery

7.3 Subject ACLs (Future)

Not needed for v1 (single NATS instance, trusted services). For multi-tenant production:

  • API: publish to all subjects
  • SMs: subscribe only to subjects relevant to their connected users
  • No direct client access to NATS (all mediated by SMs)

8. MinIO Configuration

8.1 Bucket Setup

Single bucket ajet-chat with:

  • Default retention: none (files kept indefinitely)
  • Versioning: disabled (no need for file history in v1)

8.2 Storage Key Convention

attachments/{message-uuid}/{filename}       — message attachments
avatars/users/{user-uuid}/{filename}         — user avatars
avatars/communities/{community-uuid}/{filename} — community icons
avatars/webhooks/{webhook-uuid}/{filename}   — webhook bot icons

8.3 Access Patterns

  • Upload: API service writes to MinIO on file upload
  • Download: Auth GW proxies /files/* requests to MinIO (or API generates presigned URLs)
  • No direct client access to MinIO in v1 — all through API/Auth GW

9. PostgreSQL Configuration

shared_buffers = 256MB          # 25% of available RAM
work_mem = 16MB
effective_cache_size = 768MB    # 75% of available RAM
maintenance_work_mem = 128MB
max_connections = 100
log_min_duration_statement = 500   # log queries > 500ms

9.2 Backups

  • pg_dump daily cron job (simple, sufficient for v1)
  • Dump to local volume, rotate 7 days
  • Future: WAL archiving for point-in-time recovery

10. Dockerfile Template

Each service uses the same multi-stage build:

# {service}/Dockerfile
FROM clojure:temurin-21-tools-deps-1.12 AS builder
WORKDIR /app
COPY deps.edn .
COPY shared/ shared/
COPY {service}/ {service}/
RUN clj -T:build uber :module {service}

FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/{service}.jar app.jar
EXPOSE {port}
CMD ["java", "-jar", "app.jar"]

Build alias (root deps.edn):

:build {:deps {io.github.clojure/tools.build {:mvn/version "0.10.6"}}
        :ns-default build}

11. Environment Variables Reference

11.1 All Services

Variable Default Description
AJET_DB_HOST localhost PostgreSQL host
AJET_DB_PORT 5432 PostgreSQL port
AJET_DB_DBNAME ajet_chat Database name
AJET_DB_USER ajet Database user
AJET_DB_PASSWORD Database password (required)
AJET_NATS_URL nats://localhost:4222 NATS server URL
AJET_MINIO_ENDPOINT http://localhost:9000 MinIO endpoint
AJET_MINIO_ACCESS_KEY minioadmin MinIO access key
AJET_MINIO_SECRET_KEY minioadmin MinIO secret key

11.2 Auth Gateway Only

Variable Default Description
AJET_OAUTH_GITHUB_CLIENT_ID GitHub OAuth app client ID (seed only — auto-migrated to DB)
AJET_OAUTH_GITHUB_CLIENT_SECRET GitHub OAuth app client secret (seed only — auto-migrated to DB)
AJET_OAUTH_GITEA_CLIENT_ID Gitea OAuth client ID (seed only — auto-migrated to DB)
AJET_OAUTH_GITEA_CLIENT_SECRET Gitea OAuth client secret (seed only — auto-migrated to DB)
AJET_OAUTH_GITEA_BASE_URL Gitea instance URL (seed only — auto-migrated to DB)
AJET_OAUTH_OIDC_CLIENT_ID OIDC client ID (seed only — auto-migrated to DB)
AJET_OAUTH_OIDC_CLIENT_SECRET OIDC client secret (seed only — auto-migrated to DB)
AJET_OAUTH_OIDC_ISSUER_URL OIDC issuer URL (seed only — auto-migrated to DB)
AJET_SERVICES_API_HOST localhost API service host
AJET_SERVICES_API_PORT 3001 API service port
AJET_SERVICES_WEB_SM_HOST localhost Web SM host
AJET_SERVICES_WEB_SM_PORT 3002 Web SM port
AJET_SERVICES_TUI_SM_HOST localhost TUI SM host
AJET_SERVICES_TUI_SM_PORT 3003 TUI SM port

Note on OAuth env vars: The AJET_OAUTH_* environment variables serve as a seed for initial deployment only. On first startup, if the oauth_providers DB table is empty, Auth GW auto-migrates these values into the table. After migration, providers are managed exclusively via the admin API (/api/admin/oauth-providers) or the setup wizard. The env vars are ignored once providers exist in the DB.

11.3 Session Managers Only

Variable Default Description
AJET_API_BASE_URL http://localhost:3001 Internal API base URL

12. Test Cases

ID Test Type Description
INFRA-T1 Dev compose starts Integration docker compose -f docker-compose.dev.yml up starts PG + NATS + MinIO
INFRA-T2 PG accessible Integration Can connect to Postgres on port 5432
INFRA-T3 NATS accessible Integration Can connect to NATS on port 4222
INFRA-T4 MinIO accessible Integration Can connect to MinIO on port 9000
INFRA-T5 MinIO bucket created Integration ajet-chat bucket exists after minio-init
INFRA-T6 Test compose fresh DB Integration Test DB has no data from previous runs
INFRA-T7 JetStream enabled Integration NATS JetStream API responds to stream info request
INFRA-T8 Health checks pass Integration All service health checks return healthy
INFRA-T9 Prod compose full stack E2E All services start and connect to each other
INFRA-T10 nginx proxies to auth-gw E2E HTTPS request reaches Auth Gateway via nginx