infra: single postgres instance (casdoor DB merged) + Traefik v3 edge

- remove the separate casdoor-postgres container; casdoor uses the main
  postgres (db casdoor, created by sql/02-casdoor-db.sql on fresh volumes);
  data migrated via pg_dump/pg_restore (backups kept outside the repo)
- Traefik v3.6 as the public edge (ports 80/443): docker provider pinned to
  the dedicated traefik_network (fixed-name network), host rules for
  app/api/auth/s3/storage (env-overridable, localhost defaults), frontend
  nginx demoted to internal SPA server; docker-compose.prod.yml adds real
  domains + Let's Encrypt + HTTP→HTTPS redirect (redirect middleware in
  infra/traefik-dynamic.yml)
- v3.6 required (Traefik <=3.3 fails against Docker Engine 29.x); socket
  mounted rw (Docker Desktop rejects :ro)
- start.ps1: app URL now http://localhost, casdoor + db notes
This commit is contained in:
ali_mohaghegh
2026-09-01 17:28:33 +03:30
parent c5997ac243
commit 8825f333a7
5 changed files with 198 additions and 19 deletions
+74
View File
@@ -0,0 +1,74 @@
# ============================================================================
# Production overlay — real domains + HTTPS (Let's Encrypt) + HTTP→HTTPS
# redirect. Usage:
#
# APP_HOST=app.example.com API_HOST=api.example.com AUTH_HOST=auth.example.com \
# S3_HOST=s3.example.com STORAGE_HOST=storage.example.com \
# TRAEFIK_ACME_EMAIL=admin@example.com \
# docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
#
# Requirements: DNS records for every host pointing at the server, ports
# 80 + 443 open (ACME http-challenge uses port 80), and the api's
# PROCUREMENT_CASDOOR_ISSUER must be updated to the public auth host
# (see docs/casdoor-auth.md).
# ============================================================================
services:
traefik:
environment:
# Certificates persist across restarts (acme.json in the traefik_certs volume).
TRAEFIK_ACME_EMAIL: ${TRAEFIK_ACME_EMAIL:?set TRAEFIK_ACME_EMAIL for production}
api:
ports: [] # production: only reachable through Traefik
labels:
- traefik.http.routers.api.rule=Host(`${API_HOST:?set API_HOST}`)
- traefik.http.routers.api.entrypoints=websecure
- traefik.http.routers.api.tls=true
- traefik.http.routers.api.tls.certresolver=letsencrypt
# HTTP → HTTPS
- traefik.http.routers.api-redir.rule=Host(`${API_HOST:?set API_HOST}`)
- traefik.http.routers.api-redir.entrypoints=web
- traefik.http.routers.api-redir.middlewares=redirect-https@file
casdoor:
ports: [] # production: only reachable through Traefik (auth host)
environment:
# Casdoor must know its public origin so tokens carry the right iss.
origin: "https://${AUTH_HOST:?set AUTH_HOST}"
labels:
- traefik.http.routers.casdoor.rule=Host(`${AUTH_HOST:?set AUTH_HOST}`)
- traefik.http.routers.casdoor.entrypoints=websecure
- traefik.http.routers.casdoor.tls=true
- traefik.http.routers.casdoor.tls.certresolver=letsencrypt
- traefik.http.routers.casdoor-redir.rule=Host(`${AUTH_HOST:?set AUTH_HOST}`)
- traefik.http.routers.casdoor-redir.entrypoints=web
- traefik.http.routers.casdoor-redir.middlewares=redirect-https@file
frontend:
labels:
- traefik.http.routers.frontend.rule=Host(`${APP_HOST:?set APP_HOST}`)
- traefik.http.routers.frontend.entrypoints=websecure
- traefik.http.routers.frontend.tls=true
- traefik.http.routers.frontend.tls.certresolver=letsencrypt
- traefik.http.routers.frontend-redir.rule=Host(`${APP_HOST:?set APP_HOST}`)
- traefik.http.routers.frontend-redir.entrypoints=web
- traefik.http.routers.frontend-redir.middlewares=redirect-https@file
minio:
ports: [] # production: only reachable through Traefik
labels:
- traefik.http.routers.minio-api.rule=Host(`${S3_HOST:?set S3_HOST}`)
- traefik.http.routers.minio-api.entrypoints=websecure
- traefik.http.routers.minio-api.tls=true
- traefik.http.routers.minio-api.tls.certresolver=letsencrypt
- traefik.http.routers.minio-api-redir.rule=Host(`${S3_HOST:?set S3_HOST}`)
- traefik.http.routers.minio-api-redir.entrypoints=web
- traefik.http.routers.minio-api-redir.middlewares=redirect-https@file
- traefik.http.routers.minio-console.rule=Host(`${STORAGE_HOST:?set STORAGE_HOST}`)
- traefik.http.routers.minio-console.entrypoints=websecure
- traefik.http.routers.minio-console.tls=true
- traefik.http.routers.minio-console.tls.certresolver=letsencrypt
- traefik.http.routers.minio-console-redir.rule=Host(`${STORAGE_HOST:?set STORAGE_HOST}`)
- traefik.http.routers.minio-console-redir.entrypoints=web
- traefik.http.routers.minio-console-redir.middlewares=redirect-https@file
+103 -17
View File
@@ -14,7 +14,7 @@ services:
volumes: volumes:
- postgres_data:/var/lib/postgresql - postgres_data:/var/lib/postgresql
- ./sql/migration.sql:/docker-entrypoint-initdb.d/01-migration.sql:ro - ./sql/migration.sql:/docker-entrypoint-initdb.d/01-migration.sql:ro
# - ./sql/seed.sql:/docker-entrypoint-initdb.d/02-seed.sql:ro - ./sql/02-casdoor-db.sql:/docker-entrypoint-initdb.d/02-casdoor-db.sql:ro
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d procurement"] test: ["CMD-SHELL", "pg_isready -U postgres -d procurement"]
@@ -24,6 +24,44 @@ services:
restart: unless-stopped restart: unless-stopped
# ==========================================================================
# Traefik v3 — public reverse proxy (replaces nginx as the edge)
# App is served on http://localhost (port 80) instead of :3000.
# Production: see docker-compose.prod.yml (real domains + HTTPS via
# Let's Encrypt, HTTP→HTTPS redirect).
# ==========================================================================
traefik:
# v3.6+ required: Traefik ≤3.3 fails against Docker Engine 29.x
# ("Error response from daemon" — Docker API version mismatch).
image: traefik:v3.6
container_name: procurement-traefik
restart: unless-stopped
command:
- --providers.docker=true
- --providers.docker.exposedbydefault=false
# Containers are on several networks; Traefik must use the dedicated
# proxy network IP (otherwise it may pick the default-network IP → 504).
- --providers.docker.network=traefik_network
- --providers.file.filename=/etc/traefik/dynamic.yml
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --certificatesresolvers.letsencrypt.acme.email=${TRAEFIK_ACME_EMAIL:-admin@example.com}
- --certificatesresolvers.letsencrypt.acme.storage=/certs/acme.json
- --certificatesresolvers.letsencrypt.acme.httpchallenge=true
- --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web
- --log.level=INFO
ports:
- "80:80"
- "443:443"
volumes:
# NOTE: mounted read-write — Docker Desktop rejects read-only
# /var/run/docker.sock mounts ("Error response from daemon").
- /var/run/docker.sock:/var/run/docker.sock
- ./infra/traefik-dynamic.yml:/etc/traefik/dynamic.yml:ro
- traefik_certs:/certs
networks:
- traefik_network
minio: minio:
image: minio/minio:latest image: minio/minio:latest
container_name: procurement-minio container_name: procurement-minio
@@ -41,6 +79,23 @@ services:
volumes: volumes:
- minio_data:/data - minio_data:/data
labels:
- traefik.enable=true
# S3 API
- traefik.http.routers.minio-api.rule=Host(`${S3_HOST:-s3.localhost}`)
- traefik.http.routers.minio-api.entrypoints=web
- traefik.http.routers.minio-api.service=minio-api-svc
- traefik.http.services.minio-api-svc.loadbalancer.server.port=9000
# Web console
- traefik.http.routers.minio-console.rule=Host(`${STORAGE_HOST:-storage.localhost}`)
- traefik.http.routers.minio-console.entrypoints=web
- traefik.http.routers.minio-console.service=minio-console-svc
- traefik.http.services.minio-console-svc.loadbalancer.server.port=9001
networks:
- traefik_network
- default
restart: unless-stopped restart: unless-stopped
# MinIO Client (mc): waits for the server, creates the bucket, exits. # MinIO Client (mc): waits for the server, creates the bucket, exits.
@@ -77,31 +132,35 @@ services:
# is created by backend/tools/seed_casdoor.py — run it once after the # is created by backend/tools/seed_casdoor.py — run it once after the
# first boot: # first boot:
# docker compose run --rm -e CASDOOR_BASE=http://casdoor:8000 api python tools/seed_casdoor.py # docker compose run --rm -e CASDOOR_BASE=http://casdoor:8000 api python tools/seed_casdoor.py
#
# Casdoor uses the SAME postgres instance as the app (database `casdoor`,
# created by sql/02-casdoor-db.sql on a fresh volume).
# ========================================================================== # ==========================================================================
casdoor-postgres:
image: postgres:16
container_name: procurement-casdoor-postgres
environment:
POSTGRES_DB: casdoor
POSTGRES_USER: casdoor
POSTGRES_PASSWORD: casdoor
volumes:
- casdoor_postgres_data:/var/lib/postgresql
restart: unless-stopped
casdoor: casdoor:
image: casbin/casdoor:latest image: casbin/casdoor:latest
container_name: procurement-casdoor container_name: procurement-casdoor
depends_on: depends_on:
- casdoor-postgres postgres:
condition: service_healthy
environment: environment:
RUNNING_IN_DOCKER: "true" RUNNING_IN_DOCKER: "true"
driverName: postgres driverName: postgres
dataSourceName: "user=casdoor password=casdoor host=casdoor-postgres port=5432 sslmode=disable dbname=casdoor" dataSourceName: "user=casdoor password=casdoor host=postgres port=5432 sslmode=disable dbname=casdoor"
dbName: casdoor dbName: casdoor
origin: "http://localhost:8080" origin: "http://localhost:8080"
ports: ports:
- "8080:8000" - "8080:8000"
labels:
- traefik.enable=true
- traefik.http.routers.casdoor.rule=Host(`${AUTH_HOST:-auth.localhost}`)
- traefik.http.routers.casdoor.entrypoints=web
- traefik.http.services.casdoor.loadbalancer.server.port=8000
networks:
- traefik_network
- default
restart: unless-stopped restart: unless-stopped
api: api:
@@ -126,10 +185,23 @@ services:
PROCUREMENT_CASDOOR_ORGANIZATION: inventory PROCUREMENT_CASDOOR_ORGANIZATION: inventory
PROCUREMENT_CASDOOR_APP_NAME: inventory-app PROCUREMENT_CASDOOR_APP_NAME: inventory-app
PROCUREMENT_CASDOOR_AUDIENCE: inventory-app-client PROCUREMENT_CASDOOR_AUDIENCE: inventory-app-client
# Console admin used by POST /users/sync (identity mirroring).
PROCUREMENT_CASDOOR_ADMIN_USER: admin
PROCUREMENT_CASDOOR_ADMIN_PASSWORD: "123"
ports: ports:
- "8000:8000" - "8000:8000"
labels:
- traefik.enable=true
- traefik.http.routers.api.rule=Host(`${API_HOST:-api.localhost}`)
- traefik.http.routers.api.entrypoints=web
- traefik.http.services.api.loadbalancer.server.port=8000
networks:
- traefik_network
- default
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
@@ -146,15 +218,29 @@ services:
DEV_LOGIN_ENABLED: ${DEV_LOGIN_ENABLED:-true} DEV_LOGIN_ENABLED: ${DEV_LOGIN_ENABLED:-true}
container_name: procurement-frontend container_name: procurement-frontend
ports: labels:
- "3000:80" - traefik.enable=true
- traefik.http.routers.frontend.rule=Host(`${APP_HOST:-localhost}`)
- traefik.http.routers.frontend.entrypoints=web
- traefik.http.services.frontend.loadbalancer.server.port=80
networks:
- traefik_network
- default
depends_on: depends_on:
- api - api
restart: unless-stopped restart: unless-stopped
networks:
# شبکه‌ی عمومی: فقط ترافیک و سرویس‌های عمومی. postgres روی شبکه‌ی
# پیش‌فرض می‌ماند و از بیرون قابل دسترسی نیست.
traefik_network:
name: traefik_network
driver: bridge
volumes: volumes:
postgres_data: postgres_data:
minio_data: minio_data:
casdoor_postgres_data: traefik_certs:
+9
View File
@@ -0,0 +1,9 @@
# Traefik dynamic config (file provider) — shared middlewares.
# The HTTP→HTTPS redirect is attached from docker-compose.prod.yml so that
# local development (http://localhost) keeps working without TLS.
http:
middlewares:
redirect-https:
redirectScheme:
scheme: https
permanent: true
+9
View File
@@ -0,0 +1,9 @@
-- ============================================================
-- 02-casdoor-db.sql — creates the Casdoor role + database on the
-- unified PostgreSQL instance (runs only on a fresh postgres volume,
-- after 01-migration.sql). The Casdoor service connects here instead
-- of running its own postgres container (v1.3 DB consolidation).
-- ============================================================
CREATE ROLE casdoor LOGIN PASSWORD 'casdoor';
CREATE DATABASE casdoor OWNER casdoor;
+3 -2
View File
@@ -25,8 +25,9 @@ Start-Sleep -Seconds 5
docker compose ps docker compose ps
Write-Host "" Write-Host ""
Write-Host "Frontend : http://localhost:3000" -ForegroundColor Green Write-Host "Frontend : http://localhost (via Traefik; old :3000 is gone)" -ForegroundColor Green
Write-Host "API docs : http://localhost:8000/docs" -ForegroundColor Green Write-Host "API docs : http://localhost:8000/docs" -ForegroundColor Green
Write-Host "DB : localhost:5432 (procurement / postgres / postgres)" -ForegroundColor Green Write-Host "Casdoor : http://localhost:8080 (admin / 123)" -ForegroundColor Green
Write-Host "DB : localhost:5433 (procurement / postgres / postgres; casdoor DB inside the same instance)" -ForegroundColor Green
Write-Host "" Write-Host ""
Write-Host "Stop: docker compose down | Full reset: docker compose down -v && up -d --build" -ForegroundColor DarkGray Write-Host "Stop: docker compose down | Full reset: docker compose down -v && up -d --build" -ForegroundColor DarkGray