Tool 2 of the deliverability monitoring pair (Tool 1 = mail_reputation_monitor). DMARC rua reports from dozens of operators (Google, Yahoo, Comcast, Cox, Bell, Mimecast, Cisco ESA, GMX, mail.com, ...) were landing in ops@ (dmarc@ was a DL), burying real mail and never parsed. Now ingested + queryable: - dmarc@performancewest.net converted DL -> dedicated Carbonio mailbox; isolated IMAP creds in server .env, surfaced to workers in docker-compose.yml (mirrors OPS_IMAP_*). 29 historical reports moved ops@ -> dmarc@ via IMAP. - scripts/dmarc_report_parser.py: IMAP fetch unseen -> decompress .gz/.zip/.xml (namespace-agnostic: classic + urn:ietf:params:xml:ns:dmarc-2.0 GMX/mail.com) -> parse aggregate XML -> upsert dmarc_report (keyed (org_name,report_id), no-op on re-parse) + dmarc_record per source IP. dmarc_pass = dkim_aligned OR spf_aligned. Marks \Seen. --dry-run/--all/--alert (7d per-IP summary + Telegram if one of OUR IPs <95% pass, or EXTERNAL IP sends >=20 failing msgs as us = spoofing under p=reject). psycopg2 imported lazily so --dry-run runs without the driver. - api/migrations/102_dmarc_aggregate.sql: dmarc_report + dmarc_record tables. - infra/cron/pw-dmarc-parser: 06:20 UTC daily --alert (after reputation, before scrub). - docs/deliverability.md: DMARC section DONE; query examples. Verified: dry-run --all parses all 28 reports (1 non-report test probe), 0 unknown after the namespace fix.
516 lines
19 KiB
YAML
516 lines
19 KiB
YAML
services:
|
|
# ── Residential proxy relay (healthcare NPPES/PECOS automation) ──────
|
|
# Chromium cannot use an *authenticated* SOCKS5 proxy directly, so we run a
|
|
# local unauthenticated gost relay that forwards to the authenticated
|
|
# residential upstream (username "performancewest"). The workers point
|
|
# Playwright at this relay via HEALTHCARE_PROXY_URL=socks5://proxy-relay:11080.
|
|
#
|
|
# HEALTHCARE_PROXY_UPSTREAM_URL is the authenticated upstream and is set in
|
|
# .env (rendered from the ansible vault). The password may contain URL
|
|
# special chars (e.g. '#'); store it percent-encoded ('%23') in that var.
|
|
proxy-relay:
|
|
image: ginuerzh/gost:2.11.5
|
|
command: >-
|
|
-L socks5://:11080
|
|
-F ${HEALTHCARE_PROXY_UPSTREAM_URL}
|
|
restart: unless-stopped
|
|
|
|
# ── Core Application ────────────────────────────────────────────────
|
|
site:
|
|
build: ./site
|
|
ports:
|
|
- "4322:80"
|
|
restart: unless-stopped
|
|
|
|
api:
|
|
build: ./api
|
|
ports:
|
|
- "3001:3001"
|
|
env_file: .env
|
|
environment:
|
|
- NODE_ENV=production
|
|
- PORT=3001
|
|
- DOMAIN=performancewest.net
|
|
- DATABASE_URL=postgresql://pw:${DB_PASSWORD:-pw_dev_2026}@api-postgres:5432/performancewest
|
|
- ERPNEXT_URL=http://erpnext:8000
|
|
- ERPNEXT_API_KEY=${ERPNEXT_API_KEY}
|
|
- ERPNEXT_API_SECRET=${ERPNEXT_API_SECRET}
|
|
- ERPNEXT_SITE_NAME=performancewest.net
|
|
- WORKER_URL=http://workers:8090
|
|
- STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY}
|
|
- STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET}
|
|
- STRIPE_TEST_SECRET_KEY=${STRIPE_TEST_SECRET_KEY}
|
|
- STRIPE_TEST_WEBHOOK_SECRET=${STRIPE_TEST_WEBHOOK_SECRET}
|
|
- STRIPE_TEST_IDENTITY_WEBHOOK_SECRET=${STRIPE_TEST_IDENTITY_WEBHOOK_SECRET}
|
|
- CUSTOMER_JWT_SECRET=${CUSTOMER_JWT_SECRET}
|
|
- ADMIN_JWT_SECRET=${ADMIN_JWT_SECRET}
|
|
- ADMIN_EMAIL=${ADMIN_EMAIL:-ops@performancewest.net}
|
|
- SMTP_HOST=${SMTP_HOST}
|
|
- SMTP_PORT=${SMTP_PORT}
|
|
- SMTP_USER=${SMTP_USER}
|
|
- SMTP_PASS=${SMTP_PASS}
|
|
- SMTP_FROM=${SMTP_FROM}
|
|
- MINIO_ENDPOINT=minio
|
|
- MINIO_PORT=9000
|
|
- MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY}
|
|
- MINIO_SECRET_KEY=${MINIO_SECRET_KEY}
|
|
- PAYPAL_CLIENT_ID=${PAYPAL_CLIENT_ID}
|
|
- PAYPAL_CLIENT_SECRET=${PAYPAL_CLIENT_SECRET}
|
|
- PAYPAL_API_URL=https://api-m.paypal.com
|
|
- SHKEEPER_URL=${SHKEEPER_URL:-http://127.0.0.1:5000}
|
|
- SHKEEPER_API_KEY=${SHKEEPER_API_KEY}
|
|
- SHKEEPER_PUBLIC_URL=${SHKEEPER_PUBLIC_URL:-https://crypto.performancewest.net}
|
|
- WEBHOOK_SECRET=${WEBHOOK_SECRET}
|
|
- LISTMONK_URL=http://listmonk:9000
|
|
- LISTMONK_USER=${LISTMONK_USER:-admin}
|
|
- LISTMONK_PASSWORD=${LISTMONK_PASSWORD}
|
|
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
|
|
- TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID}
|
|
depends_on:
|
|
- api-postgres
|
|
restart: unless-stopped
|
|
|
|
api-postgres:
|
|
image: postgres:16-alpine
|
|
environment:
|
|
- POSTGRES_USER=pw
|
|
- POSTGRES_PASSWORD=${DB_PASSWORD:-pw_dev_2026}
|
|
- POSTGRES_DB=performancewest
|
|
ports:
|
|
- "5432:5432"
|
|
volumes:
|
|
- api-pgdata:/var/lib/postgresql/data
|
|
restart: unless-stopped
|
|
|
|
workers:
|
|
build:
|
|
context: .
|
|
dockerfile: scripts/Dockerfile
|
|
env_file: .env
|
|
environment:
|
|
- DATABASE_URL=postgresql://pw:${DB_PASSWORD:-pw_dev_2026}@api-postgres:5432/performancewest
|
|
- NODE_ENV=production
|
|
- DOMAIN=performancewest.net
|
|
- ERPNEXT_URL=http://erpnext:8000
|
|
- ERPNEXT_API_KEY=${ERPNEXT_API_KEY}
|
|
- ERPNEXT_API_SECRET=${ERPNEXT_API_SECRET}
|
|
- ERPNEXT_SITE_NAME=performancewest.net
|
|
- MINIO_ENDPOINT=minio
|
|
- MINIO_PORT=9000
|
|
- MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY}
|
|
- MINIO_SECRET_KEY=${MINIO_SECRET_KEY}
|
|
- MINIO_BUCKET=performancewest
|
|
- MINIO_SECURE=false
|
|
- ADMIN_EMAIL=${ADMIN_EMAIL:-ops@performancewest.net}
|
|
- SMTP_HOST=${SMTP_HOST}
|
|
- SMTP_PORT=${SMTP_PORT}
|
|
- SMTP_USER=${SMTP_USER}
|
|
- SMTP_PASS=${SMTP_PASS}
|
|
- SMTP_FROM=${SMTP_FROM}
|
|
- STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY}
|
|
- STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET}
|
|
- CUSTOMER_JWT_SECRET=${CUSTOMER_JWT_SECRET}
|
|
- OLLAMA_HOST=http://ollama:11434
|
|
- USE_DOCSERVER=true
|
|
- DOCSERVER_TIMEOUT=120
|
|
- FCC_CORES_USERNAME=${FCC_CORES_USERNAME}
|
|
- FCC_CORES_PASSWORD=${FCC_CORES_PASSWORD}
|
|
- OPS_IMAP_HOST=${OPS_IMAP_HOST:-mail.performancewest.net}
|
|
- OPS_IMAP_PORT=${OPS_IMAP_PORT:-993}
|
|
- OPS_IMAP_USER=${OPS_IMAP_USER}
|
|
- OPS_IMAP_PASS=${OPS_IMAP_PASS}
|
|
# DMARC aggregate-report ingestion mailbox (scripts.dmarc_report_parser)
|
|
- DMARC_IMAP_HOST=${DMARC_IMAP_HOST:-mail.performancewest.net}
|
|
- DMARC_IMAP_PORT=${DMARC_IMAP_PORT:-993}
|
|
- DMARC_IMAP_USER=${DMARC_IMAP_USER:-dmarc@performancewest.net}
|
|
- DMARC_IMAP_PASS=${DMARC_IMAP_PASS}
|
|
- FROM_EMAIL=Performance West <noreply@performancewest.net>
|
|
- CRYPTO_SWEEP_ADMIN_EMAIL=${ADMIN_EMAIL:-ops@performancewest.net}
|
|
- USAC_USERNAME=${USAC_USERNAME}
|
|
- USAC_PASSWORD=${USAC_PASSWORD}
|
|
# Healthcare (NPPES/PECOS) Playwright flows egress through the residential
|
|
# proxy via the unauthenticated gost relay sidecar (Chromium can't do
|
|
# authenticated SOCKS5). proxy-relay forwards to the authenticated upstream.
|
|
- HEALTHCARE_PROXY_URL=${HEALTHCARE_PROXY_URL:-socks5://proxy-relay:11080}
|
|
- UNDETECTED_PROXY_URL=${UNDETECTED_PROXY_URL:-socks5://proxy-relay:11080}
|
|
- ANYTIME_MAILBOX_SIGNUP_EMAIL=${ANYTIME_MAILBOX_SIGNUP_EMAIL:-noreply@performancewest.net}
|
|
- ANYTIME_MAILBOX_SIGNUP_PHONE=${ANYTIME_MAILBOX_SIGNUP_PHONE}
|
|
- ANYTIME_MAILBOX_DEFAULT_PASSWORD=${ANYTIME_MAILBOX_DEFAULT_PASSWORD}
|
|
- ANYTIME_MAILBOX_IMAP_HOST=${ANYTIME_MAILBOX_IMAP_HOST:-co.carrierone.com}
|
|
- ANYTIME_MAILBOX_IMAP_PORT=${ANYTIME_MAILBOX_IMAP_PORT:-993}
|
|
- ANYTIME_MAILBOX_IMAP_USER=${ANYTIME_MAILBOX_IMAP_USER:-noreply@performancewest.net}
|
|
- ANYTIME_MAILBOX_IMAP_PASS=${ANYTIME_MAILBOX_IMAP_PASS}
|
|
- ANYTIME_MAILBOX_IMAP_SSL=${ANYTIME_MAILBOX_IMAP_SSL:-true}
|
|
- ANYTIME_MAILBOX_IMAP_FOLDER=${ANYTIME_MAILBOX_IMAP_FOLDER:-INBOX}
|
|
- ANYTIME_MAILBOX_OTP_SENDER_HINT=${ANYTIME_MAILBOX_OTP_SENDER_HINT:-anytimemailbox}
|
|
- ANYTIME_MAILBOX_OTP_CODE=${ANYTIME_MAILBOX_OTP_CODE}
|
|
- ANYTIME_MAILBOX_OTP_POLL_SECONDS=${ANYTIME_MAILBOX_OTP_POLL_SECONDS:-6}
|
|
- ANYTIME_MAILBOX_OTP_TIMEOUT_SECONDS=${ANYTIME_MAILBOX_OTP_TIMEOUT_SECONDS:-180}
|
|
# Listmonk source campaign IDs cloned by scripts.build_trucking_campaigns
|
|
# for the daily trucking deficiency-flag segments. Set in .env after
|
|
# running scripts/create_deficiency_source_campaigns.py on prod.
|
|
- CAMPAIGN_FOR_HIRE_ID=${CAMPAIGN_FOR_HIRE_ID}
|
|
- CAMPAIGN_IRP_IFTA_ID=${CAMPAIGN_IRP_IFTA_ID}
|
|
- CAMPAIGN_INTRASTATE_ID=${CAMPAIGN_INTRASTATE_ID}
|
|
- CAMPAIGN_WEIGHT_TAX_ID=${CAMPAIGN_WEIGHT_TAX_ID}
|
|
- CAMPAIGN_EMISSIONS_ID=${CAMPAIGN_EMISSIONS_ID}
|
|
- CAMPAIGN_HAZMAT_ID=${CAMPAIGN_HAZMAT_ID}
|
|
volumes:
|
|
- worker-data:/app/data
|
|
# Read-only host MTA warmup stamp so the trucking campaign builder caps
|
|
# daily queued recipients in lockstep with Postfix/Listmonk warmup.
|
|
- /etc/postfix/pw-warmup-start:/etc/postfix/pw-warmup-start:ro
|
|
depends_on:
|
|
- api-postgres
|
|
- proxy-relay
|
|
restart: unless-stopped
|
|
|
|
# ── ERPNext CRM ─────────────────────────────────────────────────────
|
|
erpnext:
|
|
image: performancewest-erpnext:latest
|
|
build:
|
|
context: ./erpnext
|
|
dockerfile: Dockerfile
|
|
ports:
|
|
- "8080:8000"
|
|
environment:
|
|
- DB_HOST=erpnext-mariadb
|
|
- DB_PORT=3306
|
|
- DB_NAME=erpnext
|
|
- DB_PASSWORD=${ERPNEXT_DB_PASSWORD:-d5Webu5n0LLW2GrmKDHCf5xPliVKO1Kd9XErpWRP}
|
|
- REDIS_CACHE=redis://erpnext-redis:6379/0
|
|
- REDIS_QUEUE=redis://erpnext-redis:6379/1
|
|
- REDIS_SOCKETIO=redis://erpnext-redis:6379/2
|
|
- SOCKETIO_PORT=9000
|
|
# Portal needs these: CUSTOMER_JWT_SECRET verifies the /set-password magic
|
|
# link tokens signed by the API; DATABASE_URL lets the /orders portal page
|
|
# read compliance_orders from Postgres. Without them the set-password link
|
|
# shows "Link invalid" and the portal Compliance section is empty.
|
|
- CUSTOMER_JWT_SECRET=${CUSTOMER_JWT_SECRET}
|
|
- DATABASE_URL=postgresql://pw:${DB_PASSWORD:-pw_dev_2026}@api-postgres:5432/performancewest
|
|
# Outgoing mail: the "Performance West Outgoing" Email Account password is
|
|
# reconciled from these on `bench migrate` (after_migrate hook), so the
|
|
# account can never be left with awaiting_password=1 / empty password.
|
|
- SMTP_HOST=${SMTP_HOST}
|
|
- SMTP_PORT=${SMTP_PORT}
|
|
- SMTP_USER=${SMTP_USER}
|
|
- SMTP_PASS=${SMTP_PASS}
|
|
- SMTP_FROM=${SMTP_FROM}
|
|
volumes:
|
|
- erpnext-frappe-public:/home/frappe/frappe-bench/apps/frappe/frappe/public
|
|
- erpnext-erpnext-public:/home/frappe/frappe-bench/apps/erpnext/erpnext/public
|
|
- erpnext-logs:/home/frappe/frappe-bench/logs
|
|
- erpnext-sites:/home/frappe/frappe-bench/sites
|
|
depends_on:
|
|
- erpnext-mariadb
|
|
- erpnext-redis
|
|
restart: unless-stopped
|
|
|
|
erpnext-mariadb:
|
|
image: mariadb:10.6
|
|
environment:
|
|
- MYSQL_ROOT_PASSWORD=${ERPNEXT_DB_PASSWORD:-d5Webu5n0LLW2GrmKDHCf5xPliVKO1Kd9XErpWRP}
|
|
volumes:
|
|
- erpnext-mariadb-data:/var/lib/mysql
|
|
restart: unless-stopped
|
|
|
|
erpnext-redis:
|
|
image: redis:7-alpine
|
|
restart: unless-stopped
|
|
|
|
erpnext-scheduler:
|
|
image: performancewest-erpnext:latest
|
|
command: bench schedule
|
|
volumes:
|
|
- erpnext-sites:/home/frappe/frappe-bench/sites
|
|
- erpnext-logs:/home/frappe/frappe-bench/logs
|
|
environment:
|
|
- DB_HOST=erpnext-mariadb
|
|
- DB_PORT=3306
|
|
- DB_NAME=erpnext
|
|
- DB_PASSWORD=${ERPNEXT_DB_PASSWORD:-d5Webu5n0LLW2GrmKDHCf5xPliVKO1Kd9XErpWRP}
|
|
- REDIS_CACHE=redis://erpnext-redis:6379/0
|
|
- REDIS_QUEUE=redis://erpnext-redis:6379/1
|
|
- REDIS_SOCKETIO=redis://erpnext-redis:6379/2
|
|
# Daily scheduler self-heals the outgoing Email Account password from these
|
|
# (email_account_sync.sync_outgoing_password), covering drift from
|
|
# out-of-band restarts / DB restores.
|
|
- SMTP_HOST=${SMTP_HOST}
|
|
- SMTP_PORT=${SMTP_PORT}
|
|
- SMTP_USER=${SMTP_USER}
|
|
- SMTP_PASS=${SMTP_PASS}
|
|
- SMTP_FROM=${SMTP_FROM}
|
|
depends_on:
|
|
- erpnext-mariadb
|
|
- erpnext-redis
|
|
restart: unless-stopped
|
|
|
|
erpnext-worker-default:
|
|
image: performancewest-erpnext:latest
|
|
command: bench worker --queue default
|
|
volumes:
|
|
- erpnext-sites:/home/frappe/frappe-bench/sites
|
|
- erpnext-logs:/home/frappe/frappe-bench/logs
|
|
environment:
|
|
- DB_HOST=erpnext-mariadb
|
|
- DB_PORT=3306
|
|
- DB_NAME=erpnext
|
|
- DB_PASSWORD=${ERPNEXT_DB_PASSWORD:-d5Webu5n0LLW2GrmKDHCf5xPliVKO1Kd9XErpWRP}
|
|
- REDIS_CACHE=redis://erpnext-redis:6379/0
|
|
- REDIS_QUEUE=redis://erpnext-redis:6379/1
|
|
- REDIS_SOCKETIO=redis://erpnext-redis:6379/2
|
|
depends_on:
|
|
- erpnext-mariadb
|
|
- erpnext-redis
|
|
restart: unless-stopped
|
|
|
|
erpnext-worker-short:
|
|
image: performancewest-erpnext:latest
|
|
command: bench worker --queue short
|
|
volumes:
|
|
- erpnext-sites:/home/frappe/frappe-bench/sites
|
|
- erpnext-logs:/home/frappe/frappe-bench/logs
|
|
environment:
|
|
- DB_HOST=erpnext-mariadb
|
|
- DB_PORT=3306
|
|
- DB_NAME=erpnext
|
|
- DB_PASSWORD=${ERPNEXT_DB_PASSWORD:-d5Webu5n0LLW2GrmKDHCf5xPliVKO1Kd9XErpWRP}
|
|
- REDIS_CACHE=redis://erpnext-redis:6379/0
|
|
- REDIS_QUEUE=redis://erpnext-redis:6379/1
|
|
- REDIS_SOCKETIO=redis://erpnext-redis:6379/2
|
|
depends_on:
|
|
- erpnext-mariadb
|
|
- erpnext-redis
|
|
restart: unless-stopped
|
|
|
|
# ── Supporting Services ─────────────────────────────────────────────
|
|
ollama:
|
|
image: ollama/ollama:latest
|
|
volumes:
|
|
- ollama-data:/root/.ollama
|
|
restart: unless-stopped
|
|
|
|
minio:
|
|
image: minio/minio:latest
|
|
command: server /data --console-address ":9001"
|
|
ports:
|
|
- "127.0.0.1:9000:9000"
|
|
- "127.0.0.1:9001:9001"
|
|
environment:
|
|
- MINIO_ROOT_USER=${MINIO_ACCESS_KEY}
|
|
- MINIO_ROOT_PASSWORD=${MINIO_SECRET_KEY}
|
|
volumes:
|
|
- minio-data:/data
|
|
restart: unless-stopped
|
|
|
|
listmonk:
|
|
image: listmonk/listmonk:latest
|
|
# Stable hostname so the Message-ID Listmonk derives from the container OS
|
|
# hostname is perfwest.performancewest.net, NOT the random docker container
|
|
# id -> @localhost.localdomain (a spam-score signal; see deliverability
|
|
# runbook). Matches Listmonk's SMTP hello_hostname.
|
|
hostname: perfwest.performancewest.net
|
|
ports:
|
|
- "9100:9000"
|
|
environment:
|
|
- LISTMONK_app__address=0.0.0.0:9000
|
|
- LISTMONK_db__host=api-postgres
|
|
- LISTMONK_db__port=5432
|
|
- LISTMONK_db__user=pw
|
|
- LISTMONK_db__password=${DB_PASSWORD:-pw_dev_2026}
|
|
- LISTMONK_db__database=listmonk
|
|
- LISTMONK_db__ssl_mode=disable
|
|
- LISTMONK_db__max_open=25
|
|
- LISTMONK_db__max_idle=25
|
|
- LISTMONK_db__max_lifetime=300s
|
|
- LISTMONK_ADMIN_USER=${LISTMONK_USER:-admin}
|
|
- LISTMONK_ADMIN_PASSWORD=${LISTMONK_PASSWORD}
|
|
- TZ=America/New_York
|
|
volumes:
|
|
- listmonk-uploads:/listmonk/uploads
|
|
depends_on:
|
|
- api-postgres
|
|
restart: unless-stopped
|
|
|
|
# Second Listmonk instance for the HEALTHCARE HOT stream (dual-stream email
|
|
# design, see docs/healthcare-email-stream-plan.md). Fully isolated from the
|
|
# trucking-discipline instance above:
|
|
# - its own DB (listmonk_hc) => separate contacts / bounce / complaint state
|
|
# - its own sliding-window cap (the healthcare 10k/day ceiling), driven by
|
|
# pw-hc-rampcap off /etc/postfix/hc-warmup-start
|
|
# - sends ONLY via the host Postfix hc submission ports 2526/2527/2528, which
|
|
# egress from the dedicated hc IPs .107/.108/.109 (hcmta01-03). Configure
|
|
# these three as Listmonk SMTP servers so it round-robins the hc IPs.
|
|
# Reaches the host MTA via the docker bridge gateway 172.18.0.1 (in postfix
|
|
# mynetworks 172.16/12). host.docker.internal is mapped for convenience.
|
|
listmonk-hc:
|
|
image: listmonk/listmonk:latest
|
|
# Stable hostname -> Message-ID @perfwest.performancewest.net, not the random
|
|
# container id -> @localhost.localdomain (spam-score signal). See listmonk above.
|
|
hostname: perfwest.performancewest.net
|
|
ports:
|
|
- "9101:9000"
|
|
extra_hosts:
|
|
- "host.docker.internal:host-gateway"
|
|
environment:
|
|
- LISTMONK_app__address=0.0.0.0:9000
|
|
- LISTMONK_db__host=api-postgres
|
|
- LISTMONK_db__port=5432
|
|
- LISTMONK_db__user=pw
|
|
- LISTMONK_db__password=${DB_PASSWORD:-pw_dev_2026}
|
|
- LISTMONK_db__database=listmonk_hc
|
|
- LISTMONK_db__ssl_mode=disable
|
|
- LISTMONK_db__max_open=25
|
|
- LISTMONK_db__max_idle=25
|
|
- LISTMONK_db__max_lifetime=300s
|
|
- LISTMONK_ADMIN_USER=${LISTMONK_USER:-admin}
|
|
- LISTMONK_ADMIN_PASSWORD=${LISTMONK_HC_PASSWORD:-${LISTMONK_PASSWORD}}
|
|
- TZ=America/New_York
|
|
volumes:
|
|
- listmonk-hc-uploads:/listmonk/uploads
|
|
depends_on:
|
|
- api-postgres
|
|
restart: unless-stopped
|
|
|
|
umami:
|
|
image: ghcr.io/umami-software/umami:postgresql-latest
|
|
ports:
|
|
- "3100:3000"
|
|
environment:
|
|
- DATABASE_URL=postgresql://umami:umami_dev@umami-postgres:5432/umami
|
|
- APP_SECRET=${UMAMI_APP_SECRET:-0MnMHB71wtSYMF33Pm0L+MWLKezVtLGNwbkg++PZi5c=}
|
|
depends_on:
|
|
- umami-postgres
|
|
restart: unless-stopped
|
|
|
|
umami-postgres:
|
|
image: postgres:16-alpine
|
|
environment:
|
|
- POSTGRES_USER=umami
|
|
- POSTGRES_PASSWORD=umami_dev
|
|
- POSTGRES_DB=umami
|
|
volumes:
|
|
- umami-pgdata:/var/lib/postgresql/data
|
|
restart: unless-stopped
|
|
|
|
# ── Monitoring Stack ────────────────────────────────────────────────
|
|
prometheus:
|
|
image: prom/prometheus:latest
|
|
ports:
|
|
- "127.0.0.1:9090:9090"
|
|
volumes:
|
|
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
|
- ./monitoring/alert_rules.yml:/etc/prometheus/alert_rules.yml:ro
|
|
- prometheus-data:/prometheus
|
|
command:
|
|
- --config.file=/etc/prometheus/prometheus.yml
|
|
- --storage.tsdb.retention.time=90d
|
|
- --web.enable-lifecycle
|
|
- --web.enable-admin-api
|
|
extra_hosts:
|
|
- "host.docker.internal:host-gateway"
|
|
restart: unless-stopped
|
|
|
|
grafana:
|
|
image: grafana/grafana:latest
|
|
ports:
|
|
- "127.0.0.1:3200:3000"
|
|
environment:
|
|
- GF_SECURITY_ADMIN_USER=${GRAFANA_ADMIN_USER:-admin}
|
|
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:-pw_grafana_2026}
|
|
- GF_SERVER_ROOT_URL=https://monitoring.performancewest.net
|
|
- GF_SERVER_DOMAIN=monitoring.performancewest.net
|
|
- GF_SMTP_ENABLED=true
|
|
- GF_SMTP_HOST=${SMTP_HOST}:${SMTP_PORT}
|
|
- GF_SMTP_USER=${SMTP_USER}
|
|
- GF_SMTP_PASSWORD=${SMTP_PASS}
|
|
- GF_SMTP_FROM_ADDRESS=noreply@performancewest.net
|
|
- GF_USERS_ALLOW_SIGN_UP=false
|
|
- GF_AUTH_ANONYMOUS_ENABLED=false
|
|
- GF_SECURITY_DISABLE_BRUTE_FORCE_LOGIN_PROTECTION=true
|
|
volumes:
|
|
- grafana-data:/var/lib/grafana
|
|
- ./monitoring/grafana-datasources.yml:/etc/grafana/provisioning/datasources/datasources.yml:ro
|
|
depends_on:
|
|
- prometheus
|
|
restart: unless-stopped
|
|
|
|
alertmanager:
|
|
image: prom/alertmanager:latest
|
|
ports:
|
|
- "127.0.0.1:9093:9093"
|
|
volumes:
|
|
- ./monitoring/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
|
|
command:
|
|
- --config.file=/etc/alertmanager/alertmanager.yml
|
|
- --storage.path=/alertmanager
|
|
environment:
|
|
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
|
|
- TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID}
|
|
restart: unless-stopped
|
|
|
|
node-exporter:
|
|
image: prom/node-exporter:latest
|
|
command:
|
|
- --path.rootfs=/host
|
|
- --collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)
|
|
volumes:
|
|
- /:/host:ro,rslave
|
|
pid: host
|
|
restart: unless-stopped
|
|
|
|
cadvisor:
|
|
image: gcr.io/cadvisor/cadvisor:latest
|
|
volumes:
|
|
- /:/rootfs:ro
|
|
- /var/run:/var/run:ro
|
|
- /sys:/sys:ro
|
|
- /var/lib/docker/:/var/lib/docker:ro
|
|
- /dev/disk/:/dev/disk:ro
|
|
devices:
|
|
- /dev/kmsg
|
|
privileged: true
|
|
restart: unless-stopped
|
|
|
|
postgres-exporter:
|
|
image: prometheuscommunity/postgres-exporter:latest
|
|
environment:
|
|
- DATA_SOURCE_NAME=postgresql://pw:${DB_PASSWORD:-pw_dev_2026}@api-postgres:5432/performancewest?sslmode=disable
|
|
depends_on:
|
|
- api-postgres
|
|
restart: unless-stopped
|
|
|
|
nginx-exporter:
|
|
image: nginx/nginx-prometheus-exporter:latest
|
|
command:
|
|
- -nginx.scrape-uri=http://host.docker.internal:8888/nginx_status
|
|
- -nginx.timeout=5s
|
|
extra_hosts:
|
|
- "host.docker.internal:host-gateway"
|
|
ports:
|
|
- "127.0.0.1:9113:9113"
|
|
restart: unless-stopped
|
|
|
|
blackbox-exporter:
|
|
image: prom/blackbox-exporter:latest
|
|
volumes:
|
|
- ./monitoring/blackbox.yml:/etc/blackbox_exporter/config.yml:ro
|
|
extra_hosts:
|
|
- "host.docker.internal:host-gateway"
|
|
restart: unless-stopped
|
|
|
|
volumes:
|
|
api-pgdata:
|
|
worker-data:
|
|
ollama-data:
|
|
minio-data:
|
|
erpnext-frappe-public:
|
|
erpnext-erpnext-public:
|
|
erpnext-logs:
|
|
erpnext-sites:
|
|
erpnext-mariadb-data:
|
|
listmonk-uploads:
|
|
listmonk-hc-uploads:
|
|
umami-pgdata:
|
|
prometheus-data:
|
|
grafana-data:
|