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:
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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;
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user