Owner request: one 'docker compose up -d' should bring the whole system up —
no separate monitoring project to start for the dashboards.
- docker-compose.yml: grafana service (procurement-grafana, loopback :3001,
default+shared networks, obs datasource env) + grafana_data volume;
provisioning and dashboard JSONs stay in ./monitoring/grafana/ (no file
moves, provisioning unchanged)
- docker-compose.prod.yml: GRAFANA_ADMIN_PASSWORD forced via the existing
${VAR:?} pattern (local keeps a lenient default, prod refuses defaults)
- monitoring/docker-compose.monitoring.yml: grafana service + volume removed
(no name/port conflict); glitchtip/prometheus/loki stay server-side as before
- docs updated: grafana restarts on app deploys are harmless (settings live
in the volume); SSH-tunnel posture unchanged
Verified locally: both compose files validate; 'docker compose up -d' starts
all 10 services incl. procurement-grafana; BotPostgres datasource health OK;
all 3 dashboards provisioned in the fresh volume; monitoring compose no
longer contains grafana; old monitoring-grafana container removed; port
still loopback-only.
106 lines
5.4 KiB
YAML
106 lines
5.4 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}
|
|
|
|
grafana:
|
|
environment:
|
|
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:?set GRAFANA_ADMIN_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
|