├── traefik │ ├── configfiles │ │ ├── config.yml │ │ ├── middleware-chains.yml │ │ ├── middlewares.yml │ │ ├── tls-opts.yml │ ├── docker-compose.yml │ ├── .env │ ├── traefik.log │ ├── traefik.yml │ ├── access.log
In .env steht die URL traefik.smns-bw.org sowie die Zugangsdaten für diese Seite für docker-compose.yml
In die einzelnen docker-compose.yml Files der Container kommt dann sowas (Beispiel Sammlungskatalog):
labels:
- "traefik.http.routers.webportal.rule=Host(`${URL}`)"
- "traefik.http.routers.webportal.entrypoints=https"
- "traefik.http.routers.webportal.tls=true"
- "traefik.http.routers.webportal.tls.certresolver=leresolver"
- "traefik.http.routers.webportal.middlewares=secure-collections@file"
- "traefik.http.services.webportal.loadbalancer.server.port=6543"
- "traefik.http.services.webportal.loadbalancer.sticky=true"
- "traefik.http.services.webportal.loadbalancer.sticky.cookie.name=collections.smns-bw.org"
- "traefik.http.services.webportal.loadbalancer.sticky.cookie.httpOnly=true"
- "traefik.http.services.webportal.loadbalancer.sticky.cookie.secure=true"
- "traefik.docker.network=proxy"
Damit der Docker.sock nicht nach außen exposed ist, ist zusammen mit Traefik ein docker-socket aufgesetzt, der von hier stammt: https://github.com/wollomatic/socket-proxy
[1] docker-compose.yml show
services:
traefik:
container_name: traefik
image: „traefik:latest“
restart: always
# read_only: true
command:
- –configfile=/traefik.yml
mem_limit: 2G
cpus: 0.75
ports:
- „80:80“
- „443:443“
volumes:
- „./acme.json:/acme.json“
- „./traefik.yml:/traefik.yml:ro“
- „./configfiles:/configfiles:ro“
# - „.logs/traefik.log:/traefik.log“
# - „.logs/access.log:/access.log“
- „./logs:/logs:rw“
depends_on:
- „dockerproxy“
security_opt:
- „no-new-privileges:true“
networks:
- „proxy“
- „docker-proxynet“
labels:
- „traefik.http.routers.traefik.entrypoints=https“
- „traefik.http.routers.traefik.rule=Host(`traefik.smns-bw.org`)“
- „traefik.http.routers.traefik.tls=true“
- „traefik.http.routers.traefik.tls.certresolver=leresolver“
# API service
- „traefik.http.routers.traefik.service=api@internal“
- „traefik.http.routers.traefik.middlewares=secure-traefik@file“
- „traefik.http.services.traefik.loadbalancer.sticky=true“
- „traefik.http.services.traefik.loadbalancer.sticky.cookie.httpOnly=true“
- „traefik.http.services.traefik.loadbalancer.sticky.cookie.secure=true“
healthcheck:
test: [„CMD“, „wget“, „–spider“, „http://localhost:8082/ping“]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
dockerproxy:
build:
context: .
container_name: socket-proxy
command:
- '-loglevel=DEBUG'
- '-allowfrom=traefik,172.31.0.1'
- '-listenip=0.0.0.0'
- '-allowGET=/v1\..{1,2}/(version|containers/.*|events.*)'
- '-shutdowngracetime=5'
restart: unless-stopped
user: „65534:998“
read_only: true
mem_limit: 64M
cap_drop:
- ALL
security_opt:
- no-new-privileges
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- „proxy“
- „docker-proxynet“
healthcheck:
test: [ „CMD“, „nc“, „-z“, „localhost“, „2375“ ]
interval: 1m
timeout: 3s
retries: 3
error-pages-webportal:
container_name: error-webportal
image: nginx:alpine
restart: always
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./mime.types:/etc/nginx/mime.types
- ./error-pages-webportal:/usr/share/nginx/html:ro
labels:
- „traefik.http.routers.error-pages-webportal.entrypoints=https“
- „traefik.http.routers.error-pages-webportal.tls=true“
- „traefik.http.routers.error-pages-webportal.tls.certresolver=leresolver“
- „traefik.http.routers.error-pages-webportal.rule=Host(`error-webportal.smns-bw.org`)“
# - „traefik.http.routers.error-pages-webportal.middlewares=secure-global@file“
- „traefik.http.services.error-pages-webportal.loadbalancer.sticky=true“
- „traefik.http.services.error-pages-webportal.loadbalancer.sticky.cookie.httpOnly=true“
- „traefik.http.services.error-pages-webportal.loadbalancer.sticky.cookie.secure=true“
- „traefik.http.services.error-pages-webportal.loadbalancer.server.port=80“
networks:
- „proxy“
healthcheck:
test: [„CMD“, „curl“, „-f“, „http://localhost/healthz.html“]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
networks:
proxy:
external: true
docker-proxynet:
driver: bridge
internal: true
entryPoints:
http:
address: „:80“
http:
redirections:
entryPoint:
to: ':443'
scheme: https
https:
address: „:443“
# http3:
# advertisedPort: 443
postgres:
address: „:5434“
ssh:
address: „:666“
healthcheck:
address: „:8082“
log:
level: INFO
filePath: „/logs/traefik.log“
format: json
accessLog:
filePath: „/logs/access.log“
bufferingSize: 100
api: {}
ping:
entryPoint: healthcheck
providers:
docker:
# endpoint: „tcp://socket-proxy:2375“
endpoint: „tcp://dockerproxy:2375“
# endpoint: „unix:///var/run/docker.sock“
watch: true
network: proxy
# exposedByDefault: false
file:
directory: „/configfiles“
watch: true
certificatesResolvers:
leresolver:
acme:
email: „it@smns-bw.org“
storage: „/acme.json“
caServer: „https://acme-v02.api.letsencrypt.org/directory“
tlsChallenge: {}
experimental:
plugins:
traefik-plugin-cookie-path-prefix:
moduleName: „github.com/SchmitzDan/traefik-plugin-cookie-path-prefix“
version: „v0.0.3“
http:
routers:
webmin:
entryPoints:
- „https“
rule: Host(`webmin.smns-bw.org`)
service: webmin
middlewares:
- „secure-webmin“
tls:
certResolver: leresolver
options: tls-opts
traefik:
rule: Host(`traefik.smns-bw.org`)
service: cookies
smns_stats:
rule: Host(`statistics.smns-bw.org`)
# rule: „PathPrefix(`/statistics`)“
service: smns_stats
entryPoints:
- „https“
middlewares:
- „secure-collections“
tls:
certResolver: leresolver
options: tls-opts
services:
webmin:
loadBalancer:
servers:
- url: „http://172.18.0.1:10000/“
passHostHeader: true
sticky:
cookie:
secure: true
httpOnly: true
sameSite: lax
smns_stats:
loadBalancer:
servers:
- url: „http://172.18.0.1:8050“
passHostHeader: true
cookies:
loadBalancer:
sticky:
cookie:
secure: true
httpOnly: true
sameSite: lax
http:
middlewares:
secure-global:
chain:
middlewares:
- „security-headers-base“
- „cors-default“
- „CSP-base“
secure-traefik:
chain:
middlewares:
- „traefik-auth“ # auth first, so errors still get headers/CSP
- „security-headers-base“
- „cors-default“
- „CSP-base“
secure-idservice:
chain:
middlewares:
- „security-headers-base“
- „cors-default“
- „CSP-idservice“
secure-biocase:
chain:
middlewares:
- „security-headers-base“
- „cors-default“
- „CSP-biocase“
secure-webmin:
chain:
middlewares:
- „security-headers-base“
- „cors-default“
- „CSP-webmin“
secure-prestashop:
chain:
middlewares:
- „security-headers-base“
- „cors-default“
- „CSP-prestashop“
secure-ent:
chain:
middlewares:
- „security-headers-base“
- „cors-default“
- „CSP-ent“
secure-collections:
chain:
middlewares:
- „webportal-errors“ # moved before headers so error pages get hardened
- „security-headers-hsts-subdomains“
- „security-headers-allowframes“
- „cors-collections“
- „CSP-collections“
secure-digiphyll:
chain:
middlewares:
- „security-headers-base“
- „security-headers-allowframes“
- „cors-default“
- „CSP-digiphyll“
secure-dokuwiki:
chain:
middlewares:
- „security-headers-hsts-subdomains“
- „security-headers-allowframes“
- „cors-default“
- „CSP-dokuwiki“
secure-geometroidea:
chain:
middlewares:
- „security-headers-base“
- „cors-default“
- „CSP-geometroidea“
secure-h5p:
chain:
middlewares:
- „security-headers-base“
- „security-headers-allowframes“
- „cors-default“
- „CSP-h5p“
secure-awstats:
chain:
middlewares:
- „security-headers-base“
- „cors-default“
- „CSP-awstats“
secure-librechat:
chain:
middlewares:
- „security-headers-noindex“
- „cors-default“
- „CSP-librechat“
secure-pictures:
chain:
middlewares:
- „security-headers-base“
- „security-headers-allowframes“
- „cors-default“
- „CSP-base“
http:
middlewares:
default-whitelist:
ipWhiteList:
sourceRange:
- „127.0.0.1/32“
- „172.19.0.0/24“
traefik-auth:
basicAuth:
users:
- „smns-tr:$2y$10$mUF04Rf7852wnP5xGxewte1hW4X/LcEN2c7of5xPOEyzbAXGVrEAi“
# Core security baseline used by most services
security-headers-base:
headers: &SEC_BASE
addVaryHeader: true
# Response header hardening
contentTypeNosniff: true
browserXssFilter: true
referrerPolicy: „same-origin“
permissionsPolicy: fullscreen=(self „https://smns-bw.org“), geolocation=*, midi=(), camera=(), usb=(), magnetometer=(), accelerometer=(), vr=(), speaker=(), ambient-light-sensor=(), gyroscope=(), microphone=(), payment=()
# HSTS
forceSTSHeader: true
stsSeconds: 31536000
stsPreload: false
# Keep includeSubdomains off in base to avoid accidental lock-in; use variant below when desired.
# stsIncludeSubdomains: false
# X-Frame-Options — deny by default (use CSP frame-ancestors for granular control)
frameDeny: true
# customFrameOptionsValue: „SAMEORIGIN“ # optional alternative if you want XFO too
# TLS/Proxy awareness
sslRedirect: true # FIX: belongs at headers root (not inside sslProxyHeaders)
sslProxyHeaders:
X-Forwarded-Proto: „https“
hostsProxyHeaders:
- „X-Forwarded-Host“
# Standard response scrubbing
customResponseHeaders: &RESP_BASE
Server: „“ # FIX: unify case
X-Powered-By: „“ # FIX: unify case
X-Robots-Tag: „index, follow“ # override via -noindex variant
# Upstream request adjustments (to your apps)
customRequestHeaders: &REQ_BASE
X-Forwarded-Proto: „https“ # OK to keep here
# Variant: noindex
security-headers-noindex:
headers:
«: *SEC_BASE
customResponseHeaders:
«: *RESP_BASE
X-Robots-Tag: „none,noarchive,nosnippet,notranslate,noimageindex“
# Variant: allow framing (prefer CSP frame-ancestors to control who can frame you)
security-headers-allowframes:
headers:
«: *SEC_BASE
frameDeny: false
# customFrameOptionsValue is mostly ignored by modern browsers except SAMEORIGIN/DENY.
# „ALLOW-FROM“ is deprecated; rely on CSP if you need granular control.
customFrameOptionsValue: „SAMEORIGIN“
# Variant: HSTS include subdomains, longer max-age
security-headers-hsts-subdomains:
headers:
«: *SEC_BASE
stsIncludeSubdomains: true
stsSeconds: 63072000
# Optional: shared Cache-Control for static-ish services
cache-public:
headers:
customResponseHeaders:
Cache-Control: „public“ # FIX: was (incorrectly) in customRequestHeaders
# CORS: compose these with the security headers as needed
cors-default:
headers: &CORS_DEFAULT
accessControlAllowMethods:
- GET
- OPTIONS
- PUT
accessControlAllowHeaders: „*“
accessControlMaxAge: 100
addVaryHeader: true
cors-collections:
headers:
«: *CORS_DEFAULT
accessControlAllowOriginList:
- https://pydeepzoom.smns-bw.org
- https://pictures.smns-bw.org
# Base CSP used by services that only need same-origin + *.smns-bw.org
CSP-base:
headers:
contentSecurityPolicy: &CSP_BASE >
default-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
frame-ancestors 'self' *.smns-bw.org;
frame-src *.smns-bw.org;
base-uri 'self';
form-action 'self';
img-src 'self' data:;
connect-src 'self';
font-src 'self';
style-src 'self' 'unsafe-inline';
script-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
# If you still reference CSP-global anywhere, just point it to the base:
CSP-global:
headers:
contentSecurityPolicy: *CSP_BASE
# Deltas below (only where different from base)
CSP-dokuwiki:
headers:
contentSecurityPolicy: >
default-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
frame-ancestors 'self' *.smns-bw.org;
frame-src *.smns-bw.org *.google.com;
base-uri 'self';
form-action 'self';
img-src 'self' https://www.dokuwiki.org https://www.gravatar.com data:;
connect-src 'self';
font-src 'self';
style-src 'self' 'unsafe-inline';
script-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
CSP-idservice:
headers:
contentSecurityPolicy: >
default-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
frame-ancestors 'self' *.smns-bw.org;
frame-src *.smns-bw.org;
base-uri 'self';
form-action 'self';
img-src 'self' https://physalia.evolution.uni-bonn.de data:;
connect-src 'self';
font-src 'self';
style-src 'self' 'unsafe-inline';
script-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org https://physalia.evolution.uni-bonn.de;
CSP-librechat:
headers:
contentSecurityPolicy: >
default-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
frame-ancestors 'self' *.smns-bw.org;
frame-src *.smns-bw.org;
base-uri 'self';
form-action 'self';
img-src 'self' blob: data:;
connect-src 'self';
font-src 'self';
style-src 'self' 'unsafe-inline';
script-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
# Fixed: removed invalid 'unsafe-inline' and 'unsafe-eval' from -elem directives
CSP-awstats:
headers:
contentSecurityPolicy: >
default-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
frame-ancestors 'self' *.smns-bw.org;
frame-src *.smns-bw.org;
base-uri 'self';
form-action 'self';
img-src 'self' https://chart.googleapis.com;
connect-src 'self' https://www.gstatic.com/charts/;
font-src 'self';
style-src 'self' 'unsafe-inline';
style-src-elem https://www.gstatic.com/charts/;
script-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
script-src-elem https://www.google.com/jsapi https://www.gstatic.com/charts/;
CSP-h5p:
headers:
contentSecurityPolicy: >
default-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
frame-ancestors https://www.naturkundemuseum-bw.de;
frame-src *.smns-bw.org;
base-uri 'self';
form-action 'self';
img-src 'self' data:;
connect-src 'self';
font-src 'self';
style-src 'self' 'unsafe-inline';
script-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
CSP-biocase:
headers:
contentSecurityPolicy: >
default-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
frame-ancestors 'self' *.smns-bw.org;
frame-src *.smns-bw.org;
base-uri 'self';
form-action 'self';
img-src 'self' https://unpkg.com/ https://*.tile.osm.org data:;
connect-src 'self';
font-src 'self';
style-src 'self' 'unsafe-inline' https://unpkg.com/leaflet@1.3.3/dist/leaflet.css;
script-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org https://unpkg.com/leaflet@1.3.3/dist/leaflet.js https://code.jquery.com/jquery-1.7.min.js;
CSP-prestashop:
headers:
contentSecurityPolicy: >
default-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
frame-ancestors 'self' *.smns-bw.org;
frame-src *.smns-bw.org;
base-uri 'self';
form-action 'self';
img-src 'self' data:;
connect-src 'self';
font-src 'self';
style-src 'self' 'unsafe-inline';
style-src-elem https://market.smns-bw.org;
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://market.smns-bw.org;
# Base + broader font-src
CSP-webmin:
headers:
contentSecurityPolicy: >
default-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
base-uri 'self';
img-src 'self' data:;
font-src 'self' *.smns-bw.org data:;
style-src 'self' 'unsafe-inline';
script-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
# Fixed: stray „data“ token removed, keep single „data:“ at end
CSP-collections:
headers:
contentSecurityPolicy: >
default-src 'none';
frame-ancestors 'self' *.smns-bw.org pictures.smns-bw.org;
frame-src *.smns-bw.org;
base-uri 'self';
form-action 'self';
style-src 'self' 'unsafe-inline' *.smns-bw.org https://cloud.ccm19.de;
connect-src 'self' https://cloud.ccm19.de https://matomo.naturkundemuseum-bw.de/;
font-src 'self' *.smns-bw.org;
img-src 'self' *.smns-bw.org https://a.tile.openstreetmap.org https://b.tile.openstreetmap.org https://c.tile.openstreetmap.org https://cloud.ccm19.de data:;
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://pydeepzoom.smns-bw.org https://matomo.naturkundemuseum-bw.de https://cloud.ccm19.de;
# Fixed: removed invalid 'unsafe-inline' from connect-src and from script/style -elem
CSP-ent:
headers:
contentSecurityPolicy: >
default-src 'none';
base-uri 'self';
form-action 'self';
style-src-elem https://ent.smns-bw.org/static/css/;
connect-src 'self' https://matomo.naturkundemuseum-bw.de;
script-src-elem https://matomo.naturkundemuseum-bw.de https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js https://openseadragon.github.io/openseadragon/openseadragon.js;
img-src 'self' https://openseadragon.github.io/openseadragon/images/;
CSP-digiphyll:
headers:
contentSecurityPolicy: >
default-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
frame-ancestors 'self' *.smns-bw.org;
frame-src *.smns-bw.org https://144.41.33.40/;
base-uri 'self';
form-action 'self';
img-src 'self' https://a.tile.openstreetmap.org https://b.tile.openstreetmap.org https://c.tile.openstreetmap.org https://unpkg.com/ data:;
connect-src 'self';
font-src 'self' data:;
style-src 'self' 'unsafe-inline' data:;
script-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org https://mathjax.rstudio.com/latest/ http://144.41.33.40/ data:;
# Fixed: removed invalid 'unsafe-eval' from style-src
CSP-geometroidea:
headers:
contentSecurityPolicy: >
default-src 'self';
style-src 'self' 'unsafe-inline' *.smns-bw.org https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css https://fonts.googleapis.com/css2;
font-src 'self' https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/webfonts/ https://fonts.gstatic.com/s/nunito/v25/;
img-src 'self' data:;
my-traefik-plugin-cookie-path-prefix:
plugin:
traefik-plugin-cookie-path-prefix:
prefix: smns
webportal-errors:
errors:
status:
service: error-pages-webportal@docker
- „404-503“
query: „/{status}.html“
# another-service-errors:
# errors:
# status:
# - „404-503“
# service: error-pages-another-service
# query: „/error-pages-another-service/{status}.html“
tls:
options:
tls-opts:
minVersion: VersionTLS12
cipherSuites:
- „TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256“
- „TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256“
- „TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384“
- „TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384“
- „TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305“
- „TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305“
curvePreferences:
- CurveP521
- CurveP384
# sniStrict: true
Last updated: 25.09.2025
Sets the shared hardening and sane defaults. Highlights:
Rationale:
Applies to most services unchanged.
CSP-base: headers: contentSecurityPolicy: &CSP_BASE > default-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org; frame-ancestors 'self' *.smns-bw.org; frame-src *.smns-bw.org; base-uri 'self'; form-action 'self'; img-src 'self' data:; connect-src 'self'; font-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
Notes:
We used YAML anchors to stay DRY:
This keeps the source config compact while allowing targeted overrides.
This page describes how to set up a robust Docker healthcheck for Traefik v3.x.
It covers recent Traefik changes, the “gotchas” with TLS, and provides full configuration (compose and YAML) for reliable service monitoring.
Add this to your traefik.yml:
entryPoints: healthcheck: address: ":8082" ping: entryPoint: healthcheck
healthcheck: test: [ "CMD", "wget", "--spider", "http://localhost:8082/ping" ] interval: 30s timeout: 5s retries: 3 start_period: 10s
After editing the healthcheck, you must remove and recreate the container to apply the updated check.
docker compose down docker compose up -d
or for just the traefik service:
docker compose rm traefik docker compose up -d traefik
docker inspect traefik | grep Health -A 10
„Status“: „healthy“http://localhost:8082/pingwget --spider http://localhost:8082/ping
'404 Not Found' or status stays unhealthy, check:docker inspect)Authored for SMNS IT by Chattie and AI Programmer