- Delete backups/*.dump and all real credentials from README/docs
(prod MinIO creds, Casdoor passwords, server IP, admin password)
- All stack secrets now come from git-ignored .env (.env.example added);
docker-compose.prod.yml refuses to boot without them (${VAR:?})
- seed_casdoor.py: bcrypt password hashing, generated console/client secrets
- Harden infra/deploy-webhook: HMAC-verified Gitea webhook receiver
+ systemd unit + README (replaces unauthenticated deploy endpoint)
- docs/SECRET-MANAGEMENT.md: secret inventory + rotation checklist
102 lines
5.3 KiB
YAML
102 lines
5.3 KiB
YAML
# ============================================================================
|
|
# Production overlay — real domains + HTTPS (Let's Encrypt) + HTTP→HTTPS
|
|
# redirect + security middlewares + required secrets.
|
|
#
|
|
# All credentials come from the git-ignored .env NEXT TO THE COMPOSE FILES
|
|
# (compose loads it automatically — keep it on the VPS, never in git):
|
|
#
|
|
# cp .env.example .env # then fill in STRONG values
|
|
# 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). The api's issuer is
|
|
# derived here from AUTH_HOST (see docs/casdoor-auth.md).
|
|
#
|
|
# Any `${VAR:?}` below refuses to boot when the variable is missing/empty —
|
|
# that is deliberate: production must not run on defaults.
|
|
# ============================================================================
|
|
|
|
services:
|
|
traefik:
|
|
environment:
|
|
# Certificates persist across restarts (acme.json in the traefik_certs volume).
|
|
TRAEFIK_ACME_EMAIL: ${TRAEFIK_ACME_EMAIL:?set TRAEFIK_ACME_EMAIL in .env}
|
|
|
|
postgres:
|
|
environment:
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
|
|
|
|
minio:
|
|
environment:
|
|
MINIO_ROOT_USER: ${MINIO_ROOT_USER:?set MINIO_ROOT_USER in .env}
|
|
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?set MINIO_ROOT_PASSWORD in .env}
|
|
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.middlewares=secure-headers@file
|
|
- 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.middlewares=secure-headers@file
|
|
- 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
|
|
|
|
api:
|
|
ports: [] # production: only reachable through Traefik
|
|
environment:
|
|
PROCUREMENT_DATABASE_URL: postgresql+asyncpg://postgres:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}@postgres:5432/procurement
|
|
PROCUREMENT_MINIO_ACCESS_KEY: ${MINIO_ROOT_USER:?set MINIO_ROOT_USER in .env}
|
|
PROCUREMENT_MINIO_SECRET_KEY: ${MINIO_ROOT_PASSWORD:?set MINIO_ROOT_PASSWORD in .env}
|
|
PROCUREMENT_CASDOOR_CLIENT_SECRET: ${CASDOOR_CLIENT_SECRET:?set CASDOOR_CLIENT_SECRET in .env}
|
|
PROCUREMENT_CASDOOR_ADMIN_PASSWORD: ${CASDOOR_ADMIN_PASSWORD:?set CASDOOR_ADMIN_PASSWORD in .env}
|
|
# Issuer must equal the browser-facing Casdoor origin (its discovery doc).
|
|
PROCUREMENT_CASDOOR_ISSUER: https://${AUTH_HOST:?set AUTH_HOST}
|
|
PROCUREMENT_CORS_ORIGINS: ${CORS_ORIGINS:?set CORS_ORIGINS in .env}
|
|
PROCUREMENT_ENVIRONMENT: prod
|
|
PROCUREMENT_DEV_LOGIN_ENABLED: "false"
|
|
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
|
|
- traefik.http.routers.api.middlewares=secure-headers@file,api-ratelimit@file,api-bodylimit@file
|
|
- 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.middlewares=secure-headers@file
|
|
- 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:
|
|
build:
|
|
args:
|
|
DEV_LOGIN_ENABLED: "false"
|
|
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.middlewares=secure-headers@file
|
|
- 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
|