motherfuckingblog/docker-compose.yml
Stewart Pidasso c021228161 init
2025-07-07 02:07:58 +00:00

326 lines
14 KiB
YAML

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