326 lines
14 KiB
YAML
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
|