# Production stack: one per customer. # Requires: # - COMPOSE_PROJECT_NAME=abc0001 (LOWERCASE; Compose v2 rejects uppercase. Use the # lowercase form of the customer ID — abc0001 for ABC0001.) # - CUSTOMER_HOST=abc0001.portal.example.com (Traefik routes by this Host) # - POSTGRES_PASSWORD, GRAFANA_ADMIN_PASSWORD set in .env # - An external Docker network `traefik-public` created by your Traefik stack # # Run with: # docker compose -f docker-compose.prod.yml --env-file .env up -d services: portal: build: . image: tau-acuvim-portal:latest container_name: ${COMPOSE_PROJECT_NAME}_portal restart: unless-stopped environment: - ASPNETCORE_ENVIRONMENT=Production - Application__PublicUrl=https://${CUSTOMER_HOST} # RunMode: Client (per-customer, default) or Admin (fleet aggregator). # Falls back to Client for normal customer stacks. - Application__RunMode=${Application__RunMode:-Client} - Database__ConnectionString=Host=timescaledb;Port=5432;Database=${POSTGRES_DB:-power_monitoring};Username=${POSTGRES_USER:-power_user};Password=${POSTGRES_PASSWORD} - Database__AutoProvisionLocalTimescaleDb=false - Authentication__DefaultAdminEmail=${Authentication__DefaultAdminEmail} - Authentication__DefaultAdminPassword=${Authentication__DefaultAdminPassword} - Grafana__BaseUrl=https://${CUSTOMER_HOST}${Grafana__EmbedPathPrefix:-/grafana} - Grafana__InternalUrl=http://grafana:3000 - Grafana__EmbedPathPrefix=${Grafana__EmbedPathPrefix:-/grafana} # White-label seed values. Applied ONLY on first boot (when the branding row # doesn't exist yet); after that the row in the DB is authoritative and the # operator edits via Settings → Branding. Pre-set these to spin up a new # customer already-branded so the customer admin never sees "Power Monitoring # Portal" on their first sign-in. Empty → template defaults from # appsettings.template.json. - WhiteLabel__ApplicationName=${WhiteLabel__ApplicationName:-Power Monitoring Portal} - WhiteLabel__LogoUrl=${WhiteLabel__LogoUrl:-} - WhiteLabel__PrimaryColor=${WhiteLabel__PrimaryColor:-#1f2937} - WhiteLabel__SecondaryColor=${WhiteLabel__SecondaryColor:-#374151} - WhiteLabel__AccentColor=${WhiteLabel__AccentColor:-#2563eb} - WhiteLabel__FooterText=${WhiteLabel__FooterText:-} # Fleet ingest (Client mode → push to Admin stack). Disabled by default; # enable per-customer in their .env after registering them on the Admin # Customers page. URL + Token are required when Enabled=true (startup guard). - FleetIngest__Enabled=${FleetIngest__Enabled:-false} - FleetIngest__Url=${FleetIngest__Url:-} - FleetIngest__Token=${FleetIngest__Token:-} - FleetIngest__IntervalSeconds=${FleetIngest__IntervalSeconds:-60} - FleetIngest__BatchSize=${FleetIngest__BatchSize:-5000} - FleetIngest__BatchMaxBytes=${FleetIngest__BatchMaxBytes:-1048576} depends_on: timescaledb: condition: service_healthy volumes: - portal-keys:/data/keys - portal-branding:/data/branding networks: - default - traefik-public labels: - "traefik.enable=true" - "traefik.docker.network=traefik-public" - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-portal.rule=Host(`${CUSTOMER_HOST}`)" - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-portal.entrypoints=websecure" - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-portal.tls=true" - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-portal.tls.certresolver=le" - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-portal.priority=10" - "traefik.http.services.${COMPOSE_PROJECT_NAME}-portal.loadbalancer.server.port=8080" timescaledb: image: timescale/timescaledb:2.17.2-pg16 container_name: ${COMPOSE_PROJECT_NAME}_timescale restart: unless-stopped environment: - POSTGRES_DB=${POSTGRES_DB:-power_monitoring} - POSTGRES_USER=${POSTGRES_USER:-power_user} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} volumes: - timescale-data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-power_user} -d ${POSTGRES_DB:-power_monitoring}"] interval: 10s timeout: 5s retries: 10 grafana: image: grafana/grafana:11.4.0 container_name: ${COMPOSE_PROJECT_NAME}_grafana restart: unless-stopped environment: - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD} - GF_SECURITY_ALLOW_EMBEDDING=true - GF_SERVER_ROOT_URL=https://${CUSTOMER_HOST}${Grafana__EmbedPathPrefix:-/grafana} - GF_SERVER_SERVE_FROM_SUB_PATH=true # Prod auth: option (a) — Traefik forwardAuth → portal /api/auth/check. # Anonymous stays OFF; Traefik gates every Grafana request on a valid portal # cookie via the middleware labels below. Switch modes (auth.proxy / render # tokens) → see README "Embedding Grafana — production auth options". - GF_AUTH_ANONYMOUS_ENABLED=false - GF_USERS_ALLOW_SIGN_UP=false - POSTGRES_DB=${POSTGRES_DB:-power_monitoring} - POSTGRES_USER=${POSTGRES_USER:-power_user} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} volumes: - grafana-data:/var/lib/grafana - ./grafana/provisioning:/etc/grafana/provisioning:ro - ./grafana/dashboards:/var/lib/grafana/dashboards:ro depends_on: timescaledb: condition: service_healthy networks: - default - traefik-public labels: - "traefik.enable=true" - "traefik.docker.network=traefik-public" - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-grafana.rule=Host(`${CUSTOMER_HOST}`) && PathPrefix(`${Grafana__EmbedPathPrefix:-/grafana}`)" - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-grafana.entrypoints=websecure" - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-grafana.tls=true" - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-grafana.tls.certresolver=le" - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-grafana.priority=20" - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-grafana.middlewares=${COMPOSE_PROJECT_NAME}-portal-auth@docker" - "traefik.http.services.${COMPOSE_PROJECT_NAME}-grafana.loadbalancer.server.port=3000" # forwardAuth middleware: every Grafana request gates on a sub-request to # the portal's /api/auth/check. 2xx → forward to Grafana; 401 → block. # Cookie is forwarded by default. trustForwardHeader so the portal sees the # real client IP if it ever wants to log it. authResponseHeaders is empty — # we don't need to surface anything back from the portal to Grafana. - "traefik.http.middlewares.${COMPOSE_PROJECT_NAME}-portal-auth.forwardauth.address=http://${COMPOSE_PROJECT_NAME}_portal:8080/api/auth/check" - "traefik.http.middlewares.${COMPOSE_PROJECT_NAME}-portal-auth.forwardauth.trustForwardHeader=true" volumes: portal-keys: portal-branding: timescale-data: grafana-data: networks: traefik-public: external: true