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:
|
||||
- postgres_data:/var/lib/postgresql
|
||||
- ./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:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres -d procurement"]
|
||||
@@ -24,6 +24,44 @@ services:
|
||||
|
||||
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:
|
||||
image: minio/minio:latest
|
||||
container_name: procurement-minio
|
||||
@@ -41,6 +79,23 @@ services:
|
||||
volumes:
|
||||
- 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
|
||||
|
||||
# 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
|
||||
# first boot:
|
||||
# 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:
|
||||
image: casbin/casdoor:latest
|
||||
container_name: procurement-casdoor
|
||||
depends_on:
|
||||
- casdoor-postgres
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
RUNNING_IN_DOCKER: "true"
|
||||
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
|
||||
origin: "http://localhost:8080"
|
||||
ports:
|
||||
- "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
|
||||
|
||||
api:
|
||||
@@ -126,10 +185,23 @@ services:
|
||||
PROCUREMENT_CASDOOR_ORGANIZATION: inventory
|
||||
PROCUREMENT_CASDOOR_APP_NAME: inventory-app
|
||||
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:
|
||||
- "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:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
@@ -146,15 +218,29 @@ services:
|
||||
DEV_LOGIN_ENABLED: ${DEV_LOGIN_ENABLED:-true}
|
||||
container_name: procurement-frontend
|
||||
|
||||
ports:
|
||||
- "3000:80"
|
||||
labels:
|
||||
- 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:
|
||||
- api
|
||||
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
# شبکهی عمومی: فقط ترافیک و سرویسهای عمومی. postgres روی شبکهی
|
||||
# پیشفرض میماند و از بیرون قابل دسترسی نیست.
|
||||
traefik_network:
|
||||
name: traefik_network
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
postgres_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
|
||||
|
||||
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 "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 "Stop: docker compose down | Full reset: docker compose down -v && up -d --build" -ForegroundColor DarkGray
|
||||
|
||||
Reference in New Issue
Block a user