services: traefik: image: traefik:v3 container_name: mf-traefik restart: unless-stopped env_file: .env ports: - "80:80" - "443:443" - "333:333" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - ./docker/traefik/:/letsencrypt - traefik-config:/etc/traefik - traefik-logs:/var/log/traefik command: - --api.dashboard=false - --api.insecure=false - --providers.docker=true - --providers.docker.exposedbydefault=false - --entrypoints.web.address=:80 - --entrypoints.websecure.address=:443 - --entrypoints.custom.address=:333 # Enable Traefik logs for CrowdSec - --accesslog=true - --accesslog.filepath=/var/log/traefik/access.log # CrowdSec Bouncer Plugin configuration - --experimental.plugins.crowdsec-bouncer-traefik-plugin.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin - --experimental.plugins.crowdsec-bouncer-traefik-plugin.version=v1.4.2 # HTTP to HTTPS redirect - --entrypoints.web.http.redirections.entryPoint.to=websecure - --entrypoints.web.http.redirections.entryPoint.scheme=https # Let's Encrypt configuration - --certificatesresolvers.linodedns.acme.email=${EMAIL} - --certificatesresolvers.linodedns.acme.storage=/letsencrypt/acme.json - --certificatesresolvers.linodedns.acme.dnschallenge=true - --certificatesresolvers.linodedns.acme.dnschallenge.provider=linode environment: - LINODE_TOKEN=${LINODE_API_KEY} labels: - "traefik.enable=true" - "traefik.http.routers.traefik.rule=Host(`traefik.${DOMAIN}`)" - "traefik.http.routers.traefik.entrypoints=custom" - "traefik.http.routers.traefik.service=api@internal" - "traefik.http.services.traefik.loadbalancer.server.port=8080" - "traefik.http.routers.traefik.tls=true" - "traefik.http.routers.traefik.tls.certresolver=linodedns" # CrowdSec Bouncer Plugin middleware configuration - "traefik.http.middlewares.crowdsec-bouncer.plugin.crowdsec-bouncer-traefik-plugin.crowdsecLapiKey=${CROWDSEC_BOUNCER_API_KEY}" - "traefik.http.middlewares.crowdsec-bouncer.plugin.crowdsec-bouncer-traefik-plugin.crowdsecLapiHost=crowdsec:8080" - "traefik.http.middlewares.crowdsec-bouncer.plugin.crowdsec-bouncer-traefik-plugin.crowdsecLapiScheme=http" - "traefik.http.middlewares.crowdsec-bouncer.plugin.crowdsec-bouncer-traefik-plugin.logLevel=INFO" # Security Headers Middleware - "traefik.http.middlewares.sec-headers.headers.customResponseHeaders.Strict-Transport-Security=max-age=31536000; includeSubDomains; preload" - "traefik.http.middlewares.sec-headers.headers.customResponseHeaders.X-Content-Type-Options=nosniff" - "traefik.http.middlewares.sec-headers.headers.customResponseHeaders.X-Frame-Options=DENY" - "traefik.http.middlewares.sec-headers.headers.customResponseHeaders.X-XSS-Protection=1; mode=block" - "traefik.http.middlewares.sec-headers.headers.customResponseHeaders.Referrer-Policy=strict-origin-when-cross-origin" forgejo: image: codeberg.org/forgejo/forgejo:11 container_name: forgejo restart: unless-stopped env_file: .env environment: - USER_UID=1000 - USER_GID=1000 - FORGEJO__server__DOMAIN=git.${DOMAIN} - FORGEJO__server__ROOT_URL=https://git.${DOMAIN}:333 - FORGEJO__server__HTTP_PORT=3000 - FORGEJO__server__STATIC_ROOT_PATH=/data/gitea/public - FORGEJO__service__ENABLE_BASIC_AUTHENTICATION=true - FORGEJO__ui__DEFAULT_THEME=gitea - FORGEJO__repository__ENABLE_PUSH_CREATE_USER=true - FORGEJO__repository__ENABLE_PUSH_CREATE_ORG=true - FORGEJO__repository__PREFERRED_LICENSES=MIT,Apache-2.0,GPL-3.0 - FORGEJO__service__DISABLE_REGISTRATION=false - FORGEJO__service__REQUIRE_SIGNIN_VIEW=false - FORGEJO__picture__DISABLE_GRAVATAR=false - FORGEJO__repository__ENABLE_PUSH_CREATE=true - FORGEJO__repository__ACCESS_CONTROL_ALLOW_ORIGIN=* volumes: - forgejo-data:/data - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro labels: - "traefik.enable=true" - "traefik.http.routers.forgejo.rule=Host(`git.${DOMAIN}`)" - "traefik.http.routers.forgejo.tls=true" - "traefik.http.routers.forgejo.tls.certresolver=linodedns" - "traefik.http.routers.forgejo.entrypoints=custom" - "traefik.http.services.forgejo.loadbalancer.server.port=3000" - "traefik.http.middlewares.forgejo-allowlist.ipallowlist.sourcerange=${TRUSTED_IPS}" - "traefik.http.routers.forgejo.middlewares=forgejo-allowlist,sec-headers,crowdsec-bouncer" jekyll: image: ruby:3.1 container_name: jekyll-builder restart: unless-stopped volumes: - ./sites/jekyll-source:/srv/jekyll - ./sites/jekyll-source/prod-site:/srv/jekyll/prod-site - ./sites/jekyll-source/dev-site:/srv/jekyll/dev-site environment: - JEKYLL_ENV=${JEKYLL_ENV:-development} - BUILD_TARGET=${BUILD_TARGET:-prod} - ADMIN_MODE=${ADMIN_MODE:-false} command: > /bin/sh -c ' apt-get update && apt-get install -y build-essential libffi-dev zlib1g-dev && cd /srv/jekyll && gem install bundler jekyll jekyll-admin && bundle install && set -e # Exit on any error echo "Build target: $BUILD_TARGET" echo "Admin mode: ${ADMIN_MODE:-false}" if [ "$BUILD_TARGET" = "prod" ]; then echo "Building production site..." if [ "$ADMIN_MODE" = "true" ]; then echo "Starting Jekyll server in admin mode (prod)..." bundle exec jekyll serve \ --host 0.0.0.0 \ --port 4000 \ --config _config.yml,_config_prod.yml,_config_admin.yml \ --force_polling --destination ./prod-site else bundle exec jekyll build \ --config _config.yml,_config_prod.yml \ --destination /srv/jekyll/prod-site \ --force_polling fi elif [ "$BUILD_TARGET" = "dev" ]; then if [ "$ADMIN_MODE" = "true" ]; then echo "Starting Jekyll server in admin mode (dev)..." bundle exec jekyll serve \ --host 0.0.0.0 \ --port 4000 \ --config _config.yml,_config_dev.yml,_config_admin.yml \ --force_polling --destination ./dev-site else echo "Building development site..." bundle exec jekyll build \ --watch \ --force_polling \ --config _config.yml,_config_dev.yml \ --destination /srv/jekyll/dev-site fi elif [ "$BUILD_TARGET" = "both" ]; then echo "Building both dev and prod sites..." # PROD (non-watching) bundle exec jekyll build \ --config _config.yml,_config_prod.yml \ --destination /srv/jekyll/prod-site & # DEV (non-serving, non-watching) bundle exec jekyll build \ --config _config.yml,_config_dev.yml \ --destination /srv/jekyll/dev-site & wait else echo "❌ Unknown or missing BUILD_TARGET: $BUILD_TARGET" echo "Please set BUILD_TARGET to dev, prod, or both" exit 1 fi ' labels: - "traefik.enable=true" - "traefik.http.routers.jekyll-admin.rule=Host(`admin.${DOMAIN}`)" - "traefik.http.routers.jekyll-admin.tls=true" - "traefik.http.routers.jekyll-admin.tls.certresolver=linodedns" - "traefik.http.routers.jekyll-admin.entrypoints=custom" - "traefik.http.services.jekyll-admin.loadbalancer.server.port=4000" - "traefik.http.middlewares.jekyll-allowlist.ipallowlist.sourcerange=${TRUSTED_IPS}" - "traefik.http.middlewares.jekyll-admin-auth.basicauth.users=${JEKYLL_ADMIN_USER_PASS}" - "traefik.http.routers.jekyll-admin.middlewares=jekyll-allowlist,jekyll-admin-auth,crowdsec-bouncer" nginx: image: nginx:alpine container_name: site-server restart: unless-stopped volumes: - ./docker/nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro - ./sites/jekyll-source/prod-site:/usr/share/nginx/html/prod:ro - ./sites/jekyll-source/dev-site:/usr/share/nginx/html/dev:ro labels: - "traefik.enable=true" # Main site configuration - "traefik.http.routers.site.rule=Host(`${DOMAIN}`)" - "traefik.http.routers.site.entrypoints=web,websecure" - "traefik.http.services.site.loadbalancer.server.port=80" - "traefik.http.routers.site.middlewares=sec-headers,crowdsec-bouncer,umami-cors,custom-server-header" - "traefik.http.routers.site.tls=true" - "traefik.http.routers.site.tls.certresolver=linodedns" # Main site CORS - "traefik.http.middlewares.prod-site-header.headers.accesscontrolallowmethods=GET,OPTIONS,PUT" - "traefik.http.middlewares.prod-site-header.headers.accesscontrolallowheaders=*" - "traefik.http.middlewares.prod-site-header.headers.accesscontrolalloworiginlist=https://fuckbigbro.${DOMAIN},https://${DOMAIN}" - "traefik.http.middlewares.prod-site-header.headers.accesscontrolmaxage=100" - "traefik.http.middlewares.prod-site-header.headers.addvaryheader=true" # Wildcard certificate definition - "traefik.http.routers.site.tls.domains[0].main=${DOMAIN}" - "traefik.http.routers.site.tls.domains[0].sans=*.${DOMAIN}" # Don't advertise your server stack unless you intend to: - "traefik.http.middlewares.custom-server-header.headers.customresponseheaders.Server=FuckOff" # Dev site configuration - "traefik.http.routers.site-dev.rule=Host(`dev.${DOMAIN}`)" - "traefik.http.routers.site-dev.entrypoints=custom" - "traefik.http.middlewares.site-allowlist.ipallowlist.sourcerange=${TRUSTED_IPS}" - "traefik.http.routers.site-dev.middlewares=site-allowlist,sec-headers,crowdsec-bouncer" crowdsec: image: crowdsecurity/crowdsec:latest container_name: crowdsec restart: unless-stopped environment: - GID=1000 - UID=1000 - COLLECTIONS=crowdsecurity/traefik crowdsecurity/http-cve crowdsecurity/nginx volumes: - crowdsec-data:/var/lib/crowdsec/data - crowdsec-config:/etc/crowdsec - ./docker/crowdsec/acquis.yaml:/etc/crowdsec/acquis.yaml:ro - traefik-logs:/var/log/traefik:ro depends_on: - traefik ######################################### ### BEGIN UMAMI ANALYTICS SECTION ### ######################################### umami_db: image: postgres:16-alpine restart: always volumes: - umami-db-data:/var/lib/postgresql/data environment: - POSTGRES_DB=umami - POSTGRES_USER=umami - POSTGRES_PASSWORD=${UMAMI_PG_PWD} # Set this in your .env healthcheck: test: ["CMD-SHELL", "pg_isready -U umami"] interval: 10s timeout: 5s retries: 5 start_period: 1m umami: image: ghcr.io/umami-software/umami:postgresql-latest restart: always depends_on: umami_db: condition: service_healthy environment: - DATABASE_URL=postgresql://umami:${UMAMI_PG_PWD}@umami_db:5432/umami - DATABASE_TYPE=postgresql - HASH_SALT=${UMAMI_HASH_SALT} - TRACKER_SCRIPT_NAME=script.js labels: - "traefik.enable=true" # Main Umami interface (admin dashboard) - "traefik.http.routers.umami.rule=Host(`fuckbigbro.${DOMAIN}`)" - "traefik.http.routers.umami.tls=true" - "traefik.http.routers.umami.tls.certresolver=linodedns" - "traefik.http.services.umami.loadbalancer.server.port=3000" - "traefik.http.routers.umami.entrypoints=custom" - "traefik.http.middlewares.umami-allowlist.ipallowlist.sourcerange=${TRUSTED_IPS}" - "traefik.http.routers.umami.middlewares=umami-allowlist,sec-headers,crowdsec-bouncer" # Public script endpoint - accessible via websecure (443) or web (80) - "traefik.http.routers.umami-script.rule=Host(`fuckbigbro.${DOMAIN}`) && Path(`/script.js`) && Method(`GET`)" - "traefik.http.routers.umami-script.entrypoints=websecure" - "traefik.http.routers.umami-script.service=umami" - "traefik.http.routers.umami-script.middlewares=crowdsec-bouncer" - "traefik.http.routers.umami-script.tls=true" - "traefik.http.routers.umami-script.tls.certresolver=linodedns" # Public API endpoint for collecting data - "traefik.http.routers.umami-api.rule=Host(`fuckbigbro.${DOMAIN}`) && PathPrefix(`/api/send`)" - "traefik.http.routers.umami-api.entrypoints=websecure" - "traefik.http.routers.umami-api.service=umami" - "traefik.http.routers.umami-api.middlewares=crowdsec-bouncer,sec-headers,umami-cors" - "traefik.http.routers.umami-api.tls=true" - "traefik.http.routers.umami-api.tls.certresolver=linodedns" # CORS middleware for API endpoints - "traefik.http.middlewares.umami-cors.headers.accesscontrolallowmethods=GET,POST,OPTIONS" - "traefik.http.middlewares.umami-cors.headers.accesscontrolalloworiginlist=https://fuckbigbro.${DOMAIN},https://${DOMAIN},https://www.${DOMAIN}" - "traefik.http.middlewares.umami-cors.headers.accesscontrolallowheaders=Content-Type,Authorization,X-Requested-With,X-Umami-Cache" - "traefik.http.middlewares.umami-cors.headers.accesscontrolmaxage=86400" - "traefik.http.middlewares.umami-cors.headers.addvaryheader=true" # CORS Preflight OPTIONS handler - "traefik.http.routers.umami-options.rule=Host(`fuckbigbro.${DOMAIN}`) && PathPrefix(`/api/send`) && Method(`OPTIONS`)" - "traefik.http.routers.umami-options.entrypoints=websecure" - "traefik.http.routers.umami-options.middlewares=umami-cors" - "traefik.http.routers.umami-options.tls=true" - "traefik.http.routers.umami-options.tls.certresolver=linodedns" - "traefik.http.routers.umami-options.service=noop@internal" volumes: umami-db-data: driver: local forgejo-data: driver: local traefik-config: driver: local crowdsec-data: driver: local crowdsec-config: driver: local traefik-logs: driver: local networks: default: name: traefik