Initial Traefik compose stack commit

This commit is contained in:
2026-07-16 20:02:07 -03:00
commit ee4e8804c5
8 changed files with 622 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
### macOS
# Finder metadata
.DS_Store
# Thumbnails
._*
# Custom folder icons
Icon␍
# Volume root files
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
### Windows
# Windows thumbnail cache files
Thumbs.db
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows shortcuts
*.lnk
### Linux
# Backup files
*~
# Temporary files from deleted open files
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder
.Trash-*
# NFS temporary files
.nfs*
+217
View File
@@ -0,0 +1,217 @@
# Traefik reverse proxy stack
Docker Compose stack for [Traefik](https://traefik.io/) with Lets Encrypt (Cloudflare DNS challenge), a secured dashboard, and a reusable middleware library for security headers, CSP tiers, Authentik SSO, redirects, and IP allowlists.
## Prerequisites
- Docker Engine with Compose v2
- A Cloudflare account with API access for the domains you terminate here
- DNS for those domains pointed at the host that runs this stack
- A shared Docker network that your backend services also join (default: `docknet`)
## Quick start
```bash
git clone <this-repo> traefik && cd traefik
cp example.env .env
```
Edit `.env` (see [Environment variables](#environment-variables)), then:
```bash
docker compose --env-file .env up -d
```
Traefik will join (or create) the Docker network named by `NETWORK_NAME`, watch containers on that host for Traefik labels, and issue certificates via Cloudflare DNS. ACME data is stored in the Docker named volume `traefik-acme` (created automatically; no manual file setup).
## Layout
```
.
├── compose.yml # Traefik service, ports, dashboard labels, ACME volume
├── example.env # Env template (copy to .env)
└── traefik/
├── traefik.yaml # Static config: entrypoints, providers, TLS SANs
└── conf.d/
├── middlewares.yaml # Shared middleware library
├── routers.yaml # Optional file-based routers
└── services.yaml # Optional file-based services
```
Static config lives in `traefik/traefik.yaml`. Dynamic config (middlewares, optional routers/services) is loaded from `traefik/conf.d/` and hot-reloads when files change. Certificate state lives in the `traefik-acme` volume, not in the repo.
## Environment variables
Copy `example.env` to `.env` and fill in real values:
| Variable | Purpose |
| --- | --- |
| `CLOUDFLARE_EMAIL` | Cloudflare account email (ACME + API) |
| `CLOUDFLARE_API_KEY` | Cloudflare Global API Key (or scoped token with DNS edit) |
| `CONSOLE_HOST` | Hostname for the Traefik dashboard (e.g. `traefik.example.com`) |
| `NETWORK_NAME` | Docker network name shared with backends (default `docknet`) |
| `TRAEFIK_CONSOLE_AUTH` | Basic-auth users for the dashboard (`htpasswd` format) |
| `TRAEFIK_YAML` | Host path to static config (default `./traefik/traefik.yaml`) |
| `TRAEFIK_CONFD` | Host path to dynamic config dir (default `./traefik/conf.d`) |
Generate dashboard credentials:
```bash
# Install apache2-utils / httpd-tools if needed, then:
htpasswd -nbB admin 'your-strong-password'
```
Paste the output into `TRAEFIK_CONSOLE_AUTH`. If the value contains `$`, escape them for Compose (use `$$`) or quote carefully in `.env`.
## Ports
| Host | Container | Role |
| --- | --- | --- |
| `8342` | `80` | HTTP → redirects to HTTPS |
| `8767` | `443` | HTTPS |
| `8080` | `8080` | Traefik API / dashboard (also exposed via `CONSOLE_HOST` with TLS + basic auth) |
Point your public reverse path or firewall at `8767` (and `8342` if you want HTTP→HTTPS at the edge). Adjust the left-hand ports in `compose.yml` if you prefer `80`/`443` on the host.
## TLS and certificates
- Challenge: **DNS-01** via Cloudflare (`certificatesResolvers.letsencrypt`)
- Storage: Docker named volume `traefik-acme` mounted at `/letsencrypt` (Traefik creates `acme.json` on first run)
- Default certificate SANs for the TLS store are defined under `tls.stores.default` in `traefik/traefik.yaml` — edit that list for your domains
The volume survives `docker compose down` / `up`. Prefer `docker compose down` over `docker compose down -v` unless you intentionally want to wipe certs (which will re-hit Lets Encrypt rate limits).
Ensure Cloudflare credentials can create/delete `_acme-challenge` TXT records for every zone you serve.
## Dashboard
The dashboard is enabled and routed with Docker labels on the Traefik container:
- URL: `https://<CONSOLE_HOST>/`
- Entrypoint: `websecure`
- Auth: middleware `traefik-auth` (basic auth from `TRAEFIK_CONSOLE_AUTH`)
- Certificate: Lets Encrypt via `letsencrypt` resolver
## Using middlewares on your services
Connect apps to the same Docker network (`NETWORK_NAME`), enable Traefik, and attach middlewares by name with the `@file` provider:
```yaml
services:
myapp:
image: myapp:latest
networks:
- docknet
labels:
- traefik.enable=true
- traefik.http.routers.myapp.rule=Host(`app.example.com`)
- traefik.http.routers.myapp.entrypoints=websecure
- traefik.http.routers.myapp.tls.certresolver=letsencrypt
- traefik.http.routers.myapp.middlewares=security-supermax@file,gzip@file
- traefik.http.services.myapp.loadbalancer.server.port=8080
networks:
docknet:
external: true
name: docknet
```
Chain multiple middlewares with commas. File-provider names resolve as `<name>@file`.
## Middleware catalog
Defined in `traefik/conf.d/middlewares.yaml`.
### Security chains (pick one)
| Middleware | Intent |
| --- | --- |
| `default` | CSP strict + frame deny + gzip + HSTS + baseline security headers |
| `secure-all` | Permissive CSP + same-origin frames + HSTS + security headers |
| `security-mallcop` | Open referrer + open CSP + HSTS + security headers |
| `security-lax` | Lax referrer + lax CSP + HSTS + security headers |
| `security-medium` | Permissive referrer/CSP + same-origin frames + HSTS + security headers |
| `security-prison` | Medium referrer + balanced CSP + same-origin frames + HSTS + security headers |
| `security-supermax` | No-referrer + strict CSP + frame deny + HSTS + security headers |
| `security-supermax-with-analytics` | Supermax plus GA/Clarity/Umami-friendly CSP and COOP |
### CSP building blocks
| Middleware | Notes |
| --- | --- |
| `csp-open-header` | Very permissive (legacy / hard-to-lock apps) |
| `csp-lax-header` | Self + common inline/eval allowances |
| `csp-permissive-header` | Self-focused with inline/eval for scripts/styles |
| `csp-balanced-header` | Self + common CDNs (jsDelivr, cdnjs, unpkg, Google Fonts) |
| `csp-strict-header` | Self-only |
| `csp-strict-header-with-ga` | Strict plus analytics/Clarity/GTM endpoints |
### Other headers & utilities
| Middleware | Role |
| --- | --- |
| `security-headers` | `X-Content-Type-Options: nosniff`, disables legacy `X-XSS-Protection` |
| `hsts-header` | HSTS 2y, includeSubDomains, preload |
| `frame-deny-header` | `X-Frame-Options: DENY` |
| `frame-sameorigin-header` | `X-Frame-Options: SAMEORIGIN` |
| `cross-origin-opener-strict-policy` | `Cross-Origin-Opener-Policy: same-origin` |
| `gzip` | Response compression |
| `referrer-*-header` | Referrer-Policy tiers (`open``supermax`) |
| `strip-server-path` | Strips `/db`, `/server`, `/traefik` prefixes |
### Auth & access
| Middleware | Role |
| --- | --- |
| `authentik-outpost` | ForwardAuth to Authentik outpost (`outpost.mifi.holdings`) |
| `authentik-no-cache` | Disable caching on Authentik responses |
| `authentik-static-immutable` | Long-cache immutable static assets |
| `internal-ips` | IP allowlist (edit `sourceRange` for your networks) |
### Redirects
| Middleware | Role |
| --- | --- |
| `redirect-www-to-non-www` | `www.` → apex |
| `redirect-non-www-to-www` | apex → `www.` |
| `redirect-postfixadmin` | Mail settings path redirect (site-specific) |
Update Authentik outpost URL, IP allowlists, and domain-specific redirects in `middlewares.yaml` before deploying outside this environment.
## File-based routers and services
`traefik/conf.d/routers.yaml` and `services.yaml` are stubs for optional non-Docker backends. Uncomment or add entries when you need host/path rules that are not expressed as container labels.
## Operations
```bash
# Start / recreate after config or .env changes
docker compose --env-file .env up -d
# Follow logs
docker compose logs -f traefik
# Stop (keeps the ACME volume)
docker compose down
# Inspect ACME volume location (optional)
docker volume inspect traefik_traefik-acme
```
Dynamic files under `conf.d/` reload without a full restart. Changes to `traefik.yaml`, Compose labels, ports, or env vars generally need `docker compose up -d` (or a container recreate).
## Customization checklist
1. Set `.env` from `example.env` and generate `TRAEFIK_CONSOLE_AUTH`
2. Edit TLS SANs in `traefik/traefik.yaml` for your domains
3. Adjust `internal-ips` and Authentik URLs in `middlewares.yaml`
4. Ensure backends use `traefik.enable=true` and join `NETWORK_NAME`
5. Publish host ports (`8342`/`8767`) as needed for your network edge
## Security notes
- Do not commit `.env` or real API keys
- Prefer a scoped Cloudflare API token limited to DNS edit on required zones when possible
- Keep the dashboard behind strong basic auth (and consider further restricting with `internal-ips` or Authentik)
- Review CSP chains per app; start stricter and loosen only where breakage requires it
- Do not use `docker compose down -v` in production unless you mean to discard certificates
+62
View File
@@ -0,0 +1,62 @@
services:
traefik:
image: traefik:latest
container_name: traefik
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- ${TRAEFIK_YAML:-./traefik/traefik.yaml}:/etc/traefik/traefik.yaml
- ${TRAEFIK_CONFD:-./traefik/conf.d}:/etc/traefik/conf.d
- traefik-acme:/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock
networks:
- docknet
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.api.rule=Host(`${CONSOLE_HOST}`)'
- 'traefik.http.routers.api.entrypoints=websecure'
- 'traefik.http.routers.api.service=api@internal'
- 'traefik.http.routers.api.tls=true'
- 'traefik.http.routers.api.tls.certresolver=letsencrypt'
- 'traefik.http.routers.api.middlewares=traefik-auth'
- 'traefik.http.middlewares.traefik-auth.basicauth.users=${TRAEFIK_CONSOLE_AUTH}'
ports:
- 8342:80
- 8767:443
- 8080:8080
environment:
- CLOUDFLARE_EMAIL=${CLOUDFLARE_EMAIL}
- CLOUDFLARE_API_KEY=${CLOUDFLARE_API_KEY}
command:
- --api
# Enable the Trafik dashboard
- --api.dashboard=true
# Tell Traefik to discover containers using the Docker API
- --providers.docker=true
- --providers.docker.exposedByDefault=false
# Set up LetsEncrypt
- --certificatesresolvers.letsencrypt.acme.dnschallenge=true
- --certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare
- --certificatesresolvers.letsencrypt.acme.email=${CLOUDFLARE_EMAIL}
# Set up an insecure listener that redirects all traffic to TLS
- --entrypoints.web.address=:80
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entrypoints.web.http.redirections.entrypoint.scheme=https
- --entrypoints.websecure.address=:443
# Set up the TLS configuration for our websecure listener
- --entrypoints.websecure.http.tls=true
- --entrypoints.websecure.http.tls.certResolver=letsencrypt
- --log=true
- --log.level=INFO
logging:
driver: "json-file"
options:
max-size: "1m"
networks:
docknet:
name: ${NETWORK_NAME:-docknet}
volumes:
traefik-acme:
+7
View File
@@ -0,0 +1,7 @@
CLOUDFLARE_EMAIL=[EMAIL]
CLOUDFLARE_API_KEY=[API_KEY]
CONSOLE_HOST=traefik.mifi.dev
NETWORK_NAME=docknet
TRAEFIK_CONSOLE_AUTH=[username]:[password]
TRAEFIK_YAML=./traefik/traefik.yaml
TRAEFIK_CONFD=./traefik/conf.d
+189
View File
@@ -0,0 +1,189 @@
http:
middlewares:
default:
chain:
middlewares:
- csp-strict-header
- frame-deny-header
- gzip
- hsts-header
- security-headers
# middlewares
authentik-no-cache:
headers:
customResponseHeaders:
Cache-Control: "no-store, no-cache, must-revalidate, max-age=0"
Pragma: "no-cache"
Expires: "0"
authentik-outpost:
forwardAuth:
address: "https://outpost.mifi.holdings/outpost.goauthentik.io/auth/traefik"
trustForwardHeader: true
authResponseHeaders:
- X-authentik-username
- X-authentik-groups
- X-authentik-entitlements
- X-authentik-email
- X-authentik-name
- X-authentik-uid
- X-authentik-jwt
- X-authentik-meta-jwks
- X-authentik-meta-outpost
- X-authentik-meta-provider
- X-authentik-meta-app
- X-authentik-meta-version
authentik-static-immutable:
headers:
customResponseHeaders:
Cache-Control: "public, max-age=31536000, immutable"
cross-origin-opener-strict-policy:
headers:
customResponseHeaders:
Cross-Origin-Opener-Policy: "same-origin"
csp-open-header:
headers:
customResponseHeaders:
Content-Security-Policy: "default-src * data: blob: 'unsafe-inline' 'unsafe-eval'; script-src * data: blob: 'unsafe-inline' 'unsafe-eval'; style-src * data: blob: 'unsafe-inline'; img-src * data: blob:; font-src * data: blob:; frame-src *; frame-ancestors *; base-uri *; form-action *; frame-src *; connect-src 'self'; worker-src 'self' blob:;"
csp-lax-header:
headers:
customResponseHeaders:
Content-Security-Policy: "default-src 'self'; img-src 'self' data: https:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; font-src 'self' data: https:; frame-ancestors 'self'; frame-src 'self' https:; base-uri 'self'; form-action 'self' https:; connect-src 'self'; worker-src 'self' blob:;"
csp-permissive-header:
headers:
customResponseHeaders:
Content-Security-Policy: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; connect-src 'self'; worker-src 'self';"
csp-balanced-header:
headers:
customResponseHeaders:
Content-Security-Policy: "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://unpkg.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com data:; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; connect-src 'self'; worker-src 'self';"
csp-strict-header:
headers:
customResponseHeaders:
Content-Security-Policy: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; connect-src 'self'; worker-src 'self';"
csp-strict-header-with-ga:
headers:
customResponseHeaders:
Content-Security-Policy: "default-src 'self'; script-src 'self' https://analytics.mifi.holdings https://scripts.clarity.ms https://www.clarity.ms https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; img-src 'self' https://analytics.mifi.holdings https://*.clarity.ms https://c.bing.com https://www.google-analytics.com https://www.googletagmanager.com https://stats.g.doubleclick.net; font-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; connect-src 'self' https://analytics.mifi.holdings https://*.clarity.ms https://c.bing.com https://www.google.com https://www.google-analytics.com https://region1.google-analytics.com https://stats.g.doubleclick.net; worker-src 'self'; require-trusted-types-for 'script'; trusted-types default forceInner goog#html mifi-ventures-policy svelte-trusted-html 'allow-duplicates';"
frame-deny-header:
headers:
frameDeny: true # X-Frame-Options: DENY
frame-sameorigin-header:
headers:
customResponseHeaders:
X-Frame-Options: "SAMEORIGIN"
gzip:
compress: {}
hsts-header:
headers:
# Enable HSTS (Strict Transport Security)
stsSeconds: 63072000 # 2 years
stsIncludeSubdomains: true
stsPreload: true # for preload list
internal-ips:
ipWhiteList:
sourceRange:
- 127.0.0.1/32
- 192.168.50.1/32
- 50.116.61.55
- 173.255.238.101
- 72.93.250.18
redirect-postfixadmin:
redirectRegex:
regex: "^https://mail\\.mifi\\.holdings/settings/?(.*)"
replacement: "https://postmaster.mifi.holdings/$1"
permanent: true
redirect-www-to-non-www:
redirectRegex:
regex: "^https://www\\.(.*)"
replacement: "https://${1}"
permanent: true
redirect-non-www-to-www:
redirectregex:
permanent: true
regex: "^https?://(?:www\\.)?(.+)"
replacement: "https://www.${1}"
referrer-open-header:
headers:
customResponseHeaders:
Referrer-Policy: "unsafe-url"
referrer-lax-header:
headers:
customResponseHeaders:
Referrer-Policy: "origin-when-cross-origin"
referrer-permissive-header:
headers:
customResponseHeaders:
Referrer-Policy: "strict-origin-when-cross-origin"
referrer-medium-header:
headers:
customResponseHeaders:
Referrer-Policy: "same-origin"
referrer-supermax-header:
headers:
customResponseHeaders:
Referrer-Policy: "no-referrer"
security-headers:
headers:
contentTypeNosniff: true # X-Content-Type-Options: nosniff
customResponseHeaders:
X-XSS-Protection: "0" # Disable legacy browser XSS filter (modern best practice)
secure-all:
chain:
middlewares:
- csp-permissive-header
- frame-sameorigin-header
- hsts-header
- security-headers
security-mallcop:
chain:
middlewares:
- referrer-open-header
- csp-open-header
- hsts-header
- security-headers
security-lax:
chain:
middlewares:
- referrer-lax-header
- csp-lax-header
- hsts-header
- security-headers
security-medium:
chain:
middlewares:
- referrer-permissive-header
- csp-permissive-header
- frame-sameorigin-header
- hsts-header
- security-headers
security-prison:
chain:
middlewares:
- referrer-medium-header
- csp-balanced-header
- frame-sameorigin-header
- hsts-header
- security-headers
security-supermax:
chain:
middlewares:
- referrer-supermax-header
- csp-strict-header
- frame-deny-header
- hsts-header
- security-headers
security-supermax-with-analytics:
chain:
middlewares:
- referrer-supermax-header
- cross-origin-opener-strict-policy
- csp-strict-header-with-ga
- frame-deny-header
- hsts-header
- security-headers
strip-server-path:
stripPrefix:
prefixes:
- "/db"
- "/server"
- "/traefik"
+19
View File
@@ -0,0 +1,19 @@
http:
routers:
# redirect-postfixadmin:
# entryPoints:
# - websecure
# rule: "Host(`mail.mifi.holdings`) && PathPrefix(`/settings`)"
# middlewares:
# - redirect-postfixadmin@file
# service: webmin-service
# tls:
# certResolver: letsencrypt
# webmin:
# entryPoints:
# - websecure
# middlewares: strip-server-path@file
# rule: "Host(`mail.mifi.holdings`) && PathPrefix(`/server`)"
# service: webmin-service
# tls:
# certResolver: letsencrypt
+7
View File
@@ -0,0 +1,7 @@
http:
services:
# webmin-service:
# loadBalancer:
# passHostHeader: true
# servers:
# - url: "http://173.255.238.101:10000/"
+74
View File
@@ -0,0 +1,74 @@
api:
dashboard: true
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
traefik:
address: ":8080"
ping: {}
providers:
docker:
endpoint: unix:///var/run/docker.sock
watch: true
exposedByDefault: false
file:
directory: /etc/traefik/conf.d
log:
level: info
accesslog:
filepath: /dev/stdout
format: json
fields:
defaultmode: keep
headers:
defaultmode: keep
certificatesResolvers:
letsencrypt:
acme:
email: badmf@mifi.dev
storage: /letsencrypt/acme.json
dnsChallenge:
provider: cloudflare
resolvers:
- 1.1.1.1:53
- 1.0.0.1:53
tls:
stores:
default:
defaultGeneratedCert:
resolver: letsencrypt
domain:
main: mifi.holdings
sans:
- mifi.com.br
- mifi.dev
- mifi.ventures
- fitz.guru
- michael-gerard.com
- asgardianpunk.com
- camilla-rena.com
- dining-it.com
- goodfeatherfarms.com
- mylocalpro.biz
- officelift.net
- starnora.com
- starnoraelizabethfitzpatrick.com
- thenewenglandpalletguy.com
- umlautpress.com
serversTransport:
insecureSkipVerify: false