richardnixon.dev/infrastructure/docker-compose.yml
2026-09-07 04:01:00 +00:00

684 lines
21 KiB
YAML

services:
# ===========================================
# NGINX PROXY + SSL (Traefik)
# ===========================================
traefik:
image: traefik:v3.7.13
container_name: traefik
restart: unless-stopped
ports:
- "80:80"
- "443:443"
environment:
- DOCKER_API_VERSION=1.45
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik/traefik.yml:/etc/traefik/traefik.yml:ro
- ./traefik/dynamic.yml:/etc/traefik/dynamic.yml:ro
- traefik-certificates:/letsencrypt
- traefik-logs:/var/log/traefik
networks:
- web
- wordpress-internal
- locflow-internal
- crowdsec
- monitoring
- valheim-net
- authentik-internal
# ===========================================
# WORDPRESS - richardemanu.com
# ===========================================
wordpress-db:
image: mariadb:10.6
container_name: wordpress-db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: richardemanu
MYSQL_USER: richardemanu
MYSQL_PASSWORD: ${WP_DB_PASSWORD}
volumes:
- wordpress-db-data:/var/lib/mysql
networks:
- wordpress-internal
wordpress:
image: wordpress:6-php8.2-apache
container_name: wordpress
restart: unless-stopped
depends_on:
- wordpress-db
environment:
WORDPRESS_DB_HOST: wordpress-db
WORDPRESS_DB_NAME: richardemanu
WORDPRESS_DB_USER: richardemanu
WORDPRESS_DB_PASSWORD: ${WP_DB_PASSWORD}
volumes:
- wordpress-data:/var/www/html
- /root/data/wp-content:/var/www/html/wp-content
networks:
- web
- wordpress-internal
labels:
- "traefik.enable=true"
- "traefik.http.routers.wordpress.rule=Host(`richardemanu.com`) || Host(`www.richardemanu.com`)"
- "traefik.http.routers.wordpress.entrypoints=websecure"
- "traefik.http.routers.wordpress.tls.certresolver=letsencrypt"
- "traefik.http.services.wordpress.loadbalancer.server.port=80"
# ===========================================
# FORGEJO RUNNER - CI for self-hosted git
# ===========================================
forgejo-runner:
image: code.forgejo.org/forgejo/runner:6.2.2
container_name: forgejo-runner
restart: unless-stopped
depends_on:
forgejo:
condition: service_healthy
user: root
environment:
FORGEJO_INSTANCE_URL: http://forgejo:3000
FORGEJO_RUNNER_REGISTRATION_TOKEN: ${FORGEJO_RUNNER_REGISTRATION_TOKEN}
FORGEJO_RUNNER_NAME: vps-runner
volumes:
- forgejo-runner-data:/data
- ./forgejo-runner/config.yml:/data/config.yml:ro
- ./forgejo-runner/entrypoint.sh:/entrypoint.sh:ro
- /var/run/docker.sock:/var/run/docker.sock
networks:
- forgejo-internal
- web
entrypoint: ["/bin/sh", "/entrypoint.sh"]
# ===========================================
# BLOG STATIC (Hugo) - richardnixon.dev (cutover-ready, no Traefik labels yet)
# ===========================================
blog-static:
image: nginx:alpine
container_name: blog-static
restart: unless-stopped
volumes:
- /root/richardnixon.dev-hugo/public:/usr/share/nginx/html:ro
- ./blog-static/nginx.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- web
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost/"]
interval: 30s
timeout: 3s
retries: 3
# ===========================================
# UMAMI ANALYTICS
# ===========================================
umami-db:
image: postgres:16-alpine
container_name: umami-db
restart: unless-stopped
environment:
POSTGRES_DB: umami
POSTGRES_USER: umami
POSTGRES_PASSWORD: ${UMAMI_DB_PASSWORD}
volumes:
- umami-db-data:/var/lib/postgresql/data
networks:
- umami-internal
healthcheck:
test: ["CMD-SHELL", "pg_isready -U umami"]
interval: 10s
timeout: 5s
retries: 5
umami:
image: ghcr.io/umami-software/umami:postgresql-latest
container_name: umami
restart: unless-stopped
depends_on:
umami-db:
condition: service_healthy
environment:
DATABASE_URL: postgresql://umami:${UMAMI_DB_PASSWORD}@umami-db:5432/umami
DATABASE_TYPE: postgresql
APP_SECRET: ${UMAMI_APP_SECRET}
DISABLE_TELEMETRY: 1
networks:
- web
- umami-internal
labels:
- "traefik.enable=true"
- "traefik.http.routers.umami.rule=Host(`analytics.richardnixon.dev`)"
- "traefik.http.routers.umami.entrypoints=websecure"
- "traefik.http.routers.umami.tls.certresolver=letsencrypt"
- "traefik.http.services.umami.loadbalancer.server.port=3000"
# ===========================================
# OBSERVABILITY STACK (Grafana + Prometheus + Loki)
# ===========================================
prometheus:
image: prom/prometheus:latest
container_name: prometheus
restart: unless-stopped
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=30d'
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus-data:/prometheus
networks:
- monitoring
depends_on:
- cadvisor
loki:
image: grafana/loki:latest
container_name: loki
restart: unless-stopped
command: -config.file=/etc/loki/loki-config.yml
volumes:
- ./loki/loki-config.yml:/etc/loki/loki-config.yml:ro
- loki-data:/loki
networks:
- monitoring
promtail:
image: grafana/promtail:latest
container_name: promtail
restart: unless-stopped
command: -config.file=/etc/promtail/promtail-config.yml
volumes:
- ./promtail/promtail-config.yml:/etc/promtail/promtail-config.yml:ro
- /var/log:/var/log:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
networks:
- monitoring
depends_on:
- loki
cadvisor:
image: gcr.io/cadvisor/cadvisor:latest
container_name: cadvisor
restart: unless-stopped
privileged: true
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
- /dev/disk/:/dev/disk:ro
networks:
- monitoring
node-exporter:
image: prom/node-exporter:latest
container_name: node-exporter
restart: unless-stopped
command:
- '--path.procfs=/host/proc'
- '--path.sysfs=/host/sys'
- '--path.rootfs=/rootfs'
- '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
networks:
- monitoring
grafana:
image: grafana/grafana:latest
container_name: grafana
restart: unless-stopped
environment:
GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER:-admin}
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD}
GF_SERVER_ROOT_URL: https://status.richardnixon.dev
GF_INSTALL_PLUGINS: grafana-clock-panel,grafana-piechart-panel
volumes:
- grafana-data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning:ro
networks:
- web
- monitoring
depends_on:
- prometheus
- loki
labels:
- "traefik.enable=true"
- "traefik.http.routers.grafana.rule=Host(`status.richardnixon.dev`)"
- "traefik.http.routers.grafana.entrypoints=websecure"
- "traefik.http.routers.grafana.tls.certresolver=letsencrypt"
- "traefik.http.services.grafana.loadbalancer.server.port=3000"
# ===========================================
# CROWDSEC - Security & IPS
# ===========================================
crowdsec:
image: crowdsecurity/crowdsec:latest
container_name: crowdsec
restart: unless-stopped
environment:
COLLECTIONS: "crowdsecurity/traefik crowdsecurity/http-cve crowdsecurity/whitelist-good-actors crowdsecurity/linux"
GID: "${GID-1000}"
BOUNCER_KEY_traefik: ${CROWDSEC_BOUNCER_KEY:-}
volumes:
- ./crowdsec/acquis.yaml:/etc/crowdsec/acquis.yaml:ro
- crowdsec-db:/var/lib/crowdsec/data/
- crowdsec-config:/etc/crowdsec/
- /var/log:/var/log:ro
- traefik-logs:/logs/traefik:ro
networks:
- crowdsec
- monitoring
crowdsec-bouncer:
image: fbonalair/traefik-crowdsec-bouncer:0.5.0
container_name: crowdsec-bouncer
restart: unless-stopped
environment:
CROWDSEC_BOUNCER_API_KEY: ${CROWDSEC_BOUNCER_KEY:-}
CROWDSEC_AGENT_HOST: crowdsec:8080
GIN_MODE: release
networks:
- crowdsec
- web
depends_on:
- crowdsec
# ===========================================
# PORTAINER - Docker Management
# ===========================================
portainer:
image: portainer/portainer-ce:latest
container_name: portainer
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer-data:/data
networks:
- web
labels:
- "traefik.enable=true"
- "traefik.http.routers.portainer.rule=Host(`portainer.richardnixon.dev`)"
- "traefik.http.routers.portainer.entrypoints=websecure"
- "traefik.http.routers.portainer.tls.certresolver=letsencrypt"
- "traefik.http.services.portainer.loadbalancer.server.port=9000"
# ===========================================
# VALHEIM SERVER
# ===========================================
valheim:
image: lloesche/valheim-server
container_name: valheim
restart: unless-stopped
cap_add:
- sys_nice
environment:
SERVER_NAME: "Emerald Realms"
WORLD_NAME: "EmeraldRealms"
SERVER_PASS: ${VALHEIM_PASSWORD}
ADMINLIST_IDS: "76561198083287760,76561198024907057"
PERMITTEDLIST_IDS: "76561198083287760,76561198024907057"
SERVER_PUBLIC: "true"
BACKUPS: "true"
BACKUPS_INTERVAL: "43200"
BACKUPS_MAX_AGE: "7"
SUPERVISOR_HTTP: "true"
SUPERVISOR_HTTP_PORT: "9001"
STATUS_HTTP: "true"
STATUS_HTTP_PORT: "8080"
volumes:
- valheim-config:/config
- valheim-data:/opt/valheim
- valheim-logs:/var/log/valheim
ports:
- "2456-2458:2456-2458/udp"
networks:
- valheim-net
- monitoring
valheim-status:
build:
context: ./valheim-status
dockerfile: Dockerfile
container_name: valheim-status
restart: unless-stopped
depends_on:
- valheim
networks:
- web
- valheim-net
labels:
- "traefik.enable=true"
- "traefik.http.routers.valheim-status.rule=Host(`valheim.richardnixon.dev`)"
- "traefik.http.routers.valheim-status.entrypoints=websecure"
- "traefik.http.routers.valheim-status.tls.certresolver=letsencrypt"
- "traefik.http.services.valheim-status.loadbalancer.server.port=80"
valheim-metrics:
image: ghcr.io/adaliszk/valheim-server-monitoring:metrics
container_name: valheim-metrics
restart: unless-stopped
depends_on:
- valheim
volumes:
- valheim-logs:/var/log/valheim:ro
networks:
- monitoring
# ===========================================
# EIRESCOPE - OSINT Dashboard (eirescope.richardnixon.dev)
# ===========================================
eirescope:
build:
context: /root/eirescope
dockerfile: Dockerfile
container_name: eirescope
restart: unless-stopped
environment:
EIRESCOPE_DEBUG: "false"
EIRESCOPE_SECRET_KEY: ${EIRESCOPE_SECRET_KEY}
EIRESCOPE_UMAMI_WEBSITE_ID: ${EIRESCOPE_UMAMI_WEBSITE_ID:-}
networks:
- web
labels:
- "traefik.enable=true"
- "traefik.http.routers.eirescope.rule=Host(`eirescope.richardnixon.dev`)"
- "traefik.http.routers.eirescope.entrypoints=websecure"
- "traefik.http.routers.eirescope.tls.certresolver=letsencrypt"
- "traefik.http.services.eirescope.loadbalancer.server.port=5000"
# ===========================================
# LOCFLOW - Localization Platform (locflow.richardnixon.dev)
# ===========================================
locflow-db:
image: postgres:16-alpine
container_name: locflow-db
restart: unless-stopped
environment:
POSTGRES_DB: ${LOCFLOW_DB_NAME:-locflow}
POSTGRES_USER: ${LOCFLOW_DB_USER:-locflow}
POSTGRES_PASSWORD: ${LOCFLOW_DB_PASSWORD}
volumes:
- locflow-db-data:/var/lib/postgresql/data
networks:
- locflow-internal
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${LOCFLOW_DB_USER:-locflow}"]
interval: 10s
timeout: 5s
retries: 5
locflow-web:
build:
context: /root/LocFlow
dockerfile: docker/Dockerfile.prod
container_name: locflow-web
restart: unless-stopped
depends_on:
locflow-db:
condition: service_healthy
environment:
DEBUG: "0"
SECRET_KEY: ${LOCFLOW_SECRET_KEY}
POSTGRES_DB: ${LOCFLOW_DB_NAME:-locflow}
POSTGRES_USER: ${LOCFLOW_DB_USER:-locflow}
POSTGRES_PASSWORD: ${LOCFLOW_DB_PASSWORD}
POSTGRES_HOST: locflow-db
POSTGRES_PORT: 5432
ALLOWED_HOSTS: locflow.richardnixon.dev
CORS_ALLOWED_ORIGINS: https://locflow.richardnixon.dev
UMAMI_WEBSITE_ID: ${LOCFLOW_UMAMI_WEBSITE_ID:-}
DJANGO_SUPERUSER_EMAIL: ${LOCFLOW_SUPERUSER_EMAIL:-}
DJANGO_SUPERUSER_PASSWORD: ${LOCFLOW_SUPERUSER_PASSWORD:-}
volumes:
- locflow-static:/app/staticfiles
networks:
- web
- locflow-internal
healthcheck:
test: ["CMD", "curl", "--fail", "-H", "Host: locflow.richardnixon.dev", "http://localhost:8000/health/"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
labels:
- "traefik.enable=true"
- "traefik.http.services.locflow-api.loadbalancer.server.port=8000"
locflow-frontend:
build:
context: /root/LocFlow/frontend
dockerfile: Dockerfile.prod
args:
NEXT_PUBLIC_API_URL: https://locflow.richardnixon.dev/api
NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${LOCFLOW_UMAMI_WEBSITE_ID:-}
container_name: locflow-frontend
restart: unless-stopped
depends_on:
- locflow-web
networks:
- web
# ===========================================
# AUTHENTIK - SSO/2FA Identity Provider
# ===========================================
authentik-db:
image: postgres:16-alpine
container_name: authentik-db
restart: unless-stopped
environment:
POSTGRES_DB: authentik
POSTGRES_USER: authentik
POSTGRES_PASSWORD: ${AUTHENTIK_DB_PASSWORD}
volumes:
- authentik-db-data:/var/lib/postgresql/data
networks:
- authentik-internal
healthcheck:
test: ["CMD-SHELL", "pg_isready -U authentik"]
interval: 10s
timeout: 5s
retries: 5
authentik-redis:
image: redis:7-alpine
container_name: authentik-redis
restart: unless-stopped
volumes:
- authentik-redis-data:/data
networks:
- authentik-internal
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
authentik-server:
image: ghcr.io/goauthentik/server:2026.2
container_name: authentik-server
restart: unless-stopped
command: server
depends_on:
authentik-db:
condition: service_healthy
authentik-redis:
condition: service_healthy
environment:
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
AUTHENTIK_REDIS__HOST: authentik-redis
AUTHENTIK_POSTGRESQL__HOST: authentik-db
AUTHENTIK_POSTGRESQL__USER: authentik
AUTHENTIK_POSTGRESQL__NAME: authentik
AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_DB_PASSWORD}
AUTHENTIK_BOOTSTRAP_PASSWORD: ${AUTHENTIK_BOOTSTRAP_PASSWORD}
AUTHENTIK_BOOTSTRAP_EMAIL: ${AUTHENTIK_BOOTSTRAP_EMAIL}
volumes:
- authentik-media:/media
- authentik-templates:/templates
networks:
- web
- authentik-internal
labels:
- "traefik.enable=true"
- "traefik.http.routers.authentik.rule=Host(`auth.richardnixon.dev`)"
- "traefik.http.routers.authentik.entrypoints=websecure"
- "traefik.http.routers.authentik.tls.certresolver=letsencrypt"
- "traefik.http.services.authentik.loadbalancer.server.port=9000"
authentik-worker:
image: ghcr.io/goauthentik/server:2026.2
container_name: authentik-worker
restart: unless-stopped
command: worker
depends_on:
authentik-db:
condition: service_healthy
authentik-redis:
condition: service_healthy
environment:
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
AUTHENTIK_REDIS__HOST: authentik-redis
AUTHENTIK_POSTGRESQL__HOST: authentik-db
AUTHENTIK_POSTGRESQL__USER: authentik
AUTHENTIK_POSTGRESQL__NAME: authentik
AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_DB_PASSWORD}
networks:
- authentik-internal
# ===========================================
# FORGEJO - Git Hosting (git.richardnixon.dev)
# ===========================================
forgejo-db:
image: postgres:16-alpine
container_name: forgejo-db
restart: unless-stopped
environment:
POSTGRES_DB: forgejo
POSTGRES_USER: forgejo
POSTGRES_PASSWORD: ${FORGEJO_DB_PASSWORD}
volumes:
- forgejo-db-data:/var/lib/postgresql/data
networks:
- forgejo-internal
healthcheck:
test: ["CMD-SHELL", "pg_isready -U forgejo"]
interval: 10s
timeout: 5s
retries: 5
forgejo:
image: codeberg.org/forgejo/forgejo:11
container_name: forgejo
restart: unless-stopped
depends_on:
forgejo-db:
condition: service_healthy
environment:
USER_UID: "1000"
USER_GID: "1000"
FORGEJO__database__DB_TYPE: postgres
FORGEJO__database__HOST: forgejo-db:5432
FORGEJO__database__NAME: forgejo
FORGEJO__database__USER: forgejo
FORGEJO__database__PASSWD: ${FORGEJO_DB_PASSWORD}
FORGEJO__server__DOMAIN: git.richardnixon.dev
FORGEJO__server__ROOT_URL: https://git.richardnixon.dev/
FORGEJO__server__SSH_DOMAIN: git.richardnixon.dev
FORGEJO__server__SSH_PORT: "2222"
FORGEJO__server__SSH_LISTEN_PORT: "22"
FORGEJO__server__START_SSH_SERVER: "false"
FORGEJO__server__LFS_START_SERVER: "false"
FORGEJO__server__OFFLINE_MODE: "false"
FORGEJO__server__LANDING_PAGE: explore
FORGEJO__service__DISABLE_REGISTRATION: "true"
FORGEJO__service__ALLOW_ONLY_EXTERNAL_REGISTRATION: "true"
FORGEJO__service__REQUIRE_SIGNIN_VIEW: "false"
FORGEJO__service__DEFAULT_KEEP_EMAIL_PRIVATE: "true"
FORGEJO__service__ENABLE_BASIC_AUTHENTICATION: "false"
FORGEJO__oauth2_client__ENABLE_AUTO_REGISTRATION: "true"
FORGEJO__oauth2_client__ACCOUNT_LINKING: auto
FORGEJO__oauth2_client__USERNAME: nickname
FORGEJO__oauth2_client__UPDATE_AVATAR: "true"
FORGEJO__security__INSTALL_LOCK: "true"
FORGEJO__security__SECRET_KEY: ${FORGEJO_SECRET_KEY}
FORGEJO__security__INTERNAL_TOKEN: ${FORGEJO_INTERNAL_TOKEN}
FORGEJO__oauth2__JWT_SECRET: ${FORGEJO_OAUTH2_JWT_SECRET}
FORGEJO__metrics__ENABLED: "true"
FORGEJO__log__MODE: console
FORGEJO__log__LEVEL: info
FORGEJO__openid__ENABLE_OPENID_SIGNIN: "false"
FORGEJO__openid__ENABLE_OPENID_SIGNUP: "false"
FORGEJO__actions__ENABLED: "true"
FORGEJO__actions__DEFAULT_ACTIONS_URL: "https://code.forgejo.org"
volumes:
- forgejo-data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "2222:22"
networks:
- web
- forgejo-internal
- monitoring
healthcheck:
test: ["CMD", "curl", "--fail", "http://localhost:3000/api/healthz"]
interval: 30s
timeout: 5s
retries: 3
start_period: 60s
labels:
- "traefik.enable=true"
- "traefik.http.routers.forgejo.rule=Host(`git.richardnixon.dev`)"
- "traefik.http.routers.forgejo.entrypoints=websecure"
- "traefik.http.routers.forgejo.tls.certresolver=letsencrypt"
- "traefik.http.routers.forgejo.middlewares=crowdsec-bouncer@file"
- "traefik.http.services.forgejo.loadbalancer.server.port=3000"
networks:
web:
driver: bridge
wordpress-internal:
driver: bridge
umami-internal:
driver: bridge
locflow-internal:
driver: bridge
monitoring:
driver: bridge
crowdsec:
driver: bridge
valheim-net:
driver: bridge
authentik-internal:
driver: bridge
forgejo-internal:
driver: bridge
volumes:
traefik-certificates:
traefik-logs:
wordpress-db-data:
wordpress-data:
umami-db-data:
locflow-db-data:
locflow-static:
prometheus-data:
loki-data:
grafana-data:
portainer-data:
valheim-config:
valheim-data:
valheim-logs:
crowdsec-db:
crowdsec-config:
authentik-db-data:
authentik-redis-data:
authentik-media:
authentik-templates:
forgejo-data:
forgejo-db-data:
forgejo-runner-data: