Docker

Docker + Docker Compose installieren

https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository

# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update

dann

 sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

dann

 sudo docker run hello-world

oder weiter zur Installation von Portainer

Portainer installieren (+ Docker)

Docker

curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh

 

Portainer

stand: 14.11.24

docker volume create portainer_data
docker run -d -p 8000:8000 -p 9443:9443 --name portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce:latest

Docker-Compose .yamls

Docker-Compose .yamls

Bitwarden (Vaultwarden)

PL 

updated 2026-01-27

SSO via OIDC mit Authentik: 
https://integrations.goauthentik.io/security/vaultwarden 
https://github.com/dani-garcia/vaultwarden/wiki/Enabling-SSO-support-using-OpenId-Connect 

version: '3'

services:
  bitwarden:
    container_name: bitwarden
    image: vaultwarden/server:1.35.2
    restart: always
    volumes:
      - bitwarden:/data/
    environment:
      ADMIN_TOKEN: 'oLN3gTp...82CYqV2'
      LOG_LEVEL: debug
      SSO_ENABLED: true
      SSO_AUTHORITY: https://authentik.MEINEDOMAIN.de/application/o/<PROVIDER-SLUG aus Authentik, zb bitwarden oder vaultwarden>/ 
      # ^^ Trailing slash am Ende ist notwendig!
      SSO_SCOPES: "openid email profile offline_access"
      SSO_CLIENT_ID: UKm...eux1ZLoq
      SSO_CLIENT_SECRET: xva4gwxwKzxaoNw2.........EIicxhNjE6UXu5cOyl
    ports:
      - 9780:80

volumes:
  bitwarden:

SSO OIDC variablen:

SSO_ENABLED=true
SSO_AUTHORITY=https://authentik.company/application/o/<application_slug>/
SSO_CLIENT_ID=<client_id>
SSO_CLIENT_SECRET=<client_secret>
SSO_SCOPES="openid email profile offline_access"
SSO_ALLOW_UNKNOWN_EMAIL_VERIFICATION=false
SSO_CLIENT_CACHE_EXPIRATION=0
SSO_ONLY=false # Set to true to disable email+master password login and require SSO
SSO_SIGNUPS_MATCH_EMAIL=true # Match first SSO login to existing account by email

Auto confirm invited users

https://github.com/dani-garcia/vaultwarden/discussions/3954 

bitwarden cli client installieren: https://bitwarden.com/help/cli/#download-and-install 

cronjob:

* * * * * bash bw-confirm-users.sh; bw logout

bw-confirm-users.sh

set -a
source .env.cli
set +a

bw login --raw --apikey

sessionKey="$(bw unlock --raw --passwordenv BW_PASSWORD)"

orgIds=$(bw --session $sessionKey list organizations | jq -r '.[].id')

for orgId in $orgIds; do
    members=$(bw --session $sessionKey list org-members --organizationid $orgId | jq -r '.[] | select(.status == 1) | .id')
    for memberId in $members; do
        echo "Confirming member $memberId..."
        bw confirm --session $sessionKey --organizationid $orgId org-member $memberId
    done
done

.env.cli

BW_CLIENTID=client_id
BW_CLIENTSECRET=client_secret
BW_PASSWORD=master_password


 

 

Alt

EA
services:
  bitwarden:
    image: vaultwarden/server:latest
    container_name: bitwarden
    restart: unless-stopped
    environment:
      - ADMIN_TOKEN=Q9kjrfy5---IrgendeinToken---b01fPm0ysaKRLRiwL
    ports:
      - 8081:80
    volumes:
      - /srv/dev-disk-by-label-LaufwerkSRaid/LaufwerkS/docker/bitwarden:/data
      

Eikes Fernnetz (Hetzner)
version: '3'

services:
  vaultwarden:
    container_name: vaultwarden
    image: vaultwarden/server:latest
    restart: unless-stopped
    volumes:
      - vaultwarden_data:/data/
    ports:
      - 23001:80

volumes:
  vaultwarden_data:
WIS
version: '3'

services:
  bitwarden:
    container_name: bitwarden
    image: vaultwarden/server:latest
    restart: always
    volumes:
      - ./bitwarden/data:/data/
    environment:
      ADMIN_TOKEN: 'Nc1.........eoY'
    ports:
      - 9780:80

ADMIN_TOKEN mit folgendem Befehl erstellen

openssl rand -base64 48

NEU ADMIN_TOKEN mit `vaultwarden hash` erstellen. Das Passwort bei der Tokenerstellung ist gleichzeitig das Passwort für die Adminpage https://bitwarden.meineseite.de/admin 

in container:
vaultwarden hash

oder
docker exec -it containername /vaultwarden hash

image.png

danach Konto erstellen über die Webseite (erstmal nicht öffentlich zugänglich machen, nur die eigene IP whitelisten).

Dann Konto erstellen über Adminpage deaktivieren (allow new signups = false):

drawing-4-1719867133.png
Docker-Compose .yamls

Bookstack

2026-03-30

mit Authentik verknüpft

https://hub.docker.com/r/linuxserver/bookstack

services:
  bookstack:
    image: lscr.io/linuxserver/bookstack:26.03.2
    container_name: bookstack
    environment:
      - PUID=1000
      - PGID=1000
      - APP_URL=https://bookstack.MEINEDOMAIN.DE
      - DB_HOST=bookstack_db
      - DB_PORT=3306
      - DB_USER=bookstack
      - DB_PASS=MEIN-DB-PASS
      - DB_DATABASE=bookstackapp
      # OIDC ab hier
      - AUTH_METHOD=standard # hier nachher nochmal einloggen, um external-auth-id in admin ui zu vergeben 
      #- AUTH_METHOD=oidc
      - AUTH_AUTO_INITIATE=false # Set this to "true" to automatically redirect the user to authentik.
      - OIDC_NAME=Authentik # The display name shown on the login page.
      - OIDC_DISPLAY_NAME_CLAIMS=name # Claim(s) for the user's display name. Can have multiple attributes listed, separated with a '|' in which case those values will be joined with a space.
      - OIDC_CLIENT_ID=Drv...bOx
      - OIDC_CLIENT_SECRET=ZPmeXvnaZapcw.......WrLl
      - OIDC_ISSUER=https://auth.MEINEDOMAIN.DE/application/o/bookstack/
      - OIDC_ISSUER_DISCOVER=true
      - OIDC_END_SESSION_ENDPOINT=true
    volumes:
      - app:/config
    ports:
      - 6875:80
    restart: unless-stopped
    depends_on:
      - bookstack_db
  bookstack_db:
    image: lscr.io/linuxserver/mariadb:10.11.8
    container_name: bookstack_db
    environment:
      - PUID=1000
      - PGID=1000
      - MYSQL_ROOT_PASSWORD=FmNhDfNxF8Sdf6
      - TZ=Europe/Berlin
      - MYSQL_DATABASE=bookstackapp
      - MYSQL_USER=bookstack
      - MYSQL_PASSWORD=FmNhDfNxF8Sdf6
    volumes:
      - db:/config
    restart: unless-stopped

volumes:
  db:
  app:

Docker-Compose .yamls

Heimdall Dashboard

WPD
---
version: "2.1"
services:
  heimdall:
    image: lscr.io/linuxserver/heimdall:latest
    container_name: heimdall
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Berlin
    volumes:
      - /path/to/appdata/config:/config
    ports:
      - 1080:80
      - 10443:443
    restart: unless-stopped
BS
---
version: "2.1"
services:
  heimdall:
    image: lscr.io/linuxserver/heimdall:latest
    logging:
      options:
        max-size: "100m"
    container_name: heimdall
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Berlin
    volumes:
      - ./heimdall/config:/config
    ports:
      - 8095:80
      - 10443:443
    restart: unless-stopped
Docker-Compose .yamls

Homer Dashboard

HW
version: "2"
services:
  homer:
    image: b4bz/homer
    logging:
      options:
        max-size: "100m"
    container_name: homer
    volumes:
      - /var/lib/docker/volumes/homer/:/www/assets
    ports:
      - 8090:8080
    #environment:
    #  - UID=1000
    #  - GID=1000
    restart: unless-stopped
Docker-Compose .yamls

Kimai Zeiterfassung

<MEINPW123> ersetzen

https://github.com/tobybatch/kimai2/blob/main/docker-compose.yml

HW (x86)
version: "2"

services:
  app:
    container_name: kimai_app
    image: kimai/kimai2:apache-2.45.0
    logging:
      options:
        max-size: "100m"
    restart: unless-stopped
    ports:
      - "8001:8001"
    depends_on:
        - db
    volumes:
      - var:/opt/kimai/var
    environment:
      - TRUSTED_PROXIES=localhost,127.0.0.1,kimai.MEINEDOMAIN.de,192.168.178.190,192.168.1.71,kimai.MEINEDOMAIN.it,10.1.1.71
      - DATABASE_URL=mysql://kimai:ha.......AS@db:3309/kimai

  db:
    image: mysql:8-oraclelinux8
    container_name: kimai_db
    restart: unless-stopped
    volumes:
      - mysql-data:/var/lib/mysql
    environment:
      - MYSQL_DATABASE=kimai
      - MYSQL_USER=kimai
      - MYSQL_PASSWORD=ha........AS
      - MYSQL_RANDOM_ROOT_PASSWORD=1
      - MYSQL_TCP_PORT= 3309
    ports:
      - 3309:3306
      
volumes:
  var:
  mysql-data:

Docker-Compose .yamls

Nginx Proxy Manager (NPM)

2026-02-08

version: '3.8'
services:
  app:
    image: 'jc21/nginx-proxy-manager:2.13.7'
    restart: always
    ports:
      - '80:80'
      - '81:81'
      - '443:443'
    volumes:
      - data:/data
      - le:/etc/letsencrypt

volumes:
  data:
  le:
  

Dateien über NPM zur Verfügung stellen

Dateien ins volume data hochladen:

image.png

Pfad zb in 

/var/lib/docker/volumes/npm_data/_data/nginx/default_www/images

image.png

dann unter npm > proxy host > edit > advanced > custom nginx configuration:

image.png

>>>

image.png

mit folgendem Inhalt:

location /images/ {
    alias /data/nginx/default_www/images/;
    add_header Cache-Control "public";
}

location / {
    default_type text/html;
    return 200 '<!DOCTYPE html>
<html lang="de">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>In Arbeit</title>
    <style>
        body {
            margin: 0;
            height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            font-family: Arial, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
        }
        h1 {
            font-size: 2.5rem;
            text-align: center;
        }
    </style>
</head>
<body>
    <h1>Platzhalter, diese Seite befindet sich im Aufbau</h1>
</body>
</html>';
}

Danach sind die Dateien unter der url aufrufbar, zb

https://app.MEINEDOMAIN.de/images/dreamy-aesthetic-color-year-tones-nature-landscape4.jpg 

image.png

 

Alte Einträge:

EA
services:
  nginxreverseproxymanager:
    image: jlesage/nginx-proxy-manager
    container_name: NginxReverseProxyManager
    restart: unless-stopped
    ports:
      - 8082:8080
      - 8181:8181
      - 4443:4443
    volumes:
      - /srv/dev-disk-by-label-LaufwerkSRaid/LaufwerkS/docker/nginxreverseproxymanager:/config:rw
      
WPD
version: "2"
services:
  app:
    image: 'jc21/nginx-proxy-manager:latest'
    restart: unless-stopped
    ports:
      # These ports are in format <host-port>:<container-port>
      - 8080:80 # Public HTTP Port
      - 8443:443 # Public HTTPS Port
      - 8181:81 # Admin Web Port
      # Add any other Stream port you want to expose
      # - '21:21' # FTP

    # Uncomment the next line if you uncomment anything in the section
    # environment:
      # Uncomment this if you want to change the location of 
      # the SQLite DB file within the container
      # DB_SQLITE_FILE: "/data/database.sqlite"

      # Uncomment this if IPv6 is not enabled on your host
      # DISABLE_IPV6: 'true'

    volumes:
      - ./nginx/data:/data
      - ./nginx/letsencrypt:/etc/letsencrypt

STANDARDLOGIN

admin@example.com
changeme

WIS / FN
version: '3.8'
services:
  app:
    image: 'jc21/nginx-proxy-manager:2.9.22'
    restart: always
    ports:
      - '80:80'
      - '81:81'
      - '443:443'
    volumes:
      - ./data2:/data
      - ./letsencrypt2:/etc/letsencrypt
Docker-Compose .yamls

VSCode

---
version: "2.1"
services:
  code-server:
    image: lscr.io/linuxserver/code-server
    container_name: code-server
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Berlin
      - SUDO_PASSWORD=MEINPW123
    volumes:
      - /srv/dev-disk-by-label-LaufwerkSRaid/LaufwerkS/docker/vscode/config:/config
    ports:
      - 8443:8443
    restart: unless-stopped
    
Docker-Compose .yamls

Wireguard VPN-Server

wg-easy

WPD
version: "3.8"
services:
  wg-easy:
    environment:
      # ⚠️ Required:
      # Change this to your host's public address
      - WG_HOST=<sub.deinedomain.de>

      # Optional:
      # - PASSWORD=foobar123
      # - WG_PORT=51820
      # - WG_DEFAULT_ADDRESS=10.8.0.x
      # - WG_DEFAULT_DNS=1.1.1.1
      # - WG_MTU=1420
      # - WG_ALLOWED_IPS=192.168.15.0/24, 10.0.1.0/24
      # - WG_PRE_UP=echo "Pre Up" > /etc/wireguard/pre-up.txt
      # - WG_POST_UP=echo "Post Up" > /etc/wireguard/post-up.txt
      # - WG_PRE_DOWN=echo "Pre Down" > /etc/wireguard/pre-down.txt
      # - WG_POST_DOWN=echo "Post Down" > /etc/wireguard/post-down.txt
      
    image: weejewel/wg-easy
    container_name: wg-easy
    volumes:
      - .:/etc/wireguard
    ports:
      - "51822:51820/udp"
      - "51821:51821/tcp"
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    sysctls:
      - net.ipv4.ip_forward=1
      - net.ipv4.conf.all.src_valid_mark=1
Docker-Compose .yamls

Zigbee2MQTT

HW

version: '3.8'
services:
  zigbee2mqtt:
    container_name: zigbee2mqtt
    image: koenkk/zigbee2mqtt
    logging:
      options:
        max-size: "100m"
    restart: unless-stopped
    volumes:
      - /var/lib/docker/volumes/zigbee2mqtt/data:/app/data
      - /run/udev:/run/udev:ro
    ports:
      # Frontend port
      - 8910:8080
    environment:
      - TZ=Europe/Berlin
    devices:
      # Make sure this matched your adapter location
      - /dev/serial/by-id/usb-dresden_elektronik_ingenieurtechnik_GmbH_ConBee_II_DE2123629-if00:/dev/ttyACM0

NKS

version: '3.8'

volumes:
  zigbee2mqtt_data:
  
services:
  zigbee2mqtt:
    container_name: zigbee2mqtt
    image: koenkk/zigbee2mqtt
    logging:
      options:
        max-size: "100m"
    restart: unless-stopped
    volumes:
      - zigbee2mqtt_data:/app/data
      - /run/udev:/run/udev:ro
    ports:
      # Frontend port
      - 8910:8080
    environment:
      - TZ=Europe/Berlin
    #devices:
      # Make sure this matched your adapter location
      #- /dev/serial/by-id/usb-dresden_elektronik_ingenieurtechnik_GmbH_ConBee_II_DE2123629-if00:/dev/ttyACM0
Docker-Compose .yamls

openHAB3

NKS
version: '2.2'

services:
  openhab:
    image: "openhab/openhab:3.4.2"
    restart: always
    network_mode: host
    volumes:
      - "/etc/localtime:/etc/localtime:ro"
      - "/etc/timezone:/etc/timezone:ro"
      - "openhab_addons:/openhab/addons"
      - "openhab_conf:/openhab/conf"
      - "openhab_userdata:/openhab/userdata"
    environment:
      CRYPTO_POLICY: "unlimited"
      EXTRA_JAVA_OPTS: "-Duser.timezone=Europe/Berlin"
      OPENHAB_HTTP_PORT: "8080"
      OPENHAB_HTTPS_PORT: "8443"

volumes:
  openhab_addons:
    driver: local
  openhab_conf:
    driver: local
  openhab_userdata:
    driver: local
Docker-Compose .yamls

Unifi Controller (Ubiquiti)

Stichwort Fix for ADOPTING / OFFLINE loop on Docker Unifi Controller
Falls es ein Problem gibt, mein Reddit Artikel sollte helfen:
https://www.reddit.com/r/Ubiquiti/comments/v42avk/fix_for_adopting_offline_loop_on_docker_unifi/
TLDR: Ports so lassen wie sie sind und docker-host-ip in Unifi Controller UI eintragen!

PL (aktuell)

services:
 unifi-controller:
   container_name: unificontroller
   image: jacobalberty/unifi:v9.0.114
   restart: always
   volumes:
     - unifi:/unifi
   ports:
     - 3478:3478/udp # STUN service
     - 8080:8080 # Device command/control
     - 8443:8443 # Web interface + API
     #- 8843:8843 # HTTPS portal (optional)
     #- 8880:8880 # HTTP portal (optional)
     #- 6789:6789 # Speed Test (unifi5 only) (optional)
   environment:
     - TZ=Europe/Berlin
   labels:
     - 'unifi-controller'

volumes:
  unifi:
  

HW (alte volumes)

services:
 unifi-controller:
   container_name: unificontroller
   image: jacobalberty/unifi:v8.5
   restart: always
   volumes:
     - lib:/var/lib/unifi
     - log:/var/log/unifi
     - run:/var/run/unifi
   ports:
     - 3478:3478/udp
     - 10001:10001/udp
     - 6789:6789/tcp
     - 8080:8080/tcp
     - 8880:8880/tcp
     - 8443:8443/tcp
     - 8843:8843/tcp
   environment:
     - TZ=Europe/Berlin
   labels:
     - 'unifi-controller'

volumes:
  lib:
  log:
  run:

Docker-Compose armv7 zb Raspberry Pi 32bit (alt)

version: '2'
services:
 unifi-controller:
   container_name: unificontroller
   image: jacobalberty/unifi:latest
   restart: always
   volumes:
     - ./unificontroller/lib:/var/lib/unifi
     - ./unificontroller/log:/var/log/unifi
     - ./unificontroller/run:/var/run/unifi
   ports:

     - 3478:3478/udp
     - 10001:10001/udp
     - 6789:6789/tcp
     - 8080:8080/tcp
     - 8880:8880/tcp
     - 8443:8443/tcp
     - 8843:8843/tcp
   environment:
     - TZ=Europe/Berlin
   labels:
     - 'unifi-controller'

falls es Probleme gibt, Firewall installieren und alle relevanten Ports zulassen:

sudo apt install ufw
sudo ufw allow 3478/udp
...
sudo ufw allow 8843/tcp
sudo reboot

Docker-Compose .yamls

AdGuard Home

AdBlocker Werbeblocker

version: "2"
services:
  adguardhome:
    image: adguard/adguardhome
    # network_mode: "host"
    container_name: adguardhome
    ports:
      - 53:53/tcp
      - 53:53/udp
      - 784:784/udp
      - 853:853/tcp
      - 3001:3000/tcp
      - 8680:80/tcp
      - 8643:443/tcp
    volumes:
      - ./adguard/work:/opt/adguardhome/work
      - ./adguard/conf:/opt/adguardhome/conf
    restart: unless-stopped
    networks:
      - adguard-nw

networks:
  adguard-nw:
    internal: false
    driver: bridge
    driver_opts:
      com.docker.network.bridge.name: br-adguard


Ich hab jedoch die Proxmox LXC Variante verwendet, um AdGuard eine eigene statische IP zu geben.

siehe

https://wiki.folkerts.it/books/proxmox/page/adguard-lxc-container 

Docker-Compose .yamls

paperless-ngx

2025-11-25 paperless-ngx 2.20.0

https://github.com/paperless-ngx/paperless-ngx/blob/main/docker/compose 

services:
  broker:
    image: docker.io/library/redis:8
    restart: unless-stopped
    volumes:
      - redisdata:/data
  db:
    image: docker.io/library/postgres:18
    restart: unless-stopped
    volumes:
      - pgdata:/var/lib/postgresql
    environment:
      POSTGRES_DB: paperless
      POSTGRES_USER: paperless
      POSTGRES_PASSWORD: paperless
  webserver:
    image: ghcr.io/paperless-ngx/paperless-ngx:2.20.0
    restart: unless-stopped
    depends_on:
      - db
      - broker
    ports:
      - "8010:8000"
    volumes:
      - data:/usr/src/paperless/data
      - media:/usr/src/paperless/media
      - export:/usr/src/paperless/export
      - consume:/usr/src/paperless/consume
    env_file: stack.env
    environment:
      PAPERLESS_REDIS: redis://broker:6379
      PAPERLESS_DBHOST: db
volumes:
  data:
  media:
  export:
  consume:
  pgdata:
  redisdata:

.env

###############################################################################
# Paperless-ngx settings                                                      #
###############################################################################

# See http://docs.paperless-ngx.com/configuration/ for all available options.

# The UID and GID of the user used to run paperless in the container. Set this
# to your UID and GID on the host so that you have write access to the
# consumption directory.
USERMAP_UID=1000
USERMAP_GID=1000

# See the documentation linked above for all options. A few commonly adjusted settings
# are provided below.

# This is required if you will be exposing Paperless-ngx on a public domain
# (if doing so please consider security measures such as reverse proxy)
PAPERLESS_URL=https://paperless.MEINEDOMAIN.DE

# Adjust this key if you plan to make paperless available publicly. It should
# be a very long sequence of random characters. You don't need to remember it.
PAPERLESS_SECRET_KEY=changeme<zb mit linuxcmd openssl rand -hex 24>

# Use this variable to set a timezone for the Paperless Docker containers. Defaults to UTC.
#PAPERLESS_TIME_ZONE=America/Los_Angeles
PAPERLESS_TIME_ZONE=Europe/Berlin

# The default language to use for OCR. Set this to the language most of your
# documents are written in.
#PAPERLESS_OCR_LANGUAGE=eng
PAPERLESS_OCR_LANGUAGE=deu

# Additional languages to install for text recognition, separated by a whitespace.
# Note that this is different from PAPERLESS_OCR_LANGUAGE (default=eng), which defines
# the language used for OCR.
# The container installs English, German, Italian, Spanish and French by default.
# See https://packages.debian.org/search?keywords=tesseract-ocr-&searchon=names
# for available languages.
#PAPERLESS_OCR_LANGUAGES=tur ces
PAPERLESS_OCR_LANGUAGES=eng

Danach verbinden mit paperless-ai, um ki gestützte Dokumentenprüfung und Tagging mit OpenAI oder Ollama zu erhalten (siehe hier im Wiki 'paperless-ai')

Alt (paperless-ngx v.2.11 oder älter)

# docker-compose file for running paperless from the Docker Hub.
# This file contains everything paperless needs to run.
# Paperless supports amd64, arm and arm64 hardware.
#
# All compose files of paperless configure paperless in the following way:
#
# - Paperless is (re)started on system boot, if it was running before shutdown.
# - Docker volumes for storing data are managed by Docker.
# - Folders for importing and exporting files are created in the same directory
#   as this file and mounted to the correct folders inside the container.
# - Paperless listens on port 8010.
#
# In addition to that, this docker-compose file adds the following optional
# configurations:
#
# - Instead of SQLite (default), PostgreSQL is used as the database server.
#
# To install and update paperless with this file, do the following:
#
# - Open portainer Stacks list and click 'Add stack'
# - Paste the contents of this file and assign a name, e.g. 'Paperless'
# - Click 'Deploy the stack' and wait for it to be deployed
# - Open the list of containers, select paperless_webserver_1
# - Click 'Console' and then 'Connect' to open the command line inside the container
# - Run 'python3 manage.py createsuperuser' to create a user
# - Exit the console
#
# For more extensive installation and update instructions, refer to the
# documentation.

version: "3.4"
services:
  broker:
    image: docker.io/library/redis:7
    restart: unless-stopped
    volumes:
      - redisdata:/data

  db:
    image: docker.io/library/postgres:13
    restart: unless-stopped
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: paperless
      POSTGRES_USER: paperless
      POSTGRES_PASSWORD: paperless

  webserver:
    image: ghcr.io/paperless-ngx/paperless-ngx:latest
    restart: unless-stopped
    depends_on:
      - db
      - broker
    ports:
      - "8010:8000"
    healthcheck:
      test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
      interval: 30s
      timeout: 10s
      retries: 5
    volumes:
      - data:/usr/src/paperless/data
      - media:/usr/src/paperless/media
      - ./export:/usr/src/paperless/export
      - ./consume:/usr/src/paperless/consume
    environment:
      PAPERLESS_REDIS: redis://broker:6379
      PAPERLESS_DBHOST: db
# The UID and GID of the user used to run paperless in the container. Set this
# to your UID and GID on the host so that you have write access to the
# consumption directory.
      USERMAP_UID: 1000
      USERMAP_GID: 100
# Additional languages to install for text recognition, separated by a
# whitespace. Note that this is
# different from PAPERLESS_OCR_LANGUAGE (default=eng), which defines the
# language used for OCR.
# The container installs English, German, Italian, Spanish and French by
# default.
# See https://packages.debian.org/search?keywords=tesseract-ocr-&searchon=names&suite=buster
# for available languages.
      #PAPERLESS_OCR_LANGUAGES: tur ces
# Adjust this key if you plan to make paperless available publicly. It should
# be a very long sequence of random characters. You don't need to remember it.
      #PAPERLESS_SECRET_KEY: change-me
# Use this variable to set a timezone for the Paperless Docker containers. If not specified, defaults to UTC.
      PAPERLESS_TIME_ZONE: Europe/Berlin
# The default language to use for OCR. Set this to the language most of your
# documents are written in.
      PAPERLESS_OCR_LANGUAGE: de

volumes:
  data:
  media:
  pgdata:
  redisdata:

wie in den Kommentaren geschrieben, muss man im Container einen neuen user anlegen, dafuer in Portainer mit der Shell verbinden und

python3 manage.py createsuperuser

eingeben.


Als App  Paperless mobile benutzen:

https://github.com/astubenbord/paperless-mobile

https://play.google.com/store/apps/details?id=de.astubenbord.paperless_mobile

Docker-Compose .yamls

Nextcloud

Empfohlen:
Nextcloud AIO Manual-Install Variante 

siehe https://wiki.folkerts.it/books/docker/page/nextcloud-aio 

Nextcloud AIO Variante

Anleitung für Nextcloud AIO: https://github.com/nextcloud/all-in-one 
Dockerhub: https://hub.docker.com/r/nextcloud/all-in-one 

Folgende docker-compose.yml für Reverse Proxy angepasst. Mehr dazu unter https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md 

services:
  nextcloud-aio-mastercontainer:
    image: ghcr.io/nextcloud-releases/all-in-one:20250424_092733
    init: true
    restart: always
    container_name: nextcloud-aio-mastercontainer # This line is not allowed to be changed as otherwise AIO will not work correctly
    volumes:
      - nextcloud_aio_mastercontainer:/mnt/docker-aio-config # This line is not allowed to be changed as otherwise the built-in backup solution will not work
      - /var/run/docker.sock:/var/run/docker.sock:ro # May be changed on macOS, Windows or docker rootless. See the applicable documentation. If adjusting, don't forget to also set 'WATCHTOWER_DOCKER_SOCKET_PATH'!
    network_mode: bridge # add to the same network as docker run would do
    ports:
      #- 80:80 # Can be removed when running behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else). See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md
      - 8080:8080 # AIO Web-UI
      #- 8443:8443 # Can be removed when running behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else). See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md
    environment: # Is needed when using any of the options below
      # AIO_DISABLE_BACKUP_SECTION: false # Setting this to true allows to hide the backup section in the AIO interface. See https://github.com/nextcloud/all-in-one#how-to-disable-the-backup-section
      # AIO_COMMUNITY_CONTAINERS: # With this variable, you can add community containers very easily. See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers
      APACHE_PORT: 11000 # Is needed when running behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else). See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md
      APACHE_IP_BINDING: 0.0.0.0 # Should be set when running behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else) that is running on the same host. See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md
      APACHE_ADDITIONAL_NETWORK: "" # (Optional) Connect the apache container to an additional docker network. Needed when behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else) running in a different docker network on same server. See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md
      # BORG_RETENTION_POLICY: --keep-within=7d --keep-weekly=4 --keep-monthly=6 # Allows to adjust borgs retention policy. See https://github.com/nextcloud/all-in-one#how-to-adjust-borgs-retention-policy
      # COLLABORA_SECCOMP_DISABLED: false # Setting this to true allows to disable Collabora's Seccomp feature. See https://github.com/nextcloud/all-in-one#how-to-disable-collaboras-seccomp-feature
      # FULLTEXTSEARCH_JAVA_OPTIONS: "-Xms1024M -Xmx1024M" # Allows to adjust the fulltextsearch java options. See https://github.com/nextcloud/all-in-one#how-to-adjust-the-fulltextsearch-java-options
      #NEXTCLOUD_DATADIR: /mnt/hz-s3-pl-01/nextcloud_data # Allows to set the host directory for Nextcloud's datadir. ⚠️⚠️⚠️ Warning: do not set or adjust this value after the initial Nextcloud installation is done! See https://github.com/nextcloud/all-in-one#how-to-change-the-default-location-of-nextclouds-datadir
      # NEXTCLOUD_MOUNT: /mnt/ # Allows the Nextcloud container to access the chosen directory on the host. See https://github.com/nextcloud/all-in-one#how-to-allow-the-nextcloud-container-to-access-directories-on-the-host
      # NEXTCLOUD_UPLOAD_LIMIT: 16G # Can be adjusted if you need more. See https://github.com/nextcloud/all-in-one#how-to-adjust-the-upload-limit-for-nextcloud
      # NEXTCLOUD_MAX_TIME: 3600 # Can be adjusted if you need more. See https://github.com/nextcloud/all-in-one#how-to-adjust-the-max-execution-time-for-nextcloud
      # NEXTCLOUD_MEMORY_LIMIT: 512M # Can be adjusted if you need more. See https://github.com/nextcloud/all-in-one#how-to-adjust-the-php-memory-limit-for-nextcloud
      # NEXTCLOUD_TRUSTED_CACERTS_DIR: /path/to/my/cacerts # CA certificates in this directory will be trusted by the OS of the nextcloud container (Useful e.g. for LDAPS) See https://github.com/nextcloud/all-in-one#how-to-trust-user-defined-certification-authorities-ca
      # NEXTCLOUD_STARTUP_APPS: deck twofactor_totp tasks calendar contacts notes # Allows to modify the Nextcloud apps that are installed on starting AIO the first time. See https://github.com/nextcloud/all-in-one#how-to-change-the-nextcloud-apps-that-are-installed-on-the-first-startup
      # NEXTCLOUD_ADDITIONAL_APKS: imagemagick # This allows to add additional packages to the Nextcloud container permanently. Default is imagemagick but can be overwritten by modifying this value. See https://github.com/nextcloud/all-in-one#how-to-add-os-packages-permanently-to-the-nextcloud-container
      # NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS: imagick # This allows to add additional php extensions to the Nextcloud container permanently. Default is imagick but can be overwritten by modifying this value. See https://github.com/nextcloud/all-in-one#how-to-add-php-extensions-permanently-to-the-nextcloud-container
      # NEXTCLOUD_ENABLE_DRI_DEVICE: true # This allows to enable the /dev/dri device for containers that profit from it. ⚠️⚠️⚠️ Warning: this only works if the '/dev/dri' device is present on the host! If it should not exist on your host, don't set this to true as otherwise the Nextcloud container will fail to start! See https://github.com/nextcloud/all-in-one#how-to-enable-hardware-acceleration-for-nextcloud
      # NEXTCLOUD_ENABLE_NVIDIA_GPU: true # This allows to enable the NVIDIA runtime and GPU access for containers that profit from it. ⚠️⚠️⚠️ Warning: this only works if an NVIDIA gpu is installed on the server. See https://github.com/nextcloud/all-in-one#how-to-enable-hardware-acceleration-for-nextcloud.
      # NEXTCLOUD_KEEP_DISABLED_APPS: false # Setting this to true will keep Nextcloud apps that are disabled in the AIO interface and not uninstall them if they should be installed. See https://github.com/nextcloud/all-in-one#how-to-keep-disabled-apps
      SKIP_DOMAIN_VALIDATION: true # This should only be set to true if things are correctly configured. See https://github.com/nextcloud/all-in-one?tab=readme-ov-file#how-to-skip-the-domain-validation
      # TALK_PORT: 3478 # This allows to adjust the port that the talk container is using which is exposed on the host. See https://github.com/nextcloud/all-in-one#how-to-adjust-the-talk-port
      # WATCHTOWER_DOCKER_SOCKET_PATH: /var/run/docker.sock # Needs to be specified if the docker socket on the host is not located in the default '/var/run/docker.sock'. Otherwise mastercontainer updates will fail. For macos it needs to be '/var/run/docker.sock'
    # security_opt: ["label:disable"] # Is needed when using SELinux

#   # Optional: Caddy reverse proxy. See https://github.com/nextcloud/all-in-one/discussions/575
#   # Alternatively, use Tailscale if you don't have a domain yet. See https://github.com/nextcloud/all-in-one/discussions/5439
#   # Hint: You need to uncomment APACHE_PORT: 11000 above, adjust cloud.example.com to your domain and uncomment the necessary docker volumes at the bottom of this file in order to make it work
#   # You can find further examples here: https://github.com/nextcloud/all-in-one/discussions/588
#   caddy:
#     image: caddy:alpine
#     restart: always
#     container_name: caddy
#     volumes:
#       - caddy_certs:/certs
#       - caddy_config:/config
#       - caddy_data:/data
#       - caddy_sites:/srv
#     network_mode: "host"
#     configs:
#       - source: Caddyfile
#         target: /etc/caddy/Caddyfile
# configs:
#   Caddyfile:
#     content: |
#       # Adjust cloud.example.com to your domain below
#       https://cloud.example.com:443 {
#         reverse_proxy localhost:11000
#       }

volumes: # If you want to store the data on a different drive, see https://github.com/nextcloud/all-in-one#how-to-store-the-filesinstallation-on-a-separate-drive
  nextcloud_aio_mastercontainer:
    name: nextcloud_aio_mastercontainer # This line is not allowed to be changed as otherwise the built-in backup solution will not work
  # caddy_certs:
  # caddy_config:
  # caddy_data:
  # caddy_sites:

verbotene Zeichen am Ende von Dateien und Ordnern

erstmal bestimmte zeichen generell verbieten siehe oben config.php

danach App File Access Control installieren https://apps.nextcloud.com/apps/files_accesscontrol 

Regel für Dateiname > entspricht > /^.*\.$/i aktivieren, um Punkte am Ende einer Datei zu verhindern

image.png

Standardsprache DE für neue Benutzer 

mit

docker exec -it nextcloud-app-1 bash

in die Shell des Containers gehen (oder einfach im Volume), dann

apt-get update
apt-get install nano
nano config/config.php

und folgendes anhängen:

  'loglevel' => 2,
  'maintenance' => false,
  'default_language' => 'de',
  'default_locale' => 'de_DE',
  'default_timezone' => 'Europe/Berlin',
);

image.png

https://docs.nextcloud.com/server/16/admin_manual/configuration_server/language_configuration.html

S3 Minio als primary storage in config.php

(eigentlich über die env-vars im Stack, aber falls das verpasst wurde)

  'objectstore' =>
  array (
    'class' => '\\OC\\Files\\ObjectStore\\S3',
    'arguments' =>
    array (
      'bucket' => 'nextcloud',
      'region' => '',
      'hostname' => 'minio-s3.MEINEDOMAIN.de',
      'port' => '443',
      'StorageClass' => '',
      'objectPrefix' => 'urn:oid:',
      'autocreate' => false,
      'use_ssl' => true,
      'use_path_style' => true,
      'legacy_auth' => false,
      'key' => '3svCe...wVvluT',
      'secret' => 'mHK6Q............0GTa',
    ),
  ),

E-Mail Einstellungen SMTP in config.php (auch über GUI)

  'mail_from_address' => 'admin',
  'mail_smtpmode' => 'smtp',
  'mail_sendmailmode' => 'smtp',
  'mail_domain' => 'MEINEDOMAIN.de',
  'mail_smtphost' => 'smtp.strato.de',
  'mail_smtpauth' => 1,
  'mail_smtpport' => '587',
  'mail_smtpname' => 'admin@MEINEDOMAIN.de',
  'mail_smtppassword' => 'MEIN-123-PASSWORT',

Direkter Login (umgeht OIDC/SAML)

http://nextcloud.MEINEDOMAIN.de/login?direct=1 

Troubleshooting

Zugriff ueber eine nicht vertrauenswuerdige Domain

image.png

Zugriff über eine nicht vertrauenswürdige Domain
Bitte kontaktieren Sie Ihren Administrator. Wenn Sie Administrator sind, bearbeiten Sie die „trusted_domains“-Einstellung in config/config.php. Siehe Beispiel in config/config.sample.php.

falls die trusted domains in den env nicht passen, siehe ....
      - NEXTCLOUD_TRUSTED_DOMAINS=<nextcloud.mydomain.com>
      - OVERWRITEPROTOCOL=https
      - OVERWRITECLIURL=https://<nextcloud.mydomain.com>
...muss die config/config.php noch bearbeitet werden:

'trusted_domains' =>
  array (
   0 => 'localhost',
   1 => 'server1.example.com',
   2 => '192.168.1.50',
   3 => '[fe80::1:50]',
),

image.png

PERMISSION / OWNER FEHLER

Configuration was not read or initialized correctly, not overwriting /var/www/html/config/config.php

Passiert evtl beim Migrieren

image.png

Wenn dieser Fehler kommt, kann es sein, dass der Besitz oder die Berechtigungen der Dateien im Volume nicht stimmen.

image.png
Hier zu sehen im CLI Fileexplorer Ranger, dass der Besitzer root ist (unten links), sollte aber www-data sein.
Um den Besitzer zu ändern, eine Ebene höher gehen, sodass der Ordner _data zu sehen ist, mit ! ein Shell Command ausführen
('@'-Zeichen ist der Shortcut für Shell-Kommando in Ranger):

chown -R www-data:www-data _data

das ändert die Rechte rekursiv, also auch alle untergeordneten Dateien und Ordner:

image.png

ALTE VARIANTEN AB HIER

Kunde PL

nicht-samba-variante: (Diese Variante installiert sich komplett selbst)

stand 22.11.2024, aktuelle docker-image-versionen ausm dockerhub holen:

https://hub.docker.com/r/collabora/code
https://hub.docker.com/_/nextcloud
https://hub.docker.com/_/postgres
https://hub.docker.com/_/redis 

version: '3'

services:
  db:
    image: postgres:16.5-alpine3.20
    restart: always
    volumes:
      - db:/var/lib/postgresql/data:Z
    env_file:
      - stack.env

  redis:
    image: redis:7.4.1-alpine3.20
    restart: always

  app:
    image: nextcloud:30.0.2-apache
    restart: always
    ports:
      - 8654:80
    volumes:
      - app:/var/www/html:z
    environment:
      - POSTGRES_HOST=db
      - REDIS_HOST=redis
    env_file:
      - stack.env
    depends_on:
      - db
      - redis
    deploy:
      resources:
        limits:
          cpus: '0.90'
          memory: 4000M

  cron:
    image: nextcloud:30.0.2-apache
    restart: always
    volumes:
      - app:/var/www/html:z
    entrypoint: /cron.sh
    depends_on:
      - db
      - redis

  whiteboard:
    image: ghcr.io/nextcloud-releases/whiteboard:v1.0.4
    ports:
      - 3002:3002
    environment:
      - NEXTCLOUD_URL=https://nextcloud.DOMAIN.de
      - JWT_SECRET_KEY=XYZ123...[openssl rand -base64 32]...321ZYX
    restart: unless-stopped

  collabora:
    image: collabora/code:24.04.9.2.1
    container_name: collabora
    environment:
      - aliasgroup1=https://nextcloud.DOMAIN.de
      - aliasgroup2=https://another.DOMAIN.de
      - aliasgroup3=https://another.DOMAIN.de
      #- server_name=collabora.DOMAIN.de
      - username=MYUSERNAME
      - password=MYPASSWORD
    ports:
      - '9980:9980'
    restart: unless-stopped


volumes:
  db:
  app:

dazugehörige Environment variables (bei einem Portainer stack unten auf advanced mode stellen, da steht auch dass sie als stack.env eingebunden werden müssen)

POSTGRES_PASSWORD=MEINPASSWORD123
POSTGRES_DB=nextcloud
POSTGRES_USER=nextcloud

NEXTCLOUD_ADMIN_USER=MEIN-NC-ADMIN
NEXTCLOUD_ADMIN_PASSWORD=MEIN-NC-PW

OVERWRITEPROTOCOL=https
OVERWRITECLIURL=https://nextcloud.MEINEDOMAIN.de
NEXTCLOUD_TRUSTED_DOMAINS=nextcloud.MEINEDOMAIN.de
NEXTCLOUD_DEFAULT_LANGUAGE=de

OBJECTSTORE_S3_BUCKET=nextcloud
OBJECTSTORE_S3_KEY=DC...MEIN-minio-ACCESSKEY...dVZP
OBJECTSTORE_S3_SECRET=jJsHxEhdIJUM....MEIN-minio-SECRETKEY....4xTd9REse
OBJECTSTORE_S3_HOST=minio-s3.MEINEDOMAIN.de
OBJECTSTORE_S3_PORT=443
OBJECTSTORE_S3_SSL=true
OBJECTSTORE_S3_USEPATH_STYLE=true

Diese env-vars werden nur beim ersten Erzeugen in die config/config.php geschrieben. Wenn sie bei einer bestehenden Instanz nachgetragen werden, muss man die config.php von Hand bearbeiten.

weitere interessante Einstellungen in der config/config.php:

https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/config_sample_php_parameters.html 

  'default_language' => 'de',
  'default_locale' => 'de',
  'default_timezone' => 'Europe/Berlin',
  'defaultapp' => 'files',
  'knowledgebaseenabled' => false,
  'lost_password_link' => 'disabled',
  'skeletondirectory' => '',
  'simpleSignUpLink.shown' => false,
  'loglevel' => 2,
  'default_charset' => 'UTF-8',
  'activity_use_cached_mountpoints' => true,
  'forbidden_filename_characters' => array('?', '<', '>', ':', '*', '|', '"'),
  

Whiteboard: https://github.com/nextcloud/whiteboard 

samba-variante

version: '3'

services:
  db:
    image: postgres:alpine
    restart: always
    volumes:
      - hetzner_sb:/var/lib/postgresql/data:Z
    env_file:
      - stack.env

  redis:
    image: redis:alpine
    restart: always

  app:
    image: nextcloud:apache
    restart: always
    ports:
      - 8654:80
    volumes:
      - hetzner_sb:/var/www/html:z
    environment:
      - POSTGRES_HOST=db
      - REDIS_HOST=redis
    env_file:
      - stack.env
    depends_on:
      - db
      - redis

  cron:
    image: nextcloud:apache
    restart: always
    volumes:
      - hetzner_sb:/var/www/html:z
    entrypoint: /cron.sh
    depends_on:
      - db
      - redis


volumes:
  db:
  hetzner_sb:
    driver: local
    driver_opts:
      type: cifs
      o: "username=u12345-sub1,password=zwwEXAMPLEQpp,file_mode=0770,dir_mode=0770,vers=3.1.1,seal,uid=33"
      device: "//u12345-sub1.your-storagebox.de/u12345-sub1/dockervolume_nextcloud"

...,file_mode=0770,dir_mode=0770,vers=3.1.1,seal,uid=33"am ende nicht vergessen!

Quellen:
https://help.nextcloud.com/t/how-to-get-a-rock-solid-nextcloud-installation/150002
https://github.com/nextcloud/docker/blob/master/.examples/docker-compose/insecure/postgres/apache/docker-compose.yml 

Mit MariaDB von 

https://xmpls.org/install-nextcloud-with-docker-compose/


Kunde WIS

version: '2'
 
services:
  db:
    image: mariadb:10.5
    command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW
    volumes:
      - ./nextcloud-mariadb/mariadb:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=nextclouddb
      - MYSQL_PASSWORD=nextclouddb
      - MYSQL_DATABASE=nextcloud
      - MYSQL_USER=nextcloud
    restart: unless-stopped
 
  app:
    image: nextcloud
    ports:
      - 5001:80
    links:
      - db
    volumes:
      - ./nextcloud-mariadb/nextcloud-itself:/var/www/html
    environment:
      - MYSQL_PASSWORD=nextclouddb
      - MYSQL_DATABASE=nextcloud
      - MYSQL_USER=nextcloud
      - MYSQL_HOST=db
    restart: unless-stopped

Für weitere Schritte (DOMAIN ZU TRUSTED DOMAINS HINZUFÜGEN), den Nextcloud Artikel im Buch TrueNAS folgen:

https://wiki.folkerts.it/books/truenas/page/nextcloud-configphp-anpassen-fuer-trusted-domains-und-ssl 

Fernnetz (arm)

WICHTIG! Die Environment-Variablen NEXTCLOUD_TRUSTED_DOMAINS, OVERWRITEPROTOCOL und OVERWRITECLIURL werden in die config.php von Nextcloud nur bei Erstellung des Containers geschrieben. Eine nachträgliche Änderung ist nicht möglich (zumindest nicht ueber docker-compose ENVs. Wenn man es aendern möchte muss man die config.php im container editieren). 
Siehe https://github.com/nextcloud/docker/issues/582#issuecomment-834225766

<nextcloud.mydomain.com> ersetzen

version: '2'
 
services:
  db:
    image: mariadb:10.5
    command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW
    volumes:
      - ./nextcloud/mariadb:/var/lib/mysql #befindet sich unter /data/compose/<stack-nummer>/... auf dem docker host
    environment:
      - MYSQL_ROOT_PASSWORD=nextclouddb
      - MYSQL_PASSWORD=nextclouddb
      - MYSQL_DATABASE=nextcloud
      - MYSQL_USER=nextcloud
    restart: unless-stopped
 
  app:
    image: nextcloud
    ports:
      - 5001:80
    links:
      - db
    volumes:
      - ./nextcloud/app:/var/www/html
    environment:
      - NEXTCLOUD_TRUSTED_DOMAINS=<nextcloud.mydomain.com>
      - OVERWRITEPROTOCOL=https
      - OVERWRITECLIURL=https://<nextcloud.mydomain.com>

      - MYSQL_PASSWORD=nextclouddb
      - MYSQL_DATABASE=nextcloud
      - MYSQL_USER=nextcloud
      - MYSQL_HOST=db
    restart: unless-stopped

Docker-Compose .yamls

Gitlab Community Edition (CE)

WIS

ACHTUNG: Systemmindestvoraussetzungen beachten:  4-Core CPU, 4GB RAM
https://docs.gitlab.com/ee/install/requirements.html 
Ansonsten legt es den Server lahm (hohe Festplatten I/Os und CPU am Limit, nichts geht mehr)

version: '3.6'
services:
  web:
    image: 'gitlab/gitlab-ce:latest'
    restart: always
    hostname: 'gl.wo....se.com'
    environment:
      GITLAB_OMNIBUS_CONFIG: |
        external_url 'https://gl.wo...se.com'
        # Add any other gitlab.rb configuration here, each on its own line
    ports:
      - '9680:80'
      - '9643:443'
      - '9622:22'
    volumes:
      - './gitlab/config:/etc/gitlab'
      - './gitlab/logs:/var/log/gitlab'
      - './gitlab/data:/var/opt/gitlab'
    shm_size: '256m'

danach in die container shell gehen und mit sudo cat /etc/gitlab/initial_root_passworddas pw für den Benutzer root (oder admin? oder frei wählbar?) anzeigen lassen

Quelle auch mit Hinweis zu Kubernetes secret: 
https://forum.gitlab.com/t/after-gitlab-ce-installation-on-ubuntu/32896/7

Docker-Compose .yamls

Mattermost

WIS

https://docs.mattermost.com/install/install-docker.html

Installationsanleitung folgen. Hier in kurzform:

git clone https://github.com/mattermost/docker
cd docker
cp env.example .env
mkdir -p ./volumes/app/mattermost/{config,data,logs,plugins,client/plugins,bleve-indexes}
sudo chown -R 2000:2000 ./volumes/app/mattermost


Beispiel OHNE Nginx incl. (weil wir schon einen NGINX Reverse Proxy Manager haben)

sudo docker-compose -f docker-compose.yml -f docker-compose.without-nginx.yml up -d

und

sudo docker-compose -f docker-compose.yml -f docker-compose.without-nginx.yml down
Docker-Compose .yamls

WatchTower

FN

version: "3" 
services: 
  watchtower:     
    image: containrrr/watchtower
    container_name: watchtower 
    restart: always 
    environment: 
      WATCHTOWER_SCHEDULE: "0 0 6 * * *" 
      TZ: Europe/Berlin
      WATCHTOWER_CLEANUP: "true" 
      WATCHTOWER_DEBUG: "true"      
      #WATCHTOWER_NOTIFICATIONS: "email" 
      #WATCHTOWER_NOTIFICATION_EMAIL_FROM: "cldocker01@cloud.local"
      #WATCHTOWER_NOTIFICATION_EMAIL_TO: "pushover@mailrise.xyz" 
      # you have to use a network alias here, if you use your own certificate 
      #WATCHTOWER_NOTIFICATION_EMAIL_SERVER: "10.1.149.19" 
      #WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT: "8025" 
      #WATCHTOWER_NOTIFICATION_EMAIL_DELAY: 2 
    volumes: 
      - /var/run/docker.sock:/var/run/docker.sock

 

Docker-Compose .yamls

n8n

Wichtig!

vorher ein Docker Volume erzeugen und darin die Schreibrechte anpassen, sonst kommt die Fehlermeldung

EACCES: permission denied, open '/home/node/.n8n/crash.journal'

siehe https://github.com/n8n-io/n8n/issues/1240

also:

docker volume create n8n_data

docker run -it --rm -v n8n_data:/home/data bash chown 1000:1000 -R /home/data
oder einfacher:
chown -R 1000:1000 /var/lib/docker/volumes/n8n_data/_data

danach, entweder einen Container temporär starten:

docker run -it --rm --name n8n -p 5678:5678 -v n8n_data:/home/node/.n8n n8nio/n8n

oder über docker-compose:

version: "3"

services:
  n8n:
    image: n8nio/n8n:1.76.0
    restart: always
    ports:
      - 5678:5678
    environment:
      #- PUID=1001
      #- PGID=1001
      - N8N_HOST=n8n.MEINEDOMAIN.de
      - N8N_PORT=5678
      - N8N_PROTOCOL=https
      - NODE_ENV=production
      - WEBHOOK_URL=https://n8n.MEINEDOMAIN.de
      - GENERIC_TIMEZONE=Europe/Berlin
    volumes:
      - n8n_data:/home/node/.n8n

volumes:
  n8n_data:
    driver: local

Docker-Compose .yamls

UpSnap WakeOnLan WOL

version: "3"
services:
  upsnap:
    container_name: upsnap
    image: ghcr.io/seriousm4x/upsnap:4
    network_mode: host
    restart: unless-stopped
    volumes:
      - ./data:/app/pb_data
    # # To use a non-root user, create the mountpoint first (mkdir data) so that it has the right permission.
    # user: 1000:1000
    environment:
      - TZ=Europe/Berlin # Set container timezone for cron schedules
    #   - UPSNAP_INTERVAL=@every 10s # Sets the interval in which the devices are pinged
    #   - UPSNAP_SCAN_RANGE=192.168.1.0/24 # Scan range is used for device discovery on local network
    #   - UPSNAP_WEBSITE_TITLE=Custom name # Custom website title
    # # dns is used for name resolution during network scan
    # dns:
    #   - 192.18.0.1
    #   - 192.18.0.2
    # # you can change the listen ip:port inside the container like this:
    entrypoint: /bin/sh -c "./upsnap serve --http 0.0.0.0:5080"
    healthcheck:
      test: curl -fs "http://localhost:5080/api/health" || exit 1
      interval: 10s
    # # or install custom packages for shutdown
    # entrypoint: /bin/sh -c "apk update && apk add --no-cache <YOUR_PACKAGE> && rm -rf /var/cache/apk/* && ./upsnap serve --http 0.0.0.0:8090"

 

Docker-Compose .yamls

Shlink URL-Shortener

version: '3'

services:
  shlink:
    container_name: shlink
    image: shlinkio/shlink:stable
    restart: always
    environment:
      TZ: 'Europe/Berlin'
      IS_HTTPS_ENABLED: true
      INITIAL_API_KEY: 'JUs.....fzT'
      DEFAULT_DOMAIN: 'link.folkerts.it'
    ports:
      - 8066:8080

Wenn alles geklappt hat, kommt eine 404 not found:
(damit ich die Seite sehen kann, hab ich in der Hetzner Firewall meine IP für Port 8066 erlaubt)

image.png

Es ist jetzt erstmal nur der Server installiert, eine WebUI gibt es offiziell von shlink unter app.shlink.io 
man gibt dann seinen API Key und die Serveradresse ein und verbindet sich zum eigenen Shlink Server. Vorher muss man allerdings eine https Umleitung einrichten, damit es klappt, weil nicht von https auf http zugegriffen werden kann.
(vorher natürlich die subdomain zb link.folkerts.it beim domainhoster zb strato oder web4you registrieren)
zb mit nginx proxy manager:

image.png

und

image.png

danach klappt der Abruf via RestAPI mit zb Postman:

die Authorisierung (sieht man bei Klick auf die Collection links, also der 'Ordner')

image.png

und dann der Request:

image.png

 

 

so sieht dann die Konfiguration auf https://app.shlink.io/ aus

 

image.png

 

und so die webui:

image.png

Docker-Compose .yamls

OpenProject

Port 8080 hatte ich probiert zu ändern, hat aber beim ersten Versuch nicht geklappt, daher ist es jetzt dabei geblieben. Wenn man einen Unifi Controller in Docker hat muss man allerdings zwangsweise auf einen anderen Port ausweichen, weil Unifi den 8080er braucht.

OPENPROJECT_HOST__NAME und ggf. Postgres-Passwort abändern!

version: "3.7"

networks:
  frontend:
  backend:

volumes:
  pgdata:
  opdata:

x-op-restart-policy: &restart_policy
  restart: unless-stopped
x-op-image: &image
  image: openproject/community:${TAG:-13}
x-op-app: &app
  <<: [*image, *restart_policy]
  environment:
    OPENPROJECT_HTTPS: "${OPENPROJECT_HTTPS:-true}"
    OPENPROJECT_HOST__NAME: "${OPENPROJECT_HOST__NAME:-openproject.MEINEDOMAIN.DE}"
    OPENPROJECT_HSTS: "${OPENPROJECT_HSTS:-true}"
    RAILS_CACHE_STORE: "memcache"
    OPENPROJECT_CACHE__MEMCACHE__SERVER: "cache:11211"
    OPENPROJECT_RAILS__RELATIVE__URL__ROOT: "${OPENPROJECT_RAILS__RELATIVE__URL__ROOT:-}"
    DATABASE_URL: "${DATABASE_URL:-postgres://postgres:p4ssw0rd@db/openproject?pool=20&encoding=unicode&reconnect=true}"
    RAILS_MIN_THREADS: ${RAILS_MIN_THREADS:-4}
    RAILS_MAX_THREADS: ${RAILS_MAX_THREADS:-16}
    # set to true to enable the email receiving feature. See ./docker/cron for more options
    IMAP_ENABLED: "${IMAP_ENABLED:-false}"
  volumes:
    - "${OPDATA:-opdata}:/var/openproject/assets"

services:
  db:
    image: postgres:13
    <<: *restart_policy
    stop_grace_period: "3s"
    volumes:
      - "${PGDATA:-pgdata}:/var/lib/postgresql/data"
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-p4ssw0rd}
      POSTGRES_DB: openproject
    networks:
      - backend

  cache:
    image: memcached
    <<: *restart_policy
    networks:
      - backend

  proxy:
    <<: [*image, *restart_policy]
    command: "./docker/prod/proxy"
    ports:
      - "${PORT:-8080}:80"
    environment:
      APP_HOST: web
      OPENPROJECT_RAILS__RELATIVE__URL__ROOT: "${OPENPROJECT_RAILS__RELATIVE__URL__ROOT:-}"
    depends_on:
      - web
    networks:
      - frontend

  web:
    <<: *app
    command: "./docker/prod/web"
    networks:
      - frontend
      - backend
    depends_on:
      - db
      - cache
      - seeder
    labels:
      - autoheal=true
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080${OPENPROJECT_RAILS__RELATIVE__URL__ROOT:-}/health_checks/default"]
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 30s

  autoheal:
    image: willfarrell/autoheal:1.2.0
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock"
    environment:
      AUTOHEAL_CONTAINER_LABEL: autoheal
      AUTOHEAL_START_PERIOD: 600
      AUTOHEAL_INTERVAL: 30

  worker:
    <<: *app
    command: "./docker/prod/worker"
    networks:
      - backend
    depends_on:
      - db
      - cache
      - seeder

  cron:
    <<: *app
    command: "./docker/prod/cron"
    networks:
      - backend
    depends_on:
      - db
      - cache
      - seeder

  seeder:
    <<: *app
    command: "./docker/prod/seeder"
    restart: on-failure
    networks:
      - backend

danach den NGINX einfach auf https://docker-host-ip:8080 umleiten:

image.png

man muss ein bisschen rumspielen mit der Zeile bei den environment-variablen

OPENPROJECT_HTTPS: "${OPENPROJECT_HTTPS:-true}"

Ich hatte es zuerst auf false, dann den nginx eingerichtet, dann kam auf der OpenProject GUI die Meldung, dass die Variable wieder auf true gesetzt werden muss. Danach klappt es, obwohl NGINX noch auf http verweist. Keine Ahnung was da los ist. Vielleicht klappt es auch direkt wenn die Variable auf true ist und der HOST_NAME in der nächsten Zeile direkt richtig ist. Das hatte ich zu Beginn vergessen.

Docker-Compose .yamls

Windmill

Server um Pythonprogramme über eine WebUI mit APIs zu verknüpfen

https://www.windmill.dev/docs/advanced/self_host
https://github.com/windmill-labs/windmill/blob/main/docker-compose.yml
https://github.com/windmill-labs/windmill/blob/main/.env
https://github.com/windmill-labs/windmill/blob/main/Caddyfile

OBACHT, die docker-compose.yml für Windmill + Selenium ist die aktuellste. Darunter kommt eine alte docker-compose.yml mit nur Windmill ohne Selenium

Windmill + Selenium

OBACHT: Probleme mit arm-Architektur und Selenium Browser. Nach umzug auf amd64 klappt der selenoid container. Außerdem gibt es das selenoid-ui image aktuell nicht für arm.

Mit diesem Tutorial als Basis: https://www.windmill.dev/blog/use-selenium-with-windmill 

und mit Hilfe der Selenium MAN https://aerokube.com/selenoid/latest/ 

docker-compose.yml

#https://www.windmill.dev/blog/use-selenium-with-windmill
#https://wiki.folkerts.it/books/docker/page/windmill

services:
  db:
    hostname: wm_db
    deploy:
      # To use an external database, set replicas to 0 and set DATABASE_URL to the external database url in the .env file
      replicas: 1
    image: postgres:14
    restart: unless-stopped
    volumes:
      - db_data:/var/lib/postgresql/data
    expose:
      - 5432
    environment:
      POSTGRES_PASSWORD: ${DATABASE_PW}
      POSTGRES_DB: windmill
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      windmill:

  windmill_server:
    hostname: wm_server
    image: ${WM_IMAGE}
    pull_policy: always
    deploy:
      replicas: 1
    restart: unless-stopped
    expose:
      - 8000
    ports:
      - 9920-9930:9920-9930 # <- added this; only 10 ports are opened; if you want to open more ports increase the 2nd number respectively
    environment:
      - DATABASE_URL=postgres://postgres:${DATABASE_PW}@db/windmill?sslmode=disable
      - MODE=server
      #- NUM_WORKERS=10 # <- an increased number of workers is helpful when running a lot of scraping scripts in parallel
    depends_on:
      db:
        condition: service_healthy
    networks:
      windmill:

  windmill_worker:
    #container_name: wm_worker
    image: ${WM_IMAGE}
    pull_policy: always
    deploy:
      replicas: 2
      resources:
        limits:
          cpus: "1"
          memory: 2048M
    restart: unless-stopped
    environment:
      - DATABASE_URL=postgres://postgres:${DATABASE_PW}@db/windmill?sslmode=disable
      - MODE=worker
      - WORKER_GROUP=default
    depends_on:
      db:
        condition: service_healthy
    # to mount the worker folder to debug, KEEP_JOB_DIR=true and mount /tmp/windmill
    volumes:
      # mount the docker socket to allow to run docker containers from within the workers
      - /var/run/docker.sock:/var/run/docker.sock
      - worker_dependency_cache:/tmp/windmill/cache
    networks:
      windmill:

  ## This worker is specialized for "native" jobs. Native jobs run in-process and thus are much more lightweight than other jobs
  windmill_worker_native:
    #container_name: wm_worker_nat
    # Use ghcr.io/windmill-labs/windmill-ee:main for the ee
    image: ${WM_IMAGE}
    pull_policy: always
    deploy:
      replicas: 2
      resources:
        limits:
          cpus: "0.1"
          memory: 128M
    restart: unless-stopped
    environment:
      - DATABASE_URL=postgres://postgres:${DATABASE_PW}@db/windmill?sslmode=disable
      - MODE=worker
      - WORKER_GROUP=native
    depends_on:
      db:
        condition: service_healthy
    networks:
      windmill:

  ## This worker is specialized for reports or scrapping jobs. It is assigned the "reports" worker group which has an init script that installs chromium and can be targeted by using the "chromium" worker tag.
  # windmill_worker_reports:
  #   image: ${WM_IMAGE}
  #   pull_policy: always
  #   deploy:
  #     replicas: 1
  #     resources:
  #       limits:
  #         cpus: "1"
  #         memory: 2048M
  #   restart: unless-stopped
  #   environment:
  #     - DATABASE_URL=${DATABASE_URL}
  #     - MODE=worker
  #     - WORKER_GROUP=reports
  #   depends_on:
  #     db:
  #       condition: service_healthy
  #   # to mount the worker folder to debug, KEEP_JOB_DIR=true and mount /tmp/windmill
  #   volumes:
  #     # mount the docker socket to allow to run docker containers from within the workers
  #     - /var/run/docker.sock:/var/run/docker.sock
  #     - worker_dependency_cache:/tmp/windmill/cache

  lsp:
    hostname: wm_lsp
    image: ghcr.io/windmill-labs/windmill-lsp:latest
    pull_policy: always
    restart: unless-stopped
    expose:
      - 3001
    volumes:
      - lsp_cache:/root/.cache
    networks:
      windmill:

  multiplayer:
    hostname: wm_mp
    image: ghcr.io/windmill-labs/windmill-multiplayer:latest
    deploy:
      replicas: 0 # Set to 1 to enable multiplayer, only available on Enterprise Edition
    restart: unless-stopped
    expose:
      - 3002
    networks:
      windmill:

  caddy:
    hostname: wm_caddy
    image: caddy:2.5.2-alpine
    restart: unless-stopped

    # Configure the mounted Caddyfile and the exposed ports or use another reverse proxy if needed
    volumes:
      - ${CADDYFILE_PATH}:/etc/caddy/Caddyfile
      # - ./certs:/certs # Provide custom certificate files like cert.pem and key.pem to enable HTTPS - See the corresponding section in the Caffyfile
    ports:
      # To change the exposed port, simply change 80:80 to <desired_port>:80. No other changes needed
      - ${HTTP_EXPOSE_PORT_WINDMILL}:80
      # - 443:443 # Uncomment to enable HTTPS handling by Caddy
    environment:
      - BASE_URL=":80"
#      - ADDRESS="localhost"
      # - BASE_URL=":443" # uncomment and comment line above to enable HTTPS via custom certificate and key files
      # - BASE_URL=mydomain.com # Uncomment and comment line above to enable HTTPS handling by Caddy
    networks:
      windmill:

  selenoid:
    #network_mode: bridge
    hostname: wm_selenoid
    restart: unless-stopped
    image: aerokube/selenoid:latest-release
    volumes:
      - sel_cfg:/etc/selenoid # <- change this
      - sel_video:/opt/selenoid/video # <- change this
      - sel_logs:/opt/selenoid/logs # <- change this
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - OVERRIDE_VIDEO_OUTPUT_DIR=${SELENOID_VIDEO_DIR}
    command:
      [
        '-conf',
        '/etc/selenoid/browsers.json',
        '-video-output-dir',
        '/opt/selenoid/video',
        '-log-output-dir',
        '/opt/selenoid/logs',
        '-container-network',
        '${SELENOID_CONTAINER_NETWORK}'
      ]
    ports:
      - 4444:4444
    networks:
      windmill:

  selenoid-ui:
    hostname: wm_selenoid-ui
    image: aerokube/selenoid-ui
    #network_mode: bridge
    restart: unless-stopped
    depends_on:
      - selenoid
    #links:
    #  - selenoid
    ports:
      - ${HTTP_EXPOSE_PORT_SELENOIDUI}:8080
    command: ['--selenoid-uri', 'http://wm_selenoid:4444']
    networks:
      windmill:
    
volumes:
  db_data: null
  worker_dependency_cache: null
  lsp_cache: null
  sel_logs:
  sel_video:
  sel_cfg:

networks:
  windmill:
    driver: bridge

    

.env

HTTP_EXPOSE_PORT_WINDMILL=86
HTTP_EXPOSE_PORT_SELENOIDUI=8941
DATABASE_PW=MEINTOLLES..........DBPW
WM_IMAGE=ghcr.io/windmill-labs/windmill:1.466.2
CADDYFILE_PATH=/var/lib/docker/volumes/windmill_caddy/Caddyfile
SELENOID_VIDEO_DIR=/var/lib/docker/volumes/windmill_sel_video/_data
SELENOID_CONTAINER_NETWORK=windmill_windmill

TODO:
- Caddyfile erstellen (sieh unten)
- browsers.json erstellen (siehe unten)

Caddyfile

Auf dem Server eine Caddyfile anlegen:

zb /var/lib/docker/volumes/windmill_caddy/Caddyfile

{$BASE_URL} {
        bind {$ADDRESS}
        reverse_proxy /ws/* http://lsp:3001
        # reverse_proxy /ws_mp/* http://multiplayer:3002
        reverse_proxy /* http://windmill_server:8000
        # tls /certs/cert.pem /certs/key.pem
}

image.png

Diese Datei dann wie beschrieben in der docker-compose.yml verknüpfen

browsers.json

browsers.json für selenoid Container erstellen und Pfad dafür in docker-compose (ca Zeile 171) in 

- sel_cfg:/etc/selenoid # <- change this

ablegen, also zb /var/lib/docker/volumes/windmill_sel_cfg/_data/browsers.json

{
	"firefox": {
		"default": "104.0",
		"versions": {
			"104.0": {
				"image": "selenoid/firefox:104.0",
				"port": "4444",
				"path": "/wd/hub",
				"env": ["TZ=Europe/Berlin"]
			}
		}
	},
	"chrome": {
		"default": "104.0",
		"versions": {
			"104.0": {
				"image": "selenoid/chrome:104.0",
				"port": "4444",
				"path": "/",
				"env": ["TZ=Europe/Berlin"]
			}
		}
	}
}

image.png

Login

email: 
admin@windmill.dev

pw:
changeme

webui ist danach unter http://docker-ip:86 zu erreichen
prefilled link: http://docker-ip:86/user/login?email=admin@windmill.dev&password=changeme

image.png

Docker-Images für Selenium Browser

Nun noch die Dockerimages der Browser pullen

docker pull selenoid/chrome:104.0
docker pull selenoid/vnc_chrome:104.0
docker pull selenoid/firefox:104.0
docker pull selenoid/vnc_firefox:104.0

Docker-Image für Selenium Videorecording

https://aerokube.com/selenoid/latest/#_video_recording 

docker pull selenoid/video-recorder:latest-release

Python Beispiel mit Selenium in Windmill

# extra_requirements:
# blinker==1.7.0
# selenium==4.9.1

import os
import wmill
import blinker
import selenium
from seleniumwire import webdriver
from webdriver_manager.chrome import ChromeDriverManager


def main(domain: str, mailbox_prefix: str, mailbox_password: str):
    try:
        driver = initiateDriver()
        # driver.get("https://www.github.com")

        # Test whether Seleniumwire is working
        for request in driver.requests:
            if request.response:
                print(
                    request.url,
                    request.response.status_code,
                    request.response.headers["Content-Type"],
                )

        # Geheimnisse aus Windmill-Variablen laden
        strato_username = wmill.get_variable("f/admintools/strato_username")
        strato_password = wmill.get_variable("f/admintools/strato_password")

        # Selenium-Importe innerhalb der Funktion (Windmill-Umgebung)
        import time
        from selenium import webdriver
        from selenium.webdriver.common.by import By
        from selenium.webdriver.common.action_chains import ActionChains
        from selenium.webdriver.support import expected_conditions
        from selenium.webdriver.support.wait import WebDriverWait
        from selenium.webdriver.common.keys import Keys
        from selenium.webdriver.common.desired_capabilities import DesiredCapabilities

        # WebDriver starten (Chrome als Beispiel)
        print("### Öffne strato.de")
        driver.get("https://www.strato.de/")
        driver.set_window_size(1920, 1080)
        driver.implicitly_wait(5)

        # Selenium-Schritte

        print("### Akzeptiere Cookies")
        driver.find_element(By.CSS_SELECTOR, ".consentAgree:nth-child(3)").click()
        driver.find_element(By.CSS_SELECTOR, "li:nth-child(3) .text-uppercase").click()
        print("### Füge Strato Username ein")
        driver.find_element(By.ID, "username").send_keys(strato_username)
        print("### Füge Strato Passwort ein")
        driver.find_element(By.ID, "jss_ksb_password").send_keys(strato_password)
        print("### Klicke auf Anmelden")
        driver.find_element(By.NAME, "action_customer_login.x").click()
        print("### Klicke auf E-Mail")
        driver.find_element(By.LINK_TEXT, "E-Mail").click()
        print("### Klicke auf Verwaltung")
        driver.find_element(By.LINK_TEXT, "Verwaltung").click()
        print("### Klicke auf Postfach anlegen")
        driver.find_element(By.ID, "jss_create_mailbox").click()
        print("### Wähle Basic-Postfach anlegen")
        driver.find_element(By.LINK_TEXT, "Basic-Postfach anlegen").click()
        print("### Klicke auf Mailbox Domain Dropdown")
        driver.find_element(By.ID, "create_mailbox_domain_select").click()
        print("### Wähle domain aus Domain-Dropdown über send_keys")
        select = driver.find_element(By.ID, "create_mailbox_domain_select")
        select.send_keys(f"{domain}")
        driver.find_element(By.ID, "group1").send_keys(mailbox_prefix)
        print("### Füge E-Mail Passwort ein")
        driver.find_element(By.NAME, "password_new").send_keys(mailbox_password)
        print("### Klicke auf Postfach anlegen")
        driver.find_element(By.CSS_SELECTOR, ".jss_action_handle_password").click()
        print(
            '### Warte auf den Text "Es wurde eine neue E-Mail-Adresse angelegt und aktiviert" von Strato'
        )
        WebDriverWait(driver, 5).until(
            expected_conditions.visibility_of_element_located(
                (By.CSS_SELECTOR, ".success > p:nth-child(1)")
            )
        )
        print(f"### Erfolg! {mailbox_prefix}@{domain} wurde angelegt.")

        # WebDriver schließen
        driver.quit()

        # Ergebnis zurückgeben (wird in Windmill als JSON dargestellt)
        return {
            "domain": domain,
            "mailbox_prefix": mailbox_prefix,
            "mailbox_passwort": mailbox_password,
            "status": "Mailbox erfolgreich erstellt",
        }
    except Exception as e:
        print(
            "FEHLER. Mailadresse evtl. schon vorhanden? Nicht genug Strato Mail Speicherplatz verfügbar? Passwort entspricht nicht den Anforderungen? Anforderung Stratomail-Passwort: Mindestens 10 Zeichen, Maximal 128 Zeichen, erlaubte Buchstaben: a-z und A-Z, keine Umlaute, erlaubte Ziffern: 0-9, erlaubte Sonderzeichen: !#$%&()*+,-./:;<>=?@[]^_{|}~"
        )
        print(f"Fehlermeldung: {e}")
        return {
            "domain": domain,
            "mailbox_prefix": mailbox_prefix,
            "mailbox_passwort": mailbox_password,
            "status": "Fehler beim erstellen der Mailbox: Mailadresse evtl. schon vorhanden? Nicht genug Strato Mail Speicherplatz verfügbar? Passwort entspricht nicht den Anforderungen? Anforderung Stratomail-Passwort: Mindestens 10 Zeichen, Maximal 128 Zeichen, erlaubte Buchstaben: a-z und A-Z, keine Umlaute, erlaubte Ziffern: 0-9, erlaubte Sonderzeichen: !#$%&()*+,-./:;<>=?@[]^_{|}~",
            "error": f"{e}",
        }


def initiateDriver(macM1=False):
    print("initiating driver")

    # driver = None
    if (
        macM1
    ):  # if we are on mac m1 -> custom image by selecting the browser version 91.0
        i = 9919
        while True:
            try:
                i += 1
                HOST = "host.docker.internal"
                options = {
                    "auto_config": False,
                    # the addr and the port where the proxy should start: -> starts it in the windmill container
                    "addr": "0.0.0.0",
                    "port": i,
                }

                chrome_capabilities = {
                    "browserName": "chrome",
                    "browserVersion": "91.0",  # 91.0 for mac only?
                    "selenoid:options": {"enableVNC": True, "enableVideo": True},
                    "goog:chromeOptions": {
                        "extensions": [],
                        "args": [
                            f"--proxy-server=host.docker.internal:{i}",
                            "--ignore-certificate-errors",
                        ],
                    },
                }

                print(f"Test Selenium with port:{i}")
                driver = webdriver.Remote(
                    command_executor="http://{}:4444/wd/hub".format(HOST),
                    desired_capabilities=chrome_capabilities,
                    seleniumwire_options=options,
                )

                print(f"initiated successfully with port:{i}")
                break
            except:
                print(f"initiating driver with port:{i}")
                if i > 9930:
                    print("port limit exceeded")
                    break

    else:  # windows or linux image
        i = 9919
        while True:
            try:
                i += 1
                # HOST = "host.docker.internal"
                options = {
                    "auto_config": False,
                    # the addr and the port where the proxy should start: -> starts it in the windmill container
                    "addr": "0.0.0.0",
                    "port": i,
                }

                chrome_capabilities = {
                    "browserName": "chrome",
                    "browserVersion": "104.0",  # on Windows we can use the latest version by not specifying the version number
                    "selenoid:options": {"enableVNC": True, "enableVideo": True},
                    "goog:chromeOptions": {
                        "extensions": [],
                        "args": [
                            # f"--proxy-server=wm_server:{i}",
                            "--ignore-certificate-errors",
                        ],
                    },
                }
                driver = webdriver.Remote(
                    # command_executor="http://{}:4444/wd/hub".format(HOST),
                    command_executor="http://wm_selenoid:4444/wd/hub",
                    desired_capabilities=chrome_capabilities,
                    seleniumwire_options=options,
                )

                print(f"initiated successfully with port:{i}")
                break
            except Exception as e:
                print(f"initiating driver with port:{i}: {e}")
                if i > 9930:
                    print("port limit exceeded")
                    break

    return driver

Selenoid-UI

http://DOCKER-IP:8941 zeigt selenoid-ui mit Videorecordings der Seleniumausführunge

image.png

Windmill only (alte version)

folgendes ändern:
Zeile  15: DB Passwort
Zeile 128: Dateipfad für die Caddyfile (siehe unten)
Zeile 132: Port für die Windmill WebUI

.env Datei: DB Passwort (siehe unten)

docker-compose.yml

version: "3.7"

services:
  db:
    deploy:
      # To use an external database, set replicas to 0 and set DATABASE_URL to the external database url in the .env file
      replicas: 1
    image: postgres:14
    restart: unless-stopped
    volumes:
      - db_data:/var/lib/postgresql/data
    expose:
      - 5432
    environment:
      POSTGRES_PASSWORD: MEIN.....PASSWORT
      POSTGRES_DB: windmill
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

  windmill_server:
    image: ${WM_IMAGE}
    pull_policy: always
    deploy:
      replicas: 1
    restart: unless-stopped
    expose:
      - 8000
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - MODE=server
    depends_on:
      db:
        condition: service_healthy

  windmill_worker:
    image: ${WM_IMAGE}
    pull_policy: always
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: "1"
          memory: 2048M
    restart: unless-stopped
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - MODE=worker
      - WORKER_GROUP=default
    depends_on:
      db:
        condition: service_healthy
    # to mount the worker folder to debug, KEEP_JOB_DIR=true and mount /tmp/windmill
    volumes:
      # mount the docker socket to allow to run docker containers from within the workers
      - /var/run/docker.sock:/var/run/docker.sock
      - worker_dependency_cache:/tmp/windmill/cache

  ## This worker is specialized for "native" jobs. Native jobs run in-process and thus are much more lightweight than other jobs
  windmill_worker_native:
    # Use ghcr.io/windmill-labs/windmill-ee:main for the ee
    image: ${WM_IMAGE}
    pull_policy: always
    deploy:
      replicas: 2
      resources:
        limits:
          cpus: "0.1"
          memory: 128M
    restart: unless-stopped
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - MODE=worker
      - WORKER_GROUP=native
    depends_on:
      db:
        condition: service_healthy

  ## This worker is specialized for reports or scrapping jobs. It is assigned the "reports" worker group which has an init script that installs chromium and can be targeted by using the "chromium" worker tag.
  # windmill_worker_reports:
  #   image: ${WM_IMAGE}
  #   pull_policy: always
  #   deploy:
  #     replicas: 1
  #     resources:
  #       limits:
  #         cpus: "1"
  #         memory: 2048M
  #   restart: unless-stopped
  #   environment:
  #     - DATABASE_URL=${DATABASE_URL}
  #     - MODE=worker
  #     - WORKER_GROUP=reports
  #   depends_on:
  #     db:
  #       condition: service_healthy
  #   # to mount the worker folder to debug, KEEP_JOB_DIR=true and mount /tmp/windmill
  #   volumes:
  #     # mount the docker socket to allow to run docker containers from within the workers
  #     - /var/run/docker.sock:/var/run/docker.sock
  #     - worker_dependency_cache:/tmp/windmill/cache

  lsp:
    image: ghcr.io/windmill-labs/windmill-lsp:latest
    pull_policy: always
    restart: unless-stopped
    expose:
      - 3001
    volumes:
      - lsp_cache:/root/.cache

  multiplayer:
    image: ghcr.io/windmill-labs/windmill-multiplayer:latest
    deploy:
      replicas: 0 # Set to 1 to enable multiplayer, only available on Enterprise Edition
    restart: unless-stopped
    expose:
      - 3002

  caddy:
    image: caddy:2.5.2-alpine
    restart: unless-stopped

    # Configure the mounted Caddyfile and the exposed ports or use another reverse proxy if needed
    volumes:
      - ${CADDYFILE_PATH}:/etc/caddy/Caddyfile
      # - ./certs:/certs # Provide custom certificate files like cert.pem and key.pem to enable HTTPS - See the corresponding section in the Caffyfile
    ports:
      # To change the exposed port, simply change 80:80 to <desired_port>:80. No other changes needed
      - 86:80
      # - 443:443 # Uncomment to enable HTTPS handling by Caddy
    environment:
      - BASE_URL=":80"
#      - ADDRESS="localhost"
      # - BASE_URL=":443" # uncomment and comment line above to enable HTTPS via custom certificate and key files
      # - BASE_URL=mydomain.com # Uncomment and comment line above to enable HTTPS handling by Caddy

volumes:
  db_data: null
  worker_dependency_cache: null
  lsp_cache: null

.env

DATABASE_URL=postgres://postgres:MEIN......PASSWORT@db/windmill?sslmode=disable

# For Enterprise Edition, use:
# WM_IMAGE=ghcr.io/windmill-labs/windmill-ee:main
WM_IMAGE=ghcr.io/windmill-labs/windmill:main

#Pfad für selbst angelegte Caddyfile
CADDYFILE_PATH=/var/lib/docker/volumes/windmill_caddy/Caddyfile

# To use another port than :80, setup the Caddyfile and the caddy section of the docker-compose to your needs: https://caddyserver.com/docs/getting-started
# To have caddy take care of automatic TLS

image.png

Caddyfile erstellen und verknüpfen siehe oben

Docker-Compose .yamls

Uptime Kuma

name: uptimekuma
services:
    uptime-kuma:
        container_name: uptime-kuma
        image: louislam/uptime-kuma:1.23.16
        restart: always
        ports:
            - 3001:3001
        volumes:
            - data:/app/data
            - /var/run/docker.sock:/var/run/docker.sock #damit uptime auf die Docker Container zugreifen kann

volumes:
  data:

https://hub.docker.com/r/louislam/uptime-kuma/tags

https://github.com/louislam/uptime-kuma 

Um Windows-PCs zu Pingen, folgendes in CMD mit Adminrechten eingeben:

netsh advfirewall firewall add rule name="Allow ICMPv4-In" protocol=icmpv4:8,any dir=in action=allow

netsh advfirewall firewall add rule name="Allow ICMPv4-Out" protocol=icmpv4:8,any dir=out action=allow

Docker-Compose .yamls

Babby Buddy

 

---
services:
  babybuddy:
    image: lscr.io/linuxserver/babybuddy:latest
    container_name: babybuddy
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Berlin
      - CSRF_TRUSTED_ORIGINS=http://127.0.0.1:8000,https://babybuddy.MEINEDOMAIN.DE
    volumes:
      - /var/lib/docker/volumes/babybuddy/config:/config
    ports:
      - 8774:8000
    restart: unless-stopped

 

image.png

Docker-Compose .yamls

Thingsboard MQTT Broker 'TBMQ'

port 1883 auf 1889 geändert, weil die Thingsboardinstanz schon den Port 1883 verwendet.

 

Docker-Compose .yamls

Thingsboard

version: '3.0'
services:
  mytbpe:
    restart: always
    image: "thingsboard/tb-pe:3.6.4PE"
    networks:
      - tb-bridge
    ports:
      - "8080:8080"
      - "1883:1883"
      - "7070:7070"
      - "5683-5688:5683-5688/udp"
    logging:
      options:
        max-size: "100m"
        max-file: "5"
    environment:
      TB_QUEUE_TYPE: in-memory
      SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/thingsboard
      TB_LICENSE_SECRET: vbYq.......976
      TB_LICENSE_INSTANCE_DATA_FILE: /data/license.data
    volumes:
      - /var/lib/docker/volumes/tbpe-data:/data
      - /var/lib/docker/volumes/tbpe-logs:/var/log/thingsboard
  postgres:
    restart: always
    image: "postgres:12" # Eigentlich postgres:15
    networks:
      - tb-bridge
    ports:
    - "5432:5432"
    logging:
      options:
        max-size: "100m"
        max-file: "5"
    environment:
      POSTGRES_DB: thingsboard
      POSTGRES_PASSWORD: postgres
    volumes:
      - /var/lib/docker/volumes/tbpe-data/db:/var/lib/postgresql/data

networks:
  tb-bridge:
    name: thingsboard-bridge
    driver: bridge

Docker-Compose .yamls

homepage (Homelab Dashboard)

image.png

https://github.com/gethomepage/homepage

version: "3.3"
services:
  homepage:
    image: ghcr.io/gethomepage/homepage:v0.9.11
    container_name: homepage
    #environment:
      #PUID: 1000 -- optional, your user id
      #PGID: 1000 -- optional, your group id
    ports:
      - 3000:3000
    volumes:
      - config:/app/config # Make sure your local config directory exists
      - /var/run/docker.sock:/var/run/docker.sock:ro # optional, for docker integrations
    restart: unless-stopped

volumes:
 config:

Icons

https://selfh.st/icons/ mit sh-XXX zb sh-invoice-ninja-light

https://simpleicons.org/ mit si-XXX

 

services.yaml

 


Docker-Compose .yamls

restic (Autobackups von zb Docker)

#https://www.youtube.com/watch?v=WBBTC5WfGis
#https://github.com/JamesTurland/JimsGarage/blob/main/restic/docker-compose.yml

#https://github.com/djmaze/resticker
#https://github.com/djmaze/resticker/blob/master/docker-compose.example.yml

### Restore
## Find the latest snapshot for the current host (note the ID)
#docker-compose exec app restic snapshots -H <HOSTNAME>
# Restore the given file on the host
#docker-compose exec app restic restore --include /path/to/file <ID>

version: "3.3"

services:
  backup:
    image: mazzolino/restic
    hostname: dockerhetzner
    restart: unless-stopped
    environment:
      RUN_ON_STARTUP: "true"
      BACKUP_CRON: "0 30 4 * * *"
      RESTIC_REPOSITORY: s3:https://s3-ostfr1........luckycloud.de/REPO.......NAME
      RESTIC_PASSWORD: Ur6Hs.....................................Qqfz
      RESTIC_BACKUP_SOURCES: /mnt/data/compose
      RESTIC_BACKUP_ARGS: >-
        --tag docker-volumes
        --exclude 3/nextcloud-wolke
        #--exclude another-folder/with\ space
        #--exclude *.tmp
        --verbose
      RESTIC_FORGET_ARGS: >-
        --keep-last 10
        --keep-daily 7
        --keep-weekly 5
        --keep-monthly 12
      AWS_ACCESS_KEY_ID: GP.......................X3
      AWS_SECRET_ACCESS_KEY: wQ................koJ
      TZ: Europe/Berlin
    volumes:
      - /data/compose:/mnt/data/compose:ro

 

Docker-Compose .yamls

backrest (+ restic Befehle)

restic repo GUI mit Autobackups + Luckycloud S3-Storage oder Hetzner StorageBox

image.png

https://github.com/garethgeorge/backrest 

version: "3.2"
services:
  backrest:
    image: garethgeorge/backrest
    container_name: backrest
    hostname: backrest
    volumes:
      - data:/data
      - config:/config
      - cache:/cache
      - /var/lib/docker/volumes:/docker-var-lib
      - /data/compose:/docker-data-compose
      #- /MY-BACKUP-DATA:/userdata # [optional] mount local paths to backup here.
      #- /MY-REPOS:/repos # [optional] mount repos if using local storage, not necessary for remotes e.g. B2, S3, etc.
    environment:
      - BACKREST_DATA=/data # path for backrest data. restic binary and the database are placed here.
      - BACKREST_CONFIG=/config/config.json # path for the backrest config file.
      - XDG_CACHE_HOME=/cache # path for the restic cache which greatly improves performance.
      - TZ=Europe/Berlin # set the timezone for the container, used as the timezone for cron jobs.
    restart: unless-stopped
    ports:
      - 9898:9898

volumes:
  data:
  config:
  cache:

Konfiguration mit Luckycloud

In Luckyclouc einen S3 Speicher anlegen:

image.png

auf das grüne Schloss drücken, um die Zugangsdaten AWS_ACCESS_KEY_ID und AWS_SECRET_ACCESS_KEY zu erhalten:

image.png

die Zugangsdaten bei backrest als neues repo anlegen:

image.png

die repository url ist dann sowas wie

s3:https://<luckycloudserver>.luckycloud.de/<bucketname>

und ist zu finden im luckycloud portal:

image.png

als Plan dann zb folgendes:

image.png

oder /docker-var-lib als Path wenn man das Beispiel aus der docker-compose.yaml von oben nimmt

Konfiguration mit Hetzner Storage Box (SFTP)

  1. Unter Hetzner Robot SSH aktivieren:

    image.png



  2. Auf dem Server, auf dem Docker mit backrest als Container läuft, einen SSH-Key erzeugen, wenn noch nicht geschehen:
    ssh-keygen

     

  3. SSH Public Key an Storage Box SSH Port 23 übertragen (kann auch id_ed25519.pub heißen oä, wenn es kein rsa ist):
    cat ~/.ssh/id_rsa.pub | ssh -p23 uXXXXX@uXXXXX.your-storagebox.de install-ssh-key

  4. testweise nochmal mit der Storage Box über ssh verbinden, sollte nun ohne Bestätigung klappen:
    ssh -p 23 uXXXXX@uXXXXX.your-storagebox.de

    oder mit Angabe zum Private-Key

    ssh -i /root/.ssh/id_mein-admin_ed25519 -p 23 uXXXXX@uXXXXX.your-storagebox.de
  5. /home/<user>/.ssh/known_hosts und /home/<user>/.ssh/id_ed25519 (SSH Private Key) vom Docker Server in das Config Volume vom Backrest Container kopieren, sodass backrest Zugriff auf den PrivateKey und die known_hosts Datei hat:

    image.png


    oder als Bsp auf hz-02:

    image.png


  6. Neues Repository in backrest hinzufügen:

    image.png

    image.png

    Repository URL bei SFTP hinzufügen:
    sftp:user@host:/path/to/repo
    also zb 
    sftp:u12345@u12345.your-storagebox.de:ResticBackup/DockerServer

    Das Passwort für das Restic Backup kann man einfach mit klick auf Generate generieren lassen *udontsay.gif*

    Flags (sftp.args) hinzufügen:

    -o sftp.args="-i /config/ssh_keys/id_ed25519 -P 23 -o UserKnownHostsFile=/config/ssh_keys/known_hosts"

Falls es zu Problemen kommt, kann die SSH config angepasst werden, sodass man die SSH Flags weglassen kann:

Quelle: https://forum.restic.net/t/usability-issue-with-sftp-and-sftp-command/1170/2 

OBACHT 
Dieser "Trick" ist vollkommen sinnlos, weil man einen ssh-key und eine ssh-config in den flüchtigen ordner /root/.ssh im backrest-container legt. Nach jedem Neustart des Containers sind die Dateien also weg. Besser bei oben bleiben, wo man Flags hinzufügt und den SSH-Key sowie die known_hosts in das backrest_config Docker-Volume legt

docker exec -it backrest /bin/bash
nano /root/.ssh/config

.ssh/config:

Host storagebox
    Hostname u12345.your-storagebox.de
    Port 23
    User u12345
    IdentityFile /root/.ssh/id_admin_ed25519
    ODER
    IdentityFile /config/ssh_keys/id_admin_ed25519
    JE NACHDEM WO DER SSH-KEY LIEGT!

wenn dannn noch WARNING UNPROTECTED PRIVATE KEY FILE Meldung kommt, einfach

chown 600 /config/ssh_keys/id_admin_ed25519

danach sollte

ssh storagebox

funktionieren und damit auch bei Repository URI:

sftp:storagebox:MEIN-PROJEKT/restic-backup

image.png

restic unlock

https://github.com/restic/restic/issues/1450 

Falls es zu folgender Fehlermeldung bei einem Backup kommt, kann man das Repository mit restic unlock wieder entsperren:

unable to create lock in backend: repository is already locked by PID 3604 on HOSTNAME by HOSTNAME\User (UID 0, GID 0)
lock was created at 2025-08-17 22:58:46 (725h49m33.96593014s ago)
storage ID 42...bce
the `unlock` command can be used to remove stale locks

Lösung:

In die Shell des Backrest Docker-Container gehen und folgende Befehle eingeben:

restic -r /path/to/repository/external/drive unlock
# release previous lock
restic -r /path/to/repository/external/drive check
# Return some "not referenced in any index"
restic -r /path/to/repository/external/drive rebuild-index
# rebuild index 
restic -r /path/to/repository/external/drive check
# return a few  "not referenced in any index"
restic -r /path/to/repository/external/drive prune
# remove those few incomplete packs
restic -r /path/to/repository/external/drive check
# no errors were found

Wobei aus dem Hetzner-Storagebox SFTP Beispiel oben als Repository mit angegebenem SSH-Key folgende Zeile resultiert:

restic -r sftp:u12345@u12345.your-storagebox.de:ResticBackup/DockerServer -o sftp.args="-i /config/ssh_keys/i
d_ed25519 -p 23 -o UserKnownHostsFile=/config/ssh_keys/known_hosts" check

Danach das repository-Passwort eingeben:

image.png

Backup wiederherstellen (Restic CLI)

In die Shell des Containers backrest gehen


# mit hz-03 verbinden
ssh hz-03

# mit lazydocker in den container backrest gehen:
lazydocker
# mit J/K hoch und runter zum Container "backrest" navigieren und Shift+E drücken für exec in shell:

image.png

lazydocker > exec shell in Container "backrest"

config.json des repositories anzeigen

# config des restic repositories anzeigen:
cat config/config.json

image.png

wichtig sind uri, pw und flags für sftp des repos

restic cmd vorbereiten

# dann folgende ENVs im Container festlegen (aus config.json kopiert) mit
export RESTIC_REPOSITORY="sftp:u123456@..................de:MEINEPRAXIS/ResticBackup"
export RESTIC_PASSWORD="t1Lp0iF......................KnTqEQag"

# und zuletzt einen alias für restic setzen mit
alias restic='restic -o sftp.args="-i /config/ssh_keys/id_pl-admin_ed25519 -p 23 -o UserKnownHostsFile=/config/ssh_keys/known_hosts"'

restic Befehle anwenden und Dateien / Pfade suchen

# snapshots anzeigen mit repo-name und plan-name
restic snapshots
# oder kompakt
restic snapshots --compact
# oder nur bestimmter host
restic snapshots --host meinserver

# nach PDF-Dateien in bestimmtem Ordner suchen:
restic find --tag "plan:daily-docker-nextcloud" "*/PatientInnen MAX MUSTERMANN/MUSTERFRAU, MAXI Gruppe ACT W1263456/Dokumentation Therapie/*.pdf"
# alle dateien in bestimmtem Ordner:
restic find --tag "plan:daily-docker-nextcloud" "/backup/docker-praxis-volume-03/nextcloud_aio_nextcloud_data/_data/admin/files/PLoud-Ordner/2_Praxismgmt/PatientInnen___alle_/PatientInnen MAX MUSTERMANN/MUSTERFRAU, MAXI Gruppe ACT W1263456/Dokumentation Therapie/*.*"

Je genauer der Pfad, desto schneller die Suche

Beispiel für langsame Suche

restic find --tag "plan:daily-docker-nextcloud" "*(W040499)/Dokumentation Therapie/*.pdf"

= dauert ca 1min pro Snapshot!

image.png

Beispiel für schnelle Suche

restic find --tag "plan:daily-docker-nextcloud" "/backup/docker-praxis-volume-03/nextcloud_aio_nextcloud_data/_data/admin/files/PLoud-Ordner/2_Praxismanagement/PatientInnen________alle_/PatientInnen Behandler-Vorname\ Behandler-Nachname/Pat-Nachname,\ Pat-Vorname\ \(W
040499\)/Dokumentation\ Therapie/*.*"

= innerhalb von 20sek alle Snapshots durchsucht

image.png

Dateien wiederherstellen

Wiederherstellen entweder über GUI direkt zum richtigen Snapshot navigieren und herunterladen
(siehe https://wiki.praxisluebberding.de/doc/backup-wiederherstellen-backrest-gui-w04w1wkGAJ )
oder im CLI:

restic restore b676c5c4 --target "/backrest-restored/mein-wiederhergesteller-ordner" --include "/backup/docker-praxis-volume-03/nextcloud_aio_nextcloud_data/_data/admin/files/.../Dokumentation Therapie"
# danach im ordner /mnt/backrest-restored nachsehen und auf Dateien zugreifen (evtl über mountain duck > sftp hz-03)

Achtung, immer wenn über die Web-UI (GUI) wiederhegestellt wird, landen die restores auf hz-01 im Ordner /mnt/backrest-restored, weil dort der backrest container für https://backrest.praxisluebberding.de/ läuft.

Wenn man jedoch wie hier über die CLI wiederherstellt, gibt man einen lokalen Ordner an, zb auf hz-03 /mnt/backrest-restored

 

Backup wiederherstellen (Backrest GUI)

1) https://backrest.praxisluebberding.de öffnen

2) Backrest User & Passwort eingeben (in Bitwarden hinterlegt: https://bw.praxisluebberding.de)

3) Den Backup-Plan ‘daily_docker_backup‘ auswählen

image.png

4) letztes erfolgreichen Snapshot auswählen (grünes Icon)

5) Rechts den Snapshot Browser ausklappen

6) Den gewünschten Ordner oder Datei suchen

image.png

7) Restore to Path

image.png

8) restore-pfad angeben:

/backrest-restored/NAME

zb /backrest-restored/2024-12-31_IT-HowTo-Restore

image.png

9) Zurück zur Übersicht, den Restore auswählen und auch Download Files drücken:

image.png

10) .tar.gz Datei herunterladen und entpacken

Docker-Compose .yamls

min.io (minio, S3 compatible Storage)

PL

version: '3.7'

services:
  minio:
    hostname: minio
    image: quay.io/minio/minio:latest
    command: server /data --console-address ":9001"
    ports:
      - "9000:9000"
      - "9001:9001"
    volumes:
      - data:/data
    environment:
      MINIO_ROOT_USER: minio-admin
      MINIO_ROOT_PASSWORD: Unmo..........tude0
    healthcheck:
      test: ["CMD", "mc", "ready", "local"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  data:

das oben ist eine gekürzte Variante hiervon:

https://github.com/minio/minio/blob/master/docs/orchestration/docker-compose/docker-compose.yaml

also von

version: '3.7'

# Settings and configurations that are common for all containers
x-minio-common: &minio-common
  image: quay.io/minio/minio:latest
  command: server --console-address ":9001" http://minio{1...2}/data{1...2}
  expose:
    - "9000"
    - "9001"
  # environment:
    # MINIO_ROOT_USER: minioadmin
    # MINIO_ROOT_PASSWORD: minioadmin
  healthcheck:
    test: ["CMD", "mc", "ready", "local"]
    interval: 5s
    timeout: 5s
    retries: 5

# starts 2 docker containers running minio server instances.
# using nginx reverse proxy, load balancing, you can access
# it through port 9000.
services:
  minio1:
    <<: *minio-common
    hostname: minio1
    volumes:
      - data1-1:/data1
      - data1-2:/data2

  minio2:
    <<: *minio-common
    hostname: minio2
    volumes:
      - data2-1:/data1
      - data2-2:/data2

#  nginx:
#    image: nginx:1.19.2-alpine
#    hostname: nginx
#    volumes:
#      - ./nginx.conf:/etc/nginx/nginx.conf:ro
#    ports:
#      - "9000:9000"
#      - "9001:9001"
#    depends_on:
#      - minio1
#      - minio2

## By default this config uses default local driver,
## For custom volumes replace with volume driver configuration.
volumes:
  data1-1:
  data1-2:
  data2-1:
  data2-2:

ALT:

https://min.io/docs/minio/container/index.html

https://hub.docker.com/r/bitnami/minio

version: '3.8'
  
services:
  minio:
    restart: unless-stopped
    image: 'bitnami/minio:latest'
    ports:
      - '9000:9000'
      - '9001:9001'
    environment:
      - MINIO_ROOT_USER=minio-admin
      - MINIO_ROOT_PASSWORD=MEIN...PASSWORT
    networks:
      - praxistool-nw
    volumes:
      - buckets:/bitnami/minio/data
      - data:/data
      - certs:/certs
#  myapp:
#    image: 'YOUR_APPLICATION_IMAGE'
#    networks:
#      - app-tier
#    environment:
#      - MINIO_SERVER_ACCESS_KEY=minio-access-key
#      - MINIO_SERVER_SECRET_KEY=minio-secret-key

volumes:
  data:
  certs:
  buckets:

networks:
  praxistool-nw:
    driver: bridge

oder eine Variante, eine Hetzner Storage Box einzubinden:
DIE FOLGENDE IST EINE ALTE VARIANTE! BESSER NICHT CIFS DIREKT IM CONTAINER MOUNTEN, SONDERN MIT FSTAB IM DOCKER HOST, SIEHE
https://wiki.folkerts.it/books/docker/page/hetzner-storage-box-uber-samba-einbinden 

version: '3.8'
  
services:
  minio:
    restart: unless-stopped
    image: 'bitnami/minio:latest'
    ports:
      - '9000:9000'
      - '9001:9001'
    environment:
      - MINIO_ROOT_USER=admin
      - MINIO_ROOT_PASSWORD=Unude0
    networks:
      - praxistool-nw
    volumes:
      - hetzner_storagebox_minio:/bitnami/minio/data
      - data:/data
      - certs:/certs


volumes:
  data:
  certs:
  hetzner_storagebox_minio:
    driver: local
    driver_opts:
      type: cifs
      o: "username=u4b1,password=zpp,uid=1001,gid=1001,file_mode=0777,dir_mode=0777,vers=3.1.1,seal"
      device: "//u4b1.your-storagebox.de/u4b1/dockervolume_minio"

networks:
  praxistool-nw:
    driver: bridge

Docker-Compose .yamls

authentik

image.png

https://docs.goauthentik.io/docs/installation/docker-compose

https://www.youtube.com/watch?v=N5unsATNpJk

docker-compose.yml 

wichtig:
- bei server einen hostnamen hinzufügen
- wenn möglich port 9000 belassen
- gleiches network wie der Nginx Proxy Manager Container nutzen (damit npm über den hostname auf den authentik server zugreifen kann, siehe 500 Internal Server Error weiter unten)

---

services:
  postgresql:
    image: docker.io/library/postgres:16-alpine
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
      start_period: 20s
      interval: 30s
      retries: 5
      timeout: 5s
    networks:
      - reverseproxy_network
    volumes:
      - database:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: ${PG_PASS}
      POSTGRES_USER: ${PG_USER:-authentik}
      POSTGRES_DB: ${PG_DB:-authentik}
    env_file:
      - stack.env
  redis:
    image: docker.io/library/redis:alpine
    command: --save 60 1 --loglevel warning
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
      start_period: 20s
      interval: 30s
      retries: 5
      timeout: 3s
    networks:
      - reverseproxy_network
    volumes:
      - redis:/data
  server:
    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.0}
    restart: unless-stopped
    hostname: authentik_server
    command: server
    ulimits:
      nofile:
        soft: 10240
        hard: 10240
    environment:
      AUTHENTIK_REDIS__HOST: redis
      AUTHENTIK_POSTGRESQL__HOST: postgresql
      AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
      AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
      AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
    networks:
      - reverseproxy_network
    volumes:
      - media:/media
      - custom-templates:/templates#    
    env_file:
      - stack.env
    ports:
      - "${COMPOSE_PORT_HTTP:-9000}:9000"
      - "${COMPOSE_PORT_HTTPS:-9443}:9443"
    depends_on:
      - postgresql
      - redis
  worker:
    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.0}
    restart: unless-stopped
    command: worker
    ulimits:
      nofile:
        soft: 10240
        hard: 10240
    environment:
      AUTHENTIK_REDIS__HOST: redis
      AUTHENTIK_POSTGRESQL__HOST: postgresql
      AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
      AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
      AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
    # `user: root` and the docker socket volume are optional.
    # See more for the docker socket integration here:
    # https://goauthentik.io/docs/outposts/integrations/docker
    # Removing `user: root` also prevents the worker from fixing the permissions
    # on the mounted folders, so when removing this make sure the folders have the correct UID/GID
    # (1000:1000 by default)
    user: root
    networks:
      - reverseproxy_network
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - media:/media
      - certs:/certs
      - custom-templates:/templates
    env_file:
      - stack.env
    depends_on:
      - postgresql
      - redis

volumes:
  database:
    driver: local
  redis:
    driver: local
  media:
  custom-templates:
  certs:

networks:
  reverseproxy_network:
      name: reverseproxy_network
      driver: bridge

PG_PASS

openssl rand 36 | base64

AUTHENTIK_SECRET_KEY

openssl rand 60 | base64

Portainer env via advanced mode

PG_PASS=2jvp8Jmnf8cS
AUTHENTIK_SECRET_KEY="hxHHNgxf5PALhfX0FL69v"
AUTHENTIK_ERROR_REPORTING__ENABLED=true
AUTHENTIK_EMAIL__HOST=smtp.world4you.com
AUTHENTIK_EMAIL__PORT=587
AUTHENTIK_EMAIL__USERNAME=MEINE....@MY-MAIL.DE
AUTHENTIK_EMAIL__PASSWORD=MEIN.....MAIL-PW
AUTHENTIK_EMAIL__FROM=NO-REPLY@MY-MAIL.DE
AUTHENTIK_EMAIL__USE_TLS=true
COMPOSE_PORT_HTTP=9006
COMPOSE_PORT_HTTPS=9446

image.png

Danach noch das Initial-Setup durchführen, um ein Passwort zu setzen:

http://MEIN-HOSTNAME-ODER-IP:9000/if/flow/initial-setup

oder mit meiner cfg

http://MEIN-HOSTNAME-ODER-IP:9006/if/flow/initial-setup

Zusatzinfo
die Zeilen
    ulimits:
      nofile:
        soft: 10240
        hard: 10240
in der docker-compose.yml habe ich eingefügt, weil ein Bug dazu führt, dass die CPU-Leistung des Docker Hosts auf 100% klettert und einige Stunden braucht, bis sie sich wieder erholt. Mehr Infos unter https://github.com/goauthentik/authentik/pull/7762 und
https://github.com/goauthentik/authentik/issues/7025, die Lösung stammt aus diesem Kommentar:
https://github.com/goauthentik/authentik/issues/7025#issuecomment-1868333903 

Nextcloud Integration

benötigte Nextcloud App: https://apps.nextcloud.com/apps/user_oidc

Anleitung: https://docs.goauthentik.io/integrations/services/nextcloud/ 

Zusatz:

Befehl, um nur Anmeldung über Authentik zuzulassen:

sudo -u www-data php var/www/nextcloud/occ config:app:set --value=0 user_oidc allow_multiple_user_backends

bzw wenn man diesem Artikel folgt

docker exec -it -u 33 nextcloud-app-1 /bin/bash

und dann 

./occ config:app:set --value=0 user_oidc allow_multiple_user_backends

image.png

Wenn man sich dann trotzdem über den Direktlogin zb als Super-Admin einloggen möchte, geht das über 

http://nextcloud.MEINEDOMAIN.de/login?direct=1 

Branding (eigene Marke)

IMG-UPLOAD NEU SEIT 2025.4, dieser Upload ist obsolet! In neueren Versionen einfach unter Authentik Admin-Console > Anpassung > Files hochladen, bzw. unter docker volume authentik_media und dann relativen Pfad in Branding angeben:

uploadpfad auf docker zb
/var/lib/docker/volumes/authentik_media/_data/public/Eikes-Upload/Wallpaper

image.png

Pfad dann relativ unter Branding:

image.png

Ab hier alte Anleitung

# default bg und icon:
/static/dist/assets/icons/icon_left_brand.svg
/static/dist/assets/icons/icon.png

https://www.youtube.com/watch?v=YawgyM509ng

https://www.youtube.com/watch?v=3oIRY0NWPr8 

Eigene Backgrounds und Icons

1) media-volume muss in der docker-compose.yml bei server, worker und als volume vorhanden sein:

...
  server:
    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.1}
...
    volumes:
      - media:/media # <------------ HIER
      - custom-templates:/templates#    
    env_file:
  worker:
    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.0}
...
    user: root
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - media:/media # <------------ HIER
      - certs:/certs
      - custom-templates:/templates
    env_file:
...
volumes:
  database:
    driver: local
  redis:
    driver: local
  media: # <------------ HIER
  certs:
  custom-templates:

2) Bilddateien übertragen (mit WinSCP, CLI SCP oder MountainDuck, mir egal)

image.png

3) Authentik > System > Brands > edit default (Stiftsymbol) > Branding-Settings > Pfade eingeben:

image.png

Custom Icons von walkxcode

Quelle: https://www.archy.net/cyberpunk-authentik-a-neon-glassmorphism-theme/ 

App Icons via CDN:

Since Font Awesome doesn't play, grab SVGs from the awesome dashboard-icons project. Set them in Admin > Applications > Edit > Icon URL:

https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/grafana.svg
https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/gitlab.svg
https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/proxmox.svg
https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/portainer.svg

Browse all icons at github.com/walkxcode/dashboard-icons.

Custom CSS Theme "neon"

Quellen:
https://www.archy.net/cyberpunk-authentik-a-neon-glassmorphism-theme/
https://gitlab.raidho.fr/Stephane/authentik-neon 

custom.css

@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap');
body { font-family: 'Inter', -apple-system, system-ui, sans-serif !important; }

.pf-c-page, .pf-v6-c-page {
    background-image: url('YOUR_BACKGROUND_URL') !important;
    background-size: cover !important;
    background-position: center !important;
    background-attachment: fixed !important;
}
.pf-c-page__main-section, .pf-v6-c-page__main-section { background: transparent !important; }

.ak-library-application {
    background: linear-gradient(145deg, rgba(25,18,55,0.7), rgba(12,10,28,0.85)) !important;
    backdrop-filter: blur(16px) !important;
    border: 1px solid rgba(120,80,255,0.12) !important;
    border-radius: 20px !important;
    transition: all 0.4s cubic-bezier(0.4,0,0.2,1) !important;
    box-shadow: 0 4px 24px rgba(0,0,0,0.4) !important;
    position: relative !important;
}
.ak-library-application::before {
    content: ''; position: absolute; top:0; left:20%; right:20%; height:1px;
    background: linear-gradient(90deg, transparent, #a080ff, transparent);
    opacity: 0.4; transition: all 0.4s ease;
}
.ak-library-application:hover {
    border-color: rgba(120,80,255,0.4) !important;
    box-shadow: 0 12px 50px rgba(120,80,255,0.12) !important;
    transform: translateY(-6px) scale(1.02) !important;
}
.ak-library-application:hover::before { left:10%; right:10%; opacity:0.8; }

.pf-c-button.pf-m-primary, .pf-v6-c-button.pf-m-primary {
    background: linear-gradient(135deg, #7850ff, #5030cc) !important;
    border: none !important; border-radius: 10px !important;
    box-shadow: 0 4px 18px rgba(120,80,255,0.35) !important;
}

.pf-c-form-control, .pf-v6-c-form-control {
    background: rgba(15,12,35,0.6) !important;
    border: 1px solid rgba(120,80,255,0.15) !important;
    border-radius: 10px !important; color: #e8e2ff !important;
}
.pf-c-form-control:focus, .pf-v6-c-form-control:focus {
    border-color: #7850ff !important;
    box-shadow: 0 0 15px rgba(120,80,255,0.15) !important;
}

.pf-c-modal-box, .pf-v6-c-modal-box {
    background: rgba(15,12,30,0.95) !important;
    backdrop-filter: blur(30px) !important; border-radius: 20px !important;
}
.pf-c-backdrop, .pf-v6-c-backdrop {
    background: rgba(0,0,0,0.6) !important; backdrop-filter: blur(4px) !important;
}

::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-thumb { background: rgba(120,80,255,0.25); border-radius: 10px; }
::selection { background: rgba(120,80,255,0.3); color: white; }

.ak-flow-body .pf-c-login__main, .ak-flow-body .pf-v6-c-login__main {
    background: rgba(12,10,25,0.75) !important;
    backdrop-filter: blur(20px) !important; border-radius: 24px !important;
}


Custom CSS Theme "Glassmorphism"

Seit Version 2025.12 kann man die custom.css über das Admin-Panel editieren:

Admin-Oberfläche > System > Brands > Brand editieren > Branding-Einstellungen > Custom CSS:

image.png

Neues glassmorphism Theme:

Quelle https://github.com/VULGA01/Authentik-Login-theme-Glassmorphism
mit bugfixes aus issue " broken in 2025.12.0": https://github.com/VULGA01/Authentik-Login-theme-Glassmorphism/issues/5  

wichtiger Link, um Images direkt als URL (quasi CDN) zur Verfügung zu stellen:
https://wiki.folkerts.it/books/docker/page/nginx-proxy-manager-npm 

custom.css:

/* ===== AUTHENTIK GLASSMORPHISM THEME - V1.1 (Badge Fix) ===== */

/* 
   This theme implements a modern glassmorphism look for Authentik.
   It overrides standard PatternFly and Authentik components.
   Please upload this file in the Authentik Admin Interface -> Customization -> Blueprints/Files.
*/

/* ===== EASY CUSTOMIZATION ===== */
/* Change the background image URL below */
:root {
    --ak-flow-background: url("https://cdn.MEINEDOMAIN.de/images/dreamy-aesthetic-color-year-tones-nature-landscape4_dark_blur.png");
    --ak-social-separator-text: "Continuer avec";
    --ak-accent: #d0ced0;
}


/* ===== TOTP COPY BUTTON STYLE (NEUTRAL WHITE) ===== */
:host(ak-stage-authenticator-totp) .pf-c-button.pf-m-secondary {
    background: rgba(255, 255, 255, 0.1) !important;
    color: white !important;
    border: 1px solid rgba(255, 255, 255, 0.2) !important;
    backdrop-filter: blur(10px) !important;
    border-radius: 12px !important;
    padding: 8px 16px !important;
    display: flex !important;
    align-items: center !important;
    justify-content: center !important;
    gap: 8px !important;
    font-size: 14px !important;
    font-weight: 500 !important;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1) !important;
    transition: all 0.3s ease !important;
    margin-top: 5px !important;
}

:host(ak-stage-authenticator-totp) .pf-c-button.pf-m-secondary:hover {
    background: rgba(255, 255, 255, 0.2) !important;
    border-color: rgba(255, 255, 255, 0.4) !important;
    transform: translateY(-2px) !important;
    box-shadow: 0 6px 15px rgba(0, 0, 0, 0.15) !important;
}

/* Remove reflection for this specific button to avoid glitches */
:host(ak-stage-authenticator-totp) .pf-c-button.pf-m-secondary::before,
:host(ak-stage-authenticator-totp) .pf-c-button.pf-m-secondary::after {
    display: none !important;
    content: none !important;
}

/* Fix icon positioning (it was absolute, causing overlap) */
:host(ak-stage-authenticator-totp) .pf-c-button.pf-m-secondary .pf-c-button__progress {
    position: static !important;
    transform: none !important;
    display: inline-flex !important;
    align-items: center !important;
}

:host(ak-stage-authenticator-totp) .qr-container {
    width: 100% !important;
    height: auto !important;
    max-width: none !important;
    background: transparent !important;
    box-shadow: none !important;
    display: flex !important;
    flex-direction: column !important;
    align-items: center !important;
    justify-content: center !important;
    margin: 10px 0 !important;
    border-radius: 0 !important;
}

:host(ak-stage-authenticator-totp) qr-code {
    /* Only the QR code gets the fixed white box */
    max-width: 150px !important;
    width: 150px !important;
    height: 150px !important;
    margin: 0 0 15px 0 !important;
    /* Bottom margin to push button down */
    display: flex !important;
    justify-content: center !important;
    align-items: center !important;
    border-radius: 12px !important;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2) !important;
    background: white !important;
    overflow: hidden !important;
}

:host(ak-stage-authenticator-totp) qr-code svg {
    width: 100% !important;
    height: 100% !important;
    object-fit: contain !important;
}

/* Réduire la taille du texte explicatif sur la page TOTP */
:host(ak-stage-authenticator-totp) .pf-c-form__group:not([hidden]) p,
:host(ak-stage-authenticator-totp) .pf-c-form__group:not([hidden]) li {
    font-size: 13px !important;
    line-height: 1.4 !important;
    opacity: 0.9 !important;
    margin-bottom: 8px !important;
}

/* Réduire un peu le titre du QR code container si présent */
:host(ak-stage-authenticator-totp) .pf-c-form__group label {
    font-size: 14px !important;
    margin-bottom: 4px !important;
}

/* ===== FORCE DARK MODE ===== */

:root {
    --pf-global--palette--black-50: oklab(0.9067 -0 0) !important;
    --pf-global--palette--black-100: oklab(0.8292 -0.0007 -0.0016) !important;
    --pf-global--palette--black-200: oklab(0.7407 -0.0007 -0.0017) !important;
    --pf-global--palette--black-300: oklab(0.6232 -0.0003 -0.0032) !important;
    --pf-global--palette--black-400: oklab(0.4602 -0.0003 -0.0034) !important;
    --pf-global--palette--black-500: oklab(0.3906 0.0001 -0.0052) !important;
    --pf-global--palette--black-600: oklab(0.3369 0.0001 -0.0054) !important;
    --pf-global--palette--black-700: oklab(0.2795 -0.0021 -0.0083) !important;
    --pf-global--palette--black-800: oklab(0.2303 -0.0008 -0.0083) !important;
    --pf-global--palette--black-900: oklab(0.1798 -0.0034 -0.0053) !important;
    --pf-global--palette--red-9999: oklab(0.6739 0.1853 0.1022) !important;
    --pf-global--palette--red-8888: oklab(0.723 0.153 0.0784) !important;
    --pf-v4-global--palette--blue-300: oklab(70.367% -0.07498 -0.139) !important;
    --ak-global--palette--blue-300: oklab(67.552% -0.05374 -0.16817) !important;
    --pf-global--palette--blue-300: var(--pf-v4-global--palette--blue-300) !important;
    --pf-global--BackgroundColor--100: oklab(0.2303 -0.0008 -0.0083) !important;
    --pf-global--BackgroundColor--150: oklab(0.2585 -0.0027 -0.0066) !important;
    --pf-global--BackgroundColor--200: oklab(0.1798 -0.0034 -0.0053) !important;
    --pf-global--BackgroundColor--300: oklab(0.2795 -0.0021 -0.0083) !important;
    --pf-global--BackgroundColor--400: oklab(0.3369 0.0001 -0.0054) !important;
    --pf-global--BackgroundColor--light-100: oklab(0.2303 -0.0008 -0.0083) !important;
    --pf-global--BackgroundColor--light-200: oklab(0.1798 -0.0034 -0.0053) !important;
    --pf-global--BackgroundColor--light-300: oklab(0.2795 -0.0021 -0.0083) !important;
    --pf-global--BackgroundColor--dark-100: oklab(0.2303 -0.0008 -0.0083) !important;
    --pf-global--BackgroundColor--dark-200: oklab(0.1798 -0.0034 -0.0053) !important;
    --pf-global--BackgroundColor--dark-300: oklab(0.2795 -0.0021 -0.0083) !important;
    --pf-global--BackgroundColor--dark-400: oklab(0.3369 0.0001 -0.0054) !important;
    --pf-global--BorderColor--100: oklab(0.3906 0.0001 -0.0052) !important;
    --pf-global--BorderColor--200: oklab(0.4602 -0.0003 -0.0034) !important;
    --pf-global--BorderColor--300: oklab(0.3906 0.0001 -0.0052) !important;
    --pf-global--BorderColor--400: oklab(0.7407 -0.0007 -0.0017) !important;
    --pf-global--Color--100: oklab(0.9067 -0 0) !important;
    --pf-global--Color--200: oklab(0.7407 -0.0007 -0.0017) !important;
    --pf-global--active-color--100: var(--pf-v4-global--palette--blue-300) !important;
    --pf-global--primary-color--300: oklab(0.522 -0.0434 -0.1717) !important;
    --pf-global--primary-color--100: var(--pf-global--primary-color--300) !important;
    --pf-global--link--Color: var(--pf-v4-global--palette--blue-300) !important;
    --pf-global--link--Color--hover: oklab(0.7706 -0.0485 -0.1012) !important;
    --pf-global--link--Color--visited: oklab(0.7137 0.0522 -0.151) !important;
    --pf-global--disabled-color--100: oklab(0.4602 -0.0003 -0.0034) !important;
    --pf-global--disabled-color--200: oklab(0.3906 0.0001 -0.0052) !important;
    --pf-global--disabled-color--300: oklab(0.7407 -0.0007 -0.0017) !important;
    --pf-global--icon--Color--light: oklab(0.7407 -0.0007 -0.0017) !important;
    --pf-global--icon--Color--dark: oklab(0.7407 -0.0007 -0.0017) !important;
    --pf-global--Color--dark-100: oklab(0.9067 -0 0) !important;
    --pf-global--Color--dark-200: oklab(0.7407 -0.0007 -0.0017) !important;
    --pf-global--Color--light-100: oklab(0.9067 -0 0) !important;
    --pf-global--Color--light-200: oklab(0.7407 -0.0007 -0.0017) !important;
    --pf-global--Color--light-300: oklab(0.3659 -0.0025 -0.0061) !important;
    --pf-global--BorderColor--dark-100: oklab(0.3906 0.0001 -0.0052) !important;
    --pf-global--BorderColor--light-100: oklab(0.3906 0.0001 -0.0052) !important;
    --pf-global--primary-color--light-100: oklab(67.552% -0.05374 -0.16817) !important;
    --pf-global--primary-color--dark-100: oklab(67.552% -0.05374 -0.16817) !important;
    --pf-global--link--Color--light: var(--pf-v4-global--palette--blue-300) !important;
    --pf-global--link--Color--light--hover: var(--pf-global--palette--blue-200) !important;
    --pf-global--link--Color--dark: var(--pf-v4-global--palette--blue-300) !important;
    --pf-global--link--Color--dark--hover: var(--pf-global--palette--blue-200) !important;
    --pf-global--default-color--100: var(--pf-global--palette--cyan-100) !important;
    --pf-global--default-color--300: var(--pf-global--palette--cyan-200) !important;
    --pf-global--info-color--100: var(--pf-global--palette--blue-200) !important;
    --pf-global--info-color--200: var(--pf-global--palette--blue-50) !important;
    --pf-global--warning-color--100: oklab(0.7864 0.0298 0.1604) !important;
    --pf-global--warning-color--200: oklab(0.8354 0.0112 0.1471) !important;
    --pf-global--danger-color--100: var(--pf-global--palette--red-100) !important;
    --pf-global--danger-color--200: var(--pf-global--palette--red-50) !important;
    --pf-global--success-color--100: oklab(0.6488 -0.1066 0.0846) !important;
    --pf-global--success-color--200: var(--pf-global--palette--green-50) !important;

    /* Force browser to render standard controls in dark mode */
    color-scheme: dark !important;
}

/* Ensure these variables are sticky even if the browser prefers light mode */
@media (prefers-color-scheme: light) {
    :root {
        --pf-global--BackgroundColor--100: oklab(0.2303 -0.0008 -0.0083) !important;
        --pf-global--BackgroundColor--light-100: oklab(0.2303 -0.0008 -0.0083) !important;
        --pf-global--Color--100: oklab(0.9067 -0 0) !important;
        --pf-global--Color--light-100: oklab(0.9067 -0 0) !important;
        --pf-global--Color--dark-100: oklab(0.9067 -0 0) !important;
        /* Re-apply the full block if necessary, but usually overrides are enough */
    }
}

/* ===== BOUTONS OUI/NON & ACTIONS ===== */

/* Container buttons */
.pf-c-login .pf-c-form__actions,
.pf-c-login .pf-c-form__group-control,
body:has(.pf-c-login) .pf-c-form__actions,
body:has(.pf-c-login) .pf-c-form__group-control {
    display: flex !important;
    gap: 15px !important;
    margin-top: 25px !important;
    justify-content: space-between !important;
    overflow: visible !important;
    padding: 5px !important;
}

/* Base button styles */
.pf-c-login .pf-c-form__actions button,
.pf-c-login .pf-c-form__group-control button,
body:has(.pf-c-login) .pf-c-form__actions button,
body:has(.pf-c-login) .pf-c-form__group-control button,
.pf-c-login button[type="button"],
body:has(.pf-c-login) button[type="button"],
:host(ak-stage-user-login) button[type="submit"] {
    border-radius: 14px !important;
    padding: 10px 20px !important;
    font-weight: 600 !important;
    font-size: 15px !important;
    transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94) !important;
    text-transform: none !important;
    border: none !important;
    backdrop-filter: blur(20px) saturate(130%) !important;
    -webkit-backdrop-filter: blur(20px) saturate(130%) !important;
    position: relative !important;
    overflow: hidden !important;
    box-sizing: border-box !important;
    min-width: 120px !important;
    cursor: pointer !important;
    outline: none !important;
}

/* ========================================= */
/* LOGIN BUTTON - FORCE WHITE GLASSMORPHISM  */
/* ========================================= */

/* Force white for identification login button */
.pf-c-login .pf-c-form__actions button:first-child,
.pf-c-login .pf-c-form__group-control button:first-child,
body:has(.pf-c-login) .pf-c-form__actions button:first-child,
body:has(.pf-c-login) .pf-c-form__group-control button:first-child,
.pf-c-login button[type="button"]:first-of-type,
body:has(.pf-c-login) button[type="button"]:first-of-type,
:host(ak-stage-identification) .pf-c-button.pf-m-primary,
:host(ak-stage-identification) button[type="submit"].pf-c-button.pf-m-primary {
    background: rgba(255, 255, 255, 0.15) !important;
    backdrop-filter: blur(10px) !important;
    -webkit-backdrop-filter: blur(10px) !important;
    border: 1px solid rgba(255, 255, 255, 0.3) !important;
    color: white !important;
    font-weight: 600 !important;
    padding: 14px 24px !important;
    border-radius: 14px !important;
    transition: all 0.3s ease !important;
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15), 0 2px 8px rgba(255, 255, 255, 0.1) !important;
}

:host(ak-stage-identification) .pf-c-button.pf-m-primary:hover,
:host(ak-stage-identification) button[type="submit"].pf-c-button.pf-m-primary:hover {
    background: rgba(255, 255, 255, 0.25) !important;
    border-color: rgba(255, 255, 255, 0.4) !important;
    transform: translateY(-2px) !important;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2), 0 4px 12px rgba(255, 255, 255, 0.15) !important;
}

/* ========================================= */
/* STAY CONNECTED BUTTONS (USER LOGIN STAGE) */
/* ========================================= */

/* Neutralize 'Yes' and 'No' buttons to Standard Glass Style */
:host(ak-stage-user-login) .pf-c-button.pf-m-primary,
:host(ak-stage-user-login) .pf-c-button.pf-m-secondary {
    background: rgba(255, 255, 255, 0.08) !important;
    backdrop-filter: blur(10px) !important;
    -webkit-backdrop-filter: blur(10px) !important;
    border: 1px solid rgba(255, 255, 255, 0.2) !important;
    color: white !important;
    font-weight: 500 !important;
    padding: 10px 24px !important;
    border-radius: 14px !important;
    transition: all 0.3s ease !important;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1) !important;
    margin: 0 !important;
    /* Reset margins if inherited */
}

:host(ak-stage-user-login) .pf-c-button.pf-m-primary:hover,
:host(ak-stage-user-login) .pf-c-button.pf-m-secondary:hover {
    background: rgba(255, 255, 255, 0.2) !important;
    transform: translateY(-2px) !important;
    box-shadow: 0 8px 15px rgba(0, 0, 0, 0.2) !important;
    border-color: rgba(255, 255, 255, 0.4) !important;
}

/* Remove colored pseudo-elements/reflections for Stay Connected */
:host(ak-stage-user-login) .pf-c-button.pf-m-primary::before,
:host(ak-stage-user-login) .pf-c-button.pf-m-secondary::before {
    display: none !important;
    content: none !important;
}

/* ========================================= */
/* SHARED ANIMATIONS & FIXES                 */
/* ========================================= */

/* Reflection effect for Login Button ONLY */
:host(ak-stage-identification) .pf-c-button.pf-m-primary::before {
    content: '';
    position: absolute;
    top: 0;
    left: -100%;
    width: 100%;
    height: 100%;
    background: linear-gradient(90deg,
            transparent 0%,
            rgba(255, 255, 255, 0.2) 50%,
            transparent 100%);
    transition: left 0.6s ease;
    z-index: 1;
    border-radius: 14px;
}

:host(ak-stage-identification) .pf-c-button.pf-m-primary:hover::before {
    left: 100%;
}

/* Z-index fix for text */
:host(ak-stage-user-login) .pf-c-button.pf-m-primary *,
:host(ak-stage-user-login) .pf-c-button.pf-m-secondary *,
:host(ak-stage-identification) .pf-c-button.pf-m-primary *,
:host(ak-stage-identification) .pf-c-button.pf-m-secondary * {
    position: relative !important;
    z-index: 2 !important;
}



:host(ak-stage-user-login) .pf-c-button.pf-m-primary::after,
:host(ak-stage-user-login) .pf-c-button.pf-m-secondary::after,
:host(ak-stage-identification) .pf-c-button.pf-m-primary::after,
:host(ak-stage-identification) .pf-c-button.pf-m-secondary::after {
    border: none !important;
    display: none !important;
}

:host(ak-stage-user-login) .pf-c-button:focus,
:host(ak-stage-user-login) .pf-c-button:active,
:host(ak-stage-identification) .pf-c-button:focus,
:host(ak-stage-identification) .pf-c-button:active {
    outline: none !important;
}

/* Button 'No' / Secondary */
:host(ak-stage-user-login) .pf-c-button.pf-m-secondary,
:host(ak-stage-identification) .pf-c-button.pf-m-secondary {
    background: rgba(220, 53, 69, 0.15) !important;
    color: rgba(255, 255, 255, 0.9) !important;
    border: 1.5px solid rgba(220, 53, 69, 0.3) !important;
    box-shadow:
        0 4px 15px rgba(220, 53, 69, 0.1),
        0 2px 8px rgba(0, 0, 0, 0.1),
        inset 0 1px 0 rgba(255, 255, 255, 0.1) !important;
}

/* Reflection effect 'No' */
:host(ak-stage-user-login) .pf-c-button.pf-m-secondary::before,
:host(ak-stage-identification) .pf-c-button.pf-m-secondary::before {
    content: '';
    position: absolute;
    top: 0;
    left: -100%;
    width: 100%;
    height: 100%;
    background: linear-gradient(90deg,
            transparent 0%,
            rgba(255, 255, 255, 0.15) 50%,
            transparent 100%);
    transition: left 0.6s ease;
    z-index: 1;
    border-radius: 14px;
}

:host(ak-stage-user-login) .pf-c-button.pf-m-secondary:hover::before,
:host(ak-stage-identification) .pf-c-button.pf-m-secondary:hover::before {
    left: 100%;
}

:host(ak-stage-user-login) .pf-c-button.pf-m-secondary:hover,
:host(ak-stage-identification) .pf-c-button.pf-m-secondary:hover {
    background: rgba(220, 53, 69, 0.25) !important;
    border-color: rgba(220, 53, 69, 0.45) !important;
    color: white !important;
    transform: translateY(-1px) !important;
    box-shadow:
        0 8px 25px rgba(220, 53, 69, 0.2),
        0 4px 12px rgba(0, 0, 0, 0.15),
        inset 0 1px 0 rgba(255, 255, 255, 0.15) !important;
}

/* Z-index fix for text */
:host(ak-stage-user-login) .pf-c-button.pf-m-primary *,
:host(ak-stage-user-login) .pf-c-button.pf-m-secondary *,
:host(ak-stage-identification) .pf-c-button.pf-m-primary *,
:host(ak-stage-identification) .pf-c-button.pf-m-secondary * {
    position: relative !important;
    z-index: 2 !important;
}

/* ========================================= */
/* SOCIAL LOGIN BUTTONS (GLASSMORPHISM)      */
/* ========================================= */

/* Base style for all social login buttons */
:host(ak-stage-identification) .source-button {
    background: rgba(255, 255, 255, 0.08) !important;
    border: 1px solid rgba(255, 255, 255, 0.2) !important;
    color: white !important;
    border-radius: 12px !important;
    padding: 10px 20px !important;
    transition: all 0.3s ease !important;
    display: flex !important;
    align-items: center !important;
    justify-content: center !important;
    text-decoration: none !important;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1) !important;
    position: relative !important;
    font-weight: 600 !important;
    font-size: 15px !important;
    gap: 8px !important;
    backdrop-filter: blur(10px) !important;
    -webkit-backdrop-filter: blur(10px) !important;
    overflow: hidden !important;
}

:host(ak-stage-identification) .source-button:hover {
    background: rgba(255, 255, 255, 0.2) !important;
    transform: translateY(-2px) scale(1.02) !important;
    box-shadow: 0 8px 15px rgba(0, 0, 0, 0.2) !important;
    border-color: rgba(255, 255, 255, 0.4) !important;
}

/* Discord-specific branding */
:host(ak-stage-identification) button[name*="discord"].source-button {
    background: rgba(88, 101, 242, 0.25) !important;
    border-color: rgba(88, 101, 242, 0.4) !important;
    box-shadow:
        0 4px 12px rgba(88, 101, 242, 0.2),
        inset 0 1px 0 rgba(255, 255, 255, 0.1) !important;
}

:host(ak-stage-identification) button[name*="discord"].source-button:hover {
    background: rgba(88, 101, 242, 0.35) !important;
    border-color: rgba(88, 101, 242, 0.6) !important;
    box-shadow:
        0 8px 20px rgba(88, 101, 242, 0.35),
        0 0 0 2px rgba(88, 101, 242, 0.2),
        inset 0 1px 0 rgba(255, 255, 255, 0.15) !important;
}

/* Shine effect on hover for all social buttons */
:host(ak-stage-identification) .source-button:hover::before {
    content: "";
    position: absolute;
    top: 0;
    left: -70%;
    width: 55%;
    height: 100%;
    background: linear-gradient(120deg, transparent 0%, rgba(255, 255, 255, 0.5) 60%, transparent 100%);
    transform: skewX(-25deg);
    animation: socialShine 0.65s linear 1;
    pointer-events: none;
}

@keyframes socialShine {
    100% {
        left: 130%;
    }
}

/* Icon styling within social buttons */
:host(ak-stage-identification) .source-button img,
:host(ak-stage-identification) .source-button span.pf-c-button__icon {
    width: 18px !important;
    height: 18px !important;
    flex-shrink: 0;
}

/* Make Discord icon white */
:host(ak-stage-identification) button[name*="discord"].source-button img {
    filter: brightness(0) invert(1);
}

/* Google-specific branding */
:host(ak-stage-identification) button[name*="google"].source-button {
    background: rgba(66, 133, 244, 0.25) !important;
    border-color: rgba(66, 133, 244, 0.4) !important;
}

:host(ak-stage-identification) button[name*="google"].source-button:hover {
    background: rgba(66, 133, 244, 0.35) !important;
    border-color: rgba(66, 133, 244, 0.6) !important;
    box-shadow:
        0 8px 20px rgba(66, 133, 244, 0.35),
        0 0 0 2px rgba(66, 133, 244, 0.2) !important;
}



:host(ak-stage-user-login) .pf-c-button.pf-m-primary::after,
:host(ak-stage-user-login) .pf-c-button.pf-m-secondary::after {
    border: none !important;
    display: none !important;
}

:host(ak-stage-user-login) .pf-c-button:focus,
:host(ak-stage-user-login) .pf-c-button:active {
    outline: none !important;
}

/* Responsive */
@media (max-width: 768px) {

    .pf-c-login .pf-c-form__actions,
    body:has(.pf-c-login) .pf-c-form__actions {
        flex-direction: column !important;
        gap: 12px !important;
    }

    :host(ak-stage-user-login) .pf-c-button.pf-m-primary,
    :host(ak-stage-user-login) .pf-c-button.pf-m-secondary {
        width: 100% !important;
        min-width: auto !important;
        padding: 14px 28px !important;
    }
}

/* ===== RADIO & CHECKBOX (REMEMBER ME) ===== */

.pf-c-login .pf-c-check,
.pf-c-login .pf-c-switch,
body:has(.pf-c-login) .pf-c-check,
body:has(.pf-c-login) .pf-c-switch {
    margin: 20px 0 !important;
    display: flex !important;
    align-items: center !important;
    gap: 6px !important;
}

.pf-c-login .pf-c-check__input,
.pf-c-login .pf-c-switch__input,
.pf-c-login input[type="checkbox"],
body:has(.pf-c-login) .pf-c-check__input,
body:has(.pf-c-login) .pf-c-switch__input,
body:has(.pf-c-login) input[type="checkbox"],
input[id*="authentik-remember-me"],
input[name*="remember"] {
    appearance: none !important;
    -webkit-appearance: none !important;
    width: 16px !important;
    height: 16px !important;
    background: rgba(255, 255, 255, 0.08) !important;
    border: 2px solid rgba(255, 255, 255, 0.3) !important;
    border-radius: 5px !important;
    margin-right: 8px !important;
    position: relative !important;
    vertical-align: middle !important;
    transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94) !important;
    backdrop-filter: blur(20px) !important;
    -webkit-backdrop-filter: blur(20px) !important;
    box-shadow:
        0 4px 15px rgba(0, 0, 0, 0.15),
        inset 0 1px 0 rgba(255, 255, 255, 0.1) !important;
    cursor: pointer !important;
}

.pf-c-login .pf-c-check__input:checked,
.pf-c-login .pf-c-switch__input:checked,
.pf-c-login input[type="checkbox"]:checked,
body:has(.pf-c-login) .pf-c-check__input:checked,
body:has(.pf-c-login) .pf-c-switch__input:checked,
body:has(.pf-c-login) input[type="checkbox"]:checked,
input[id*="authentik-remember-me"]:checked,
input[name*="remember"]:checked {
    background: rgba(102, 126, 234, 0.7) !important;
    border-color: rgba(102, 126, 234, 0.8) !important;
    box-shadow:
        0 0 0 2px rgba(102, 126, 234, 0.2),
        0 6px 20px rgba(102, 126, 234, 0.3),
        inset 0 1px 0 rgba(255, 255, 255, 0.2) !important;
}

.pf-c-login .pf-c-check__input:checked::after,
.pf-c-login .pf-c-switch__input:checked::after,
.pf-c-login input[type="checkbox"]:checked::after,
body:has(.pf-c-login) .pf-c-check__input:checked::after,
body:has(.pf-c-login) .pf-c-switch__input:checked::after,
body:has(.pf-c-login) input[type="checkbox"]:checked::after,
input[id*="authentik-remember-me"]:checked::after,
input[name*="remember"]:checked::after {
    content: '✓' !important;
    position: absolute !important;
    left: 50% !important;
    top: 50% !important;
    transform: translate(-50%, -50%) !important;
    color: white !important;
    font-size: 10px !important;
    font-weight: bold !important;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5) !important;
}

.pf-c-login .pf-c-check__label,
.pf-c-login .pf-c-switch__label {
    color: rgba(255, 255, 255, 0.9) !important;
    font-weight: 400 !important;
    font-size: 14px !important;
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) !important;
}

/* ===== HIDE 'Powered by authentik' ===== */

/* If text is in a span */
span:contains("Propulsé par"),
span:contains("Powered by") {
    display: none !important;
    visibility: hidden !important;
    font-size: 0 !important;
    line-height: 0 !important;
}

/* If it's in a <ul> list in footer */
.pf-c-login__footer .pf-c-list.pf-m-inline,
ul.pf-c-list.pf-m-inline li span:contains("authentik") {
    display: none !important;
    visibility: hidden !important;
}

/* Fallback selector: hide all brand links elements */
ak-brand-links,
.pf-c-login__footer .pf-c-login__footer .pf-c-list li:last-child,
*[data-ouia-component-type="PF4/Footer"],
ul.pf-c-list.pf-m-inline,
.pf-c-list.pf-m-inline {
    display: none !important;
    visibility: hidden !important;
}

/* ===== LOGIN PAGE MODERN DESIGN ===== */

/* Page Background */
.pf-c-login .pf-c-page,
body:has(.pf-c-login) .pf-c-page {
    /* Fixed background workaround for iOS */
    position: relative !important;
    min-height: 100vh !important;
    min-height: 100dvh !important;
    z-index: 0 !important;
}

.pf-c-login .pf-c-page::before,
body:has(.pf-c-login) .pf-c-page::before,
body::before {
    content: "" !important;
    position: fixed !important;
    top: 0 !important;
    left: 0 !important;
    width: 100% !important;
    height: 100% !important;
    background-image: var(--ak-flow-background) !important;
    background-size: cover !important;
    background-position: center !important;
    background-repeat: no-repeat !important;
    z-index: -1 !important;
    pointer-events: none !important;
}

/* Login page wrapper */
.pf-c-login,
body:has(.pf-c-login) {
    position: relative !important;
    min-height: 100vh !important;
    /* SAFEST CENTER ALIGNMENT */
    display: flex !important;
    flex-direction: column !important;
    align-items: center !important;
    /* justify-content: center REMOVED: prevents top clipping */
    overflow-y: auto !important;
    padding: 20px !important;
}

/* Login Container with Glass effect */
.pf-c-login .pf-c-login__container,
ak-flow-executor.pf-c-login {
    background:
        rgba(255, 255, 255, 0.03),
        url("data:image/svg+xml,%3Csvg viewBox='0 0 400 400' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' opacity='0.1'/%3E%3C/svg%3E") !important;
    backdrop-filter: blur(40px) !important;
    -webkit-backdrop-filter: blur(40px) !important;
    border: 1px solid rgba(255, 255, 255, 0.1) !important;
    border-radius: 24px !important;
    box-shadow:
        0 8px 32px rgba(0, 0, 0, 0.15),
        inset 0 1px 0 rgba(255, 255, 255, 0.1) !important;
    padding: 40px !important;
    /* Reduced padding */
    /* Desktop default: 450px min-width */
    min-width: 450px !important;
    /* Combined with max-width for responsiveness */
    width: 100% !important;
    max-width: 700px !important;
    transition: all 0.3s ease !important;
    position: relative !important;

    /* THE MAGICAL FIX: Safe Centering */
    margin: auto !important;

    /* STRICT SIZING: Prevent stretching */
    height: auto !important;
    flex-grow: 0 !important;
    flex-shrink: 0 !important;
    flex-basis: auto !important;

    max-height: none !important;
    overflow: visible !important;

    display: flex !important;
    flex-direction: column !important;
    justify-content: center !important;

    /* NUCLEAR SAFEGUARD */
    align-self: center !important;
    justify-self: center !important;
}

/* ===== SHADOW DOM LAYOUT FIXES (v2025.12+) ===== */

/* Fix 1: Remove BOTH ::before and ::after spacers that cause vertical gap */
.pf-c-login::before,
.pf-c-login::after,
ak-flow-executor.pf-c-login::before,
ak-flow-executor.pf-c-login::after,
body:has(.pf-c-login) .pf-c-login::before,
body:has(.pf-c-login) .pf-c-login::after {
    display: none !important;
    content: none !important;
    height: 0 !important;
}

/* Fix 2: Remove double card effect - strip glassmorphism from outer container */
ak-flow-executor.pf-c-login {
    background: transparent !important;
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
    border: none !important;
    box-shadow: none !important;
    padding: 0 !important;
    min-height: 100vh !important;
    margin: 0 !important;

    /* Fix grid layout for perfect centering */
    display: grid !important;
    align-content: center !important;
    justify-items: center !important;
    grid-template-rows: auto !important;
    gap: 20px !important;
}

/* Fix 3: Ensure body centers content without overflow */
body:has(.pf-c-login) {
    display: flex !important;
    align-items: center !important;
    justify-content: center !important;
    overflow-y: auto !important;
    min-height: 100vh !important;
}

/* Login Content */
.pf-c-login .pf-c-login__main,
ak-flow-executor::part(main) {
    box-sizing: border-box;
    background: transparent !important;
    backdrop-filter: blur(20px) !important;
    -webkit-backdrop-filter: blur(20px) !important;
    border: 1px solid rgba(255, 255, 255, 0.08) !important;
    border-radius: 66px !important;
    box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 16px, rgba(255, 255, 255, 0.05) 0px 1px 0px inset !important;
    padding: 45px !important;
    position: relative !important;
    z-index: 10 !important;

    /* NUCLEAR SIZING on the ACTUAL BOX */
    height: fit-content !important;
    min-height: 0 !important;
    flex-grow: 0 !important;

    /* Allow box to grow with content */
    overflow-y: visible !important;
    min-width: auto !important;
    max-height: none !important;
    width: 100% !important;
    /* iOS Fix: width 100% instead of 54vh */
    max-width: 570px !important;
    /* iOS Fix: max-width px instead of vh */
    margin: auto !important;
    height: fit-content !important;
    /* Ensure it shrinks to content */
}

/* Hide empty social links container to prevent spacing issues */
.pf-c-login__main-footer-links:empty,
ak-brand-links:empty {
    display: none !important;
    margin: 0 !important;
    padding: 0 !important;
}

/* Agrandissement exceptionnel pour le stage TOTP */
.pf-c-login .pf-c-login__main:has(ak-stage-authenticator-totp),
body:has(.pf-c-login) .pf-c-login__main:has(ak-stage-authenticator-totp) {
    max-height: 90vh !important;
    max-width: 600px !important;
    width: auto !important;
    padding-top: 30px !important;
    padding-bottom: 30px !important;
}

/* Overlay de chargement avec le même arrondi */
ak-loading-overlay {
    border-radius: 66px !important;
    overflow: hidden !important;
}

/* Force White Text in Login Container - Shadow DOM Compatible */
:host(ak-flow-executor) .pf-c-title,
:host(ak-flow-executor) h1,
:host(ak-flow-executor) h2,
:host(ak-flow-executor) h3,
:host(ak-flow-executor) p,
:host(ak-flow-executor) span,
:host(ak-flow-executor) div,
:host(ak-flow-executor) label,
:host(ak-flow-executor) .pf-c-form__label,
:host(ak-stage-identification) label,
:host(ak-stage-password) label,
:host(ak-stage-identification) .pf-c-title,
:host(ak-stage-password) .pf-c-title,
:host(ak-stage-consent) .pf-c-title,
:host(ak-stage-authenticator-validate) .pf-c-title,
:host(ak-stage-authenticator-validate) label,
:host(ak-stage-authenticator-validate-code) .pf-c-title,
:host(ak-stage-authenticator-validate-code) label,
:host(ak-stage-authenticator-validate-duo) .pf-c-title,
:host(ak-stage-authenticator-validate-duo) label,
:host(ak-user-settings) .pf-c-title,
:host(ak-user-settings) label,
:host(ak-user-settings) .pf-c-card__title,
:host(ak-user-settings-flow-executor) .pf-c-card__title,
:host(ak-user-settings-password) .pf-c-card__title,
:host(ak-form-element-horizontal) label,
:host(ak-form-element-horizontal) .pf-c-form__label,
:host(ak-user-stage-prompt) label,
:host(ak-user-stage-prompt) .pf-c-title,
:host(ak-stage-prompt) label,
:host(ak-stage-prompt) .pf-c-title {
    color: white !important;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5) !important;
}

/* Links in Login */
.pf-c-login a {
    color: rgba(255, 255, 255, 0.9) !important;
    text-decoration: none !important;
    transition: all 0.2s ease !important;
}

.pf-c-login a:hover {
    color: white !important;
    text-shadow: 0 0 8px rgba(255, 255, 255, 0.6) !important;
    text-decoration: underline !important;
}

/* Form Inputs - Shadow DOM targeted */
:host(ak-stage-identification) .pf-c-form-control,
:host(ak-stage-password) .pf-c-form-control,
:host(ak-flow-executor) .pf-c-form-control,
:host(ak-stage-identification) input:not([type="checkbox"]),
:host(ak-stage-password) input:not([type="checkbox"]),
:host(ak-stage-authenticator-validate) .pf-c-form-control,
:host(ak-stage-authenticator-validate) input:not([type="checkbox"]),
:host(ak-stage-authenticator-validate-code) .pf-c-form-control,
:host(ak-stage-authenticator-validate-code) input:not([type="checkbox"]),
:host(ak-stage-authenticator-totp) .pf-c-form-control,
:host(ak-stage-authenticator-totp) input:not([type="checkbox"]),
:host(ak-user-settings) .pf-c-form-control,
:host(ak-user-settings-flow-executor) .pf-c-form-control,
:host(ak-user-settings-password) .pf-c-form-control,
:host(ak-form-element-horizontal) .pf-c-form-control,
:host(ak-form-element-horizontal) input,
:host(ak-form-element-horizontal) select,
:host(ak-user-stage-prompt) .pf-c-form-control,
:host(ak-user-stage-prompt) input,
:host(ak-user-stage-prompt) select,
:host(ak-stage-prompt) .pf-c-form-control,
:host(ak-stage-prompt) input,
:host(ak-stage-prompt) select {
    background-color: rgba(255, 255, 255, 0.06) !important;
    border: 1.5px solid rgba(255, 255, 255, 0.2) !important;
    border-radius: 14px !important;
    padding: 10px 15px !important;
    font-size: 16px !important;
    color: white !important;
    height: auto !important;
    min-height: 40px !important;
    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
    backdrop-filter: blur(20px) saturate(130%) !important;
    -webkit-backdrop-filter: blur(20px) saturate(130%) !important;
    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.08) !important;
}

/* Ensure dropdown options are readable and select text is visible */
:host(ak-form-element-horizontal) select,
:host(ak-user-stage-prompt) select,
:host(ak-stage-prompt) select {
    color: white !important;
    background-color: rgba(255, 255, 255, 0.1) !important;
}

:host(ak-form-element-horizontal) select option,
:host(ak-user-stage-prompt) select option,
:host(ak-stage-prompt) select option {
    background-color: #1f1f1f !important;
    color: white !important;
    text-shadow: none !important;
}

:host(ak-stage-identification) .pf-c-form-control:focus,
:host(ak-stage-password) .pf-c-form-control:focus,
:host(ak-flow-executor) .pf-c-form-control:focus,
:host(ak-stage-identification) input:not([type="checkbox"]):focus,
:host(ak-stage-password) input:not([type="checkbox"]):focus,
:host(ak-stage-authenticator-validate) .pf-c-form-control:focus,
:host(ak-stage-authenticator-validate) input:not([type="checkbox"]):focus,
:host(ak-stage-authenticator-validate-code) .pf-c-form-control:focus,
:host(ak-stage-authenticator-validate-code) input:not([type="checkbox"]):focus,
:host(ak-user-settings) .pf-c-form-control:focus,
:host(ak-user-settings-flow-executor) .pf-c-form-control:focus,
:host(ak-user-settings-password) .pf-c-form-control:focus,
:host(ak-form-element-horizontal) .pf-c-form-control:focus,
:host(ak-form-element-horizontal) input:focus,
:host(ak-form-element-horizontal) select:focus,
:host(ak-user-stage-prompt) .pf-c-form-control:focus,
:host(ak-user-stage-prompt) input:focus,
:host(ak-user-stage-prompt) select:focus,
:host(ak-stage-prompt) .pf-c-form-control:focus,
:host(ak-stage-prompt) input:focus,
:host(ak-stage-prompt) select:focus {
    background-color: rgba(255, 255, 255, 0.08) !important;
    border-color: rgba(102, 126, 234, 0.5) !important;
    border-width: 1.5px !important;
    outline: none !important;
    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.08) !important;
}

/* Primary Button Login - Shadow DOM targeted */
:host(ak-stage-identification) .pf-c-button.pf-m-primary,
:host(ak-stage-password) .pf-c-button.pf-m-primary,
:host(ak-stage-consent) .pf-c-button.pf-m-primary,
:host(ak-stage-identification) button.pf-m-primary,
:host(ak-stage-password) button.pf-m-primary,
:host(ak-stage-authenticator-validate) .pf-c-button.pf-m-primary,
:host(ak-stage-authenticator-validate-code) .pf-c-button.pf-m-primary,
:host(ak-stage-authenticator-totp) .pf-c-button.pf-m-primary,
:host(ak-stage-authenticator-validate-duo) .pf-c-button.pf-m-primary,
:host(ak-stage-authenticator-validate-webauthn) .pf-c-button.pf-m-primary,
:host(ak-user-settings) .pf-c-button.pf-m-primary,
:host(ak-user-settings-flow-executor) .pf-c-button.pf-m-primary,
:host(ak-user-settings-password) .pf-c-button.pf-m-primary,
:host(ak-user-stage-prompt) .pf-c-button.pf-m-primary,
:host(ak-form-element-horizontal) .pf-c-button.pf-m-primary,
:host(ak-stage-authenticator-static) .pf-c-button.pf-m-primary,
:host(ak-stage-prompt) .pf-c-button.pf-m-primary,
/* FIX: Connect Button in User Settings */
:host(ak-user-settings-source-oauth) .pf-c-button.pf-m-primary {
    background: rgba(255, 255, 255, 0.15) !important;
    color: white !important;
    border: 1px solid rgba(255, 255, 255, 0.3) !important;
    border-radius: 14px !important;
    box-shadow:
        0 6px 20px rgba(0, 0, 0, 0.15),
        0 2px 8px rgba(255, 255, 255, 0.1),
        inset 0 1px 0 rgba(255, 255, 255, 0.2) !important;
    backdrop-filter: blur(20px) !important;
    -webkit-backdrop-filter: blur(20px) !important;
    transition: all 0.3s ease !important;
    text-transform: none !important;
    font-weight: 600 !important;
    font-size: 16px !important;
    /* Force height */
    padding-top: 12px !important;
    padding-bottom: 12px !important;
    padding-top: 12px !important;
    padding-bottom: 12px !important;
}

/* FIX: Disconnect Button (Danger) in User Settings */
:host(ak-user-settings-source-oauth) .pf-c-button.pf-m-danger {
    background: rgba(220, 20, 60, 0.25) !important;
    /* Red tint */
    color: white !important;
    border: 1px solid rgba(255, 100, 100, 0.3) !important;
    border-radius: 14px !important;
    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15) !important;
    backdrop-filter: blur(20px) !important;
    -webkit-backdrop-filter: blur(20px) !important;
    transition: all 0.3s ease !important;
    font-weight: 600 !important;
    font-size: 14px !important;
    /* Slightly smaller than primary */
    text-transform: none !important;
}

:host(ak-user-settings-source-oauth) .pf-c-button.pf-m-danger:hover {
    background: rgba(220, 20, 60, 0.4) !important;
    /* Brighter red on hover */
    border-color: rgba(255, 100, 100, 0.6) !important;
    transform: translateY(-2px) !important;
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25) !important;
    color: white !important;
}

/* Secondary Button (Security Key, WebAuthn, etc.) - Glassmorphism */
:host(ak-stage-identification) .pf-c-button.pf-m-secondary,
:host(ak-stage-password) .pf-c-button.pf-m-secondary,
:host(ak-stage-authenticator-validate) .pf-c-button.pf-m-secondary,
:host(ak-stage-authenticator-validate-code) .pf-c-button.pf-m-secondary,
:host(ak-stage-authenticator-webauthn) .pf-c-button.pf-m-secondary,
:host(ak-flow-executor) .pf-c-button.pf-m-secondary {
    background: rgba(255, 255, 255, 0.08) !important;
    color: white !important;
    border: 1px solid rgba(255, 255, 255, 0.2) !important;
    border-radius: 14px !important;
    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1) !important;
    backdrop-filter: blur(20px) !important;
    -webkit-backdrop-filter: blur(20px) !important;
    transition: all 0.3s ease !important;
    font-weight: 500 !important;
    margin-top: 10px !important;
    display: block !important;
    width: 100% !important;
    text-align: center !important;
}

:host(ak-stage-identification) .pf-c-button.pf-m-secondary:hover,
:host(ak-stage-password) .pf-c-button.pf-m-secondary:hover,
:host(ak-stage-authenticator-validate) .pf-c-button.pf-m-secondary:hover,
:host(ak-stage-authenticator-validate-code) .pf-c-button.pf-m-secondary:hover,
:host(ak-stage-authenticator-webauthn) .pf-c-button.pf-m-secondary:hover,
:host(ak-flow-executor) .pf-c-button.pf-m-secondary:hover {
    background: rgba(255, 255, 255, 0.15) !important;
    border-color: rgba(255, 255, 255, 0.4) !important;
    transform: translateY(-2px) !important;
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2) !important;
    color: white !important;
    color: white !important;
}

/* KEY ICON & GLITCH FIX */
/* Repurpose ::before to be a Key Icon instead of a glitchy bar */
:host(ak-stage-identification) .pf-c-button.pf-m-secondary::before,
:host(ak-stage-password) .pf-c-button.pf-m-secondary::before,
:host(ak-stage-authenticator-validate) .pf-c-button.pf-m-secondary::before,
:host(ak-flow-executor) .pf-c-button.pf-m-secondary::before {
    content: "" !important;
    display: inline-block !important;
    width: 16px !important;
    height: 16px !important;
    margin-right: 8px !important;
    /* Font Awesome Key Icon (White) */
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' fill='white'%3E%3Cpath d='M336 352c97.2 0 176-78.8 176-176S433.2 0 336 0S160 78.8 160 176c0 18.7 2.9 36.6 8.3 53.7L7 391c-4.5 4.5-7 10.6-7 17v80c0 13.3 10.7 24 24 24h80c13.3 0 24-10.7 24-24V448h40c13.3 0 24-10.7 24-24V384h40c6.4 0 12.5-2.5 17-7l33.3-33.3c16.9 5.2 38.6 352zM336 96a80 80 0 1 1 0 160 80 80 0 1 1 0-160z'/%3E%3C/svg%3E") !important;
    background-repeat: no-repeat !important;
    background-position: center !important;
    background-size: contain !important;
    /* Reset any glitchy borders/backgrounds from previous states */
    border: none !important;
    background-color: transparent !important;
    position: static !important;
    transform: none !important;
}

:host(ak-stage-identification) .pf-c-button.pf-m-primary:hover,
:host(ak-stage-password) .pf-c-button.pf-m-primary:hover,
:host(ak-stage-identification) button.pf-m-primary:hover,
:host(ak-stage-password) button.pf-m-primary:hover,
:host(ak-stage-authenticator-validate) .pf-c-button.pf-m-primary:hover,
:host(ak-stage-authenticator-validate-code) .pf-c-button.pf-m-primary:hover,
:host(ak-stage-authenticator-validate-duo) .pf-c-button.pf-m-primary:hover,
:host(ak-stage-authenticator-validate-webauthn) .pf-c-button.pf-m-primary:hover,
:host(ak-user-settings) .pf-c-button.pf-m-primary:hover,
:host(ak-user-settings-flow-executor) .pf-c-button.pf-m-primary:hover,
:host(ak-user-settings-password) .pf-c-button.pf-m-primary:hover,
:host(ak-user-stage-prompt) .pf-c-button.pf-m-primary:hover,
:host(ak-form-element-horizontal) .pf-c-button.pf-m-primary:hover,
:host(ak-stage-authenticator-static) .pf-c-button.pf-m-primary:hover,
:host(ak-stage-prompt) .pf-c-button.pf-m-primary:hover,
:host(ak-user-settings-source-oauth) .pf-c-button.pf-m-primary:hover {
    background: rgba(255, 255, 255, 0.25) !important;
    border-color: rgba(255, 255, 255, 0.4) !important;
    transform: translateY(-2px) !important;
    box-shadow:
        0 10px 30px rgba(0, 0, 0, 0.2),
        0 4px 12px rgba(255, 255, 255, 0.15),
        inset 0 1px 0 rgba(255, 255, 255, 0.25) !important;
}

/* Remove default PatternFly after element on buttons inside specific hosts */
:host(ak-stage-identification) .pf-c-button::after,
:host(ak-stage-password) .pf-c-button::after,
:host(ak-stage-consent) .pf-c-button::after {
    display: none !important;
    content: none !important;
}

/* Footer "Forgot Password" Band */
.pf-c-login__main-footer-band {
    background: transparent !important;
    backdrop-filter: none !important;
    border: none !important;
    box-shadow: none !important;
    margin-top: 10px !important;
    display: flex !important;
    justify-content: center !important;
}

.pf-c-login__main-footer-band a {
    color: rgba(255, 255, 255, 0.6) !important;
    text-decoration: none !important;
    font-size: 14px !important;
    transition: all 0.2s ease !important;
}

.pf-c-login__main-footer-band a:hover {
    color: white !important;
    text-decoration: underline !important;
    text-shadow: 0 0 5px rgba(255, 255, 255, 0.5) !important;
}

/* ===== UI GLOBAL (SCROLLBAR) ===== */

:root {
    --sb-size: 10px;
    --sb-radius: 12px;
    --sb-track: rgba(255, 255, 255, 0.06);
    --sb-thumb: rgba(255, 255, 255, 0.28);
    --sb-thumb-hover: rgba(255, 255, 255, 0.42);
    --sb-thumb-active: rgba(255, 255, 255, 0.55);
}

*::-webkit-scrollbar {
    width: var(--sb-size);
    height: var(--sb-size);
}

*::-webkit-scrollbar-track {
    background: var(--sb-track);
    border-radius: var(--sb-radius);
    margin: 4px;
}

*::-webkit-scrollbar-thumb {
    background-color: var(--sb-thumb);
    border-radius: var(--sb-radius);
    border: 2px solid transparent;
    background-clip: padding-box;
}

/* ===== APPS & DASHBOARD ===== */

/* Application Cards & User Settings Cards */
.pf-c-card.pf-m-compact,
.pf-c-expandable-section.pf-m-display-lg,
:host(ak-user-settings) .pf-c-card,
:host(ak-user-settings-flow-executor) .pf-c-card,
:host(ak-user-settings-password) .pf-c-card,
:host(ak-user-settings-mfa) .pf-c-card,
:host(ak-user-settings-source) .pf-c-card {
    position: relative;
    overflow: hidden;
    border-radius: 20px !important;
    background: rgba(255, 255, 255, 0.05) !important;
    backdrop-filter: blur(20px) saturate(130%) !important;
    -webkit-backdrop-filter: blur(20px) saturate(130%) !important;
    border: 1px solid rgba(255, 255, 255, 0.15) !important;
    transition: all 0.3s ease-in-out !important;
    box-shadow:
        0 4px 16px rgba(0, 0, 0, 0.4),
        inset 0 1px 0 rgba(255, 255, 255, 0.05) !important;
}

.pf-c-card.pf-m-compact:hover,
:host(ak-user-settings) .pf-c-card:hover,
:host(ak-user-settings-flow-executor) .pf-c-card:hover,
:host(ak-user-settings-password) .pf-c-card:hover {
    transform: translateY(-3px) scale(1.01) !important;
    box-shadow:
        0 8px 32px rgba(102, 126, 234, 0.25),
        0 4px 12px rgba(118, 75, 162, 0.2) !important;
    border-color: rgba(102, 126, 234, 0.4) !important;
}

/* User Settings Tabs - Shadow DOM */
:host(ak-user-settings) .pf-c-tabs,
:host(ak-user-settings) .pf-c-tabs__list {
    background: transparent !important;
    border-bottom-color: rgba(255, 255, 255, 0.1) !important;
}

:host(ak-user-settings) .pf-c-tabs__button {
    color: rgba(255, 255, 255, 0.7) !important;
}

:host(ak-user-settings) .pf-c-tabs__link.pf-m-current,
:host(ak-user-settings) .pf-c-tabs__item.pf-m-current .pf-c-tabs__button {
    color: white !important;
    background: rgba(255, 255, 255, 0.1) !important;
    border-radius: 10px 10px 0 0 !important;
}

:host(ak-user-settings) .pf-c-tabs__item.pf-m-current::after {
    border-bottom-color: #667eea !important;
    border-bottom-width: 3px !important;
}

/* Sidebar */
.pf-c-page__sidebar {
    background: rgba(19, 19, 19, 0.4) !important;
    backdrop-filter: blur(20px) saturate(120%) !important;
    -webkit-backdrop-filter: blur(20px) saturate(120%) !important;
    border-right: 1px solid rgba(255, 255, 255, 0.08) !important;
}

/* Header */
.pf-c-page__header,
.pf-c-page__header-tools {
    background: transparent !important;
    border-bottom: none !important;
    box-shadow: none !important;
}

.pf-c-page>.pf-c-page__header {
    background: rgba(0, 0, 0, 0.4) !important;
    backdrop-filter: blur(18px) !important;
    -webkit-backdrop-filter: blur(18px) !important;
    border-bottom: 1px solid rgba(255, 255, 255, 0.1) !important;
}

/* IOS/Mobile Fix: .pf-c-page needs relative positioning, fixed bg moved to ::before */
.pf-c-page {
    /* iOS Fix: moved to pseudo-element to preventing scrolling glitch */
    /* background-image removed from here, handled via ::before on specific pages or globally if needed */
    position: relative !important;
    background: transparent !important;
    background-color: transparent !important;
}

/* Ensure inner page containers are transparent so the background shows through */
:host(ak-library-impl) .pf-c-page,
:host(ak-library-impl) .pf-c-page__main,
.pf-c-page__main,
.pf-c-page__main-section,
.pf-c-page__drawer,
.pf-c-drawer__content,
.pf-c-drawer__main,
:host(ak-library-impl) .pf-l-gallery {
    background: transparent !important;
    background-color: transparent !important;
}

/* Global Background: ONLY apply outside of Admin Interface */
/* This prevents the background from leaking into the admin panel */
/* Global Background: Applied generally, but hidden in specific contexts below */
/* OPT-IN BACKGROUND STRATEGY */
/* Only apply the fixed background to User Interface and Login flows */
/* This automatically excludes the Admin Interface */

/* REF-INSPIRED BACKGROUND STRATEGY */
/* Apply background directly to the page wrapper, like in theme-ref-icon.css */
.pf-c-page {
    background-image: var(--ak-flow-background) !important;
    background-size: cover !important;
    background-position: center !important;
    background-repeat: no-repeat !important;
    background-attachment: fixed !important;
}

/* HIJACK ADMIN PANEL: Remove background image in admin interface */
:host(ak-interface-admin) .pf-c-page {
    background-image: none !important;
    background-color: var(--pf-global--BackgroundColor--100) !important;
}

/* DASHBOARD TRANSPARENCY FIX */
/* Force variables to transparent inside the library/dashboard host AND user interface wrapper */
:host(ak-library-impl),
:host(ak-interface-user) {
    --pf-global--BackgroundColor--100: transparent !important;
    --pf-c-page--BackgroundColor: transparent !important;
    --pf-c-page__main-section--BackgroundColor: transparent !important;
}

/* DASHBOARD TRANSPARENCY FIX */
/* Force variables to transparent inside the library/dashboard host AND user interface wrapper */
:host(ak-library-impl),
:host(ak-interface-user) {
    --pf-global--BackgroundColor--100: transparent !important;
    --pf-c-page--BackgroundColor: transparent !important;
    --pf-c-page__main-section--BackgroundColor: transparent !important;
}

/* Force specific containers to be transparent in dashboard */
:host(ak-library-impl) .pf-c-page,
:host(ak-library-impl) .pf-c-page__main,
:host(ak-library-impl) .pf-c-page__main-section,
:host(ak-library-impl) .pf-c-drawer__content,
:host(ak-library-impl) .pf-c-drawer__main,
:host(ak-interface-user) .pf-c-page,
:host(ak-interface-user) .pf-c-page__main {
    background: transparent !important;
    background-color: transparent !important;
}


/* ========================================= */
/* === FORM INPUTS & PASSWORD TOGGLE FIX === */
/* ========================================= */

/* Fix Input Group Position to allow absolute button */
.pf-c-input-group {
    position: relative !important;
    display: flex !important;
    width: 100% !important;
}

/* Position the "Show Password" button inside the input */
.pf-c-button.pf-m-control {
    position: absolute !important;
    top: 50% !important;
    right: 5px !important;
    transform: translateY(-50%) !important;
    background: transparent !important;
    border: none !important;
    box-shadow: none !important;
    /* Very high z-index to stay above focused inputs */
    z-index: 9999 !important;
    padding: 5px !important;
    margin: 0 !important;
    height: auto !important;
    min-width: 0 !important;
    width: 30px !important;
    display: flex !important;
    align-items: center !important;
    justify-content: center !important;
    cursor: pointer !important;
    pointer-events: auto !important;
}

/* Hover effect */
.pf-c-button.pf-m-control:hover {
    background: rgba(255, 255, 255, 0.1) !important;
    border-radius: 50% !important;
}

/* Remove borders */
.pf-c-button.pf-m-control::after,
.pf-c-button.pf-m-control::before {
    display: none !important;
    content: none !important;
}

/* Ensure input has space */
.pf-c-form-control {
    padding-right: 40px !important;
}

/* ===== ADMIN INTERFACE PRESERVATION ===== */
/* Reset sensitive admin elements to avoid breaking them */

[data-ak-interface-root="ak-interface-admin"] .pf-c-button.pf-m-plain,
[data-ak-interface-root="ak-interface-admin"] .pf-c-button.pf-m-secondary,
[data-ak-interface-root="ak-interface-admin"] .pf-c-button.pf-m-primary {
    width: auto !important;
    min-width: 0 !important;
    height: auto !important;
    transform: none !important;
    background: var(--pf-c-button--m-plain--BackgroundColor, transparent) !important;
    border: none !important;
    box-shadow: none !important;
    color: var(--pf-c-button--m-plain--Color, inherit) !important;
}

[data-ak-interface-root="ak-interface-admin"] .pf-c-select__toggle,
[data-ak-interface-root="ak-interface-admin"] .pf-c-form-control {
    height: auto !important;
    min-height: 0 !important;
    padding: initial !important;
    background: var(--pf-c-form-control--BackgroundColor, #fff) !important;
    border: 1px solid var(--pf-c-form-control--BorderColor, #ccc) !important;
    border-radius: var(--pf-c-form-control--BorderRadius, 3px) !important;
    backdrop-filter: none !important;
    box-shadow: none !important;
    color: var(--pf-c-form-control--Color, initial) !important;
}

/* ===== ADMIN BUTTON STYLING ===== */

:host(ak-interface-user) a[href*="/if/admin/"],
:host(ak-interface-user-presentation) a[href*="/if/admin/"],
:host(ak-interface-user) .pf-c-page__header-tools-item a.pf-c-button.pf-m-secondary,
:host(ak-interface-user-presentation) .pf-c-page__header-tools-item a.pf-c-button.pf-m-secondary {
    border-radius: 50px !important;
    border: none !important;
    background: rgba(255, 255, 255, 0.15) !important;
    backdrop-filter: blur(10px) !important;
    -webkit-backdrop-filter: blur(10px) !important;
    color: white !important;
    padding: 8px 24px !important;
    transition: all 0.3s ease !important;
    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1) !important;
    text-transform: none !important;
    font-weight: 500 !important;
}

/* Button 'Yes' / Primary */
:host(ak-stage-identification) .pf-c-button.pf-m-primary {
    background: rgba(40, 167, 69, 0.25) !important;
    backdrop-filter: blur(10px) !important;
    -webkit-backdrop-filter: blur(10px) !important;
    border: 1.5px solid rgba(40, 167, 69, 0.4) !important;
    color: white !important;
    font-weight: 600 !important;
    padding: 10px 24px !important;
    border-radius: 14px !important;
    transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important;
    box-shadow:
        0 4px 15px rgba(40, 167, 69, 0.15),
        0 2px 8px rgba(0, 0, 0, 0.1),
        inset 0 1px 0 rgba(255, 255, 255, 0.15) !important;
    position: relative !important;
    overflow: hidden !important;
    margin-top: 4px !important;
    /* Fix for hover clipping */
    margin-bottom: 4px !important;
}

:host(ak-interface-user) a[href*="/if/admin/"]:hover,
:host(ak-interface-user-presentation) a[href*="/if/admin/"]:hover,
:host(ak-interface-user) .pf-c-page__header-tools-item a.pf-c-button.pf-m-secondary:hover,
:host(ak-interface-user-presentation) .pf-c-page__header-tools-item a.pf-c-button.pf-m-secondary:hover {
    background: rgba(255, 255, 255, 0.25) !important;
    transform: translateY(-2px) !important;
    box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.2) !important;
    color: white !important;
    text-decoration: none !important;
}

:host(ak-interface-user) a[href*="/if/admin/"]::after,
:host(ak-interface-user-presentation) a[href*="/if/admin/"]::after,
:host(ak-interface-user) .pf-c-page__header-tools-item a.pf-c-button.pf-m-secondary::after,
:host(ak-interface-user-presentation) .pf-c-page__header-tools-item a.pf-c-button.pf-m-secondary::after {
    border: none !important;
    display: none !important;
    content: none !important;
}

:host(ak-interface-user) a[href*="/if/admin/"]:focus,
:host(ak-interface-user-presentation) a[href*="/if/admin/"]:focus,
:host(ak-interface-user) .pf-c-page__header-tools-item a.pf-c-button.pf-m-secondary:focus,
:host(ak-interface-user-presentation) .pf-c-page__header-tools-item a.pf-c-button.pf-m-secondary:focus {
    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1) !important;
}

/* ===== DASHBOARD CARDS ===== */

:host(ak-library-impl) .pf-c-card.pf-m-hoverable {
    background: rgba(255, 255, 255, 0.08) !important;
    border: 1px solid rgba(255, 255, 255, 0.1) !important;
    backdrop-filter: blur(10px) !important;
    transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;
    border-radius: 20px !important;
}

:host(ak-library-impl) .pf-c-card.pf-m-hoverable:hover {
    background: rgba(255, 255, 255, 0.15) !important;
    transform: translateY(-4px) !important;
    box-shadow: 0 12px 24px rgba(0, 0, 0, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.15) !important;
    border-color: rgba(255, 255, 255, 0.25) !important;
}

/* Remove PatternFly default hover flash/border */
:host(ak-library-impl) .pf-c-card.pf-m-hoverable::after,
:host(ak-library-impl) .pf-c-card.pf-m-hoverable::before,
:host(ak-library-impl) .pf-c-card.pf-m-hoverable:hover::after,
:host(ak-library-impl) .pf-c-card.pf-m-hoverable:hover::before {
    display: none !important;
    border: none !important;
    content: none !important;
    box-shadow: none !important;
}

:host(ak-library-impl) .pf-c-card__title,
:host(ak-library-impl) .pf-c-card__body {
    color: white !important;
    color: white !important;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5) !important;
}

/* ===== DASHBOARD SEARCH BAR ===== */
/* Fix the red underline on hover/focus to be white instead */

:host(ak-library-impl) input[type="text"],
:host(ak-library-impl) input[type="search"],
:host(ak-library-impl) .pf-c-form-control {
    border-bottom-color: rgba(255, 255, 255, 0.3) !important;
}

:host(ak-library-impl) input[type="text"]:hover,
:host(ak-library-impl) input[type="search"]:hover,
:host(ak-library-impl) input[type="text"]:focus,
:host(ak-library-impl) input[type="search"]:focus,
:host(ak-library-impl) .pf-c-form-control:hover,
:host(ak-library-impl) .pf-c-form-control:focus {
    border-bottom-color: white !important;
    border-color: rgba(255, 255, 255, 0.5) !important;
}


/* ===== DISABLE HOVER ON SETTINGS CARDS ===== */
/* These cards should be static containers, not interactive */

:host(ak-user-settings) .pf-c-card:hover,
:host(ak-user-settings-flow-executor) .pf-c-card:hover,
:host(ak-user-settings-password) .pf-c-card:hover {
    transform: none !important;
    background: rgba(255, 255, 255, 0.08) !important;
    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1) !important;
    border-color: rgba(255, 255, 255, 0.1) !important;
}

:host(ak-user-settings) .pf-c-card.pf-m-hoverable::after,
:host(ak-user-settings) .pf-c-card.pf-m-hoverable::before,
:host(ak-user-settings) .pf-c-card:hover::after,
:host(ak-user-settings) .pf-c-card:hover::before,
:host(ak-user-settings-flow-executor) .pf-c-card:hover::after,
:host(ak-user-settings-flow-executor) .pf-c-card:hover::before,
:host(ak-user-settings-password) .pf-c-card:hover::after,
:host(ak-user-settings-password) .pf-c-card:hover::before {
    display: none !important;
    content: none !important;
    border: none !important;
}

/* ===== USER SETTINGS TABLES TRANSPARENCY ===== */
/* Remove grey background from tables in user settings only */

:host(ak-user-session-list) .pf-c-table,
:host(ak-user-settings-mfa) .pf-c-table,
:host(ak-user-consent-list) .pf-c-table,
:host(ak-user-reputation-list) .pf-c-table,
:host(ak-user-token-list) .pf-c-table,
:host(ak-user-session-list) .pf-c-table tr,
:host(ak-user-settings-mfa) .pf-c-table tr,
:host(ak-user-consent-list) .pf-c-table tr,
:host(ak-user-reputation-list) .pf-c-table tr,
:host(ak-user-token-list) .pf-c-table tr {
    background-color: transparent !important;
    background: transparent !important;
    --pf-c-table--BackgroundColor: transparent !important;
}

:host(ak-user-session-list) .pf-c-table tbody>tr>*,
:host(ak-user-settings-mfa) .pf-c-table tbody>tr>*,
:host(ak-user-consent-list) .pf-c-table tbody>tr>*,
:host(ak-user-reputation-list) .pf-c-table tbody>tr>*,
:host(ak-user-token-list) .pf-c-table tbody>tr>* {
    --pf-c-table--cell--BackgroundColor: transparent !important;
    border-bottom-color: rgba(255, 255, 255, 0.1) !important;
}

/* Remove card background if it wraps the table */
:host(ak-user-session-list) .pf-c-card,
:host(ak-user-settings-mfa) .pf-c-card,
:host(ak-user-consent-list) .pf-c-card,
:host(ak-user-reputation-list) .pf-c-card,
:host(ak-user-token-list) .pf-c-card {
    background-color: transparent !important;
    box-shadow: none !important;
}

/* Table Headers (Toolbar) and Footers (Pagination) */
:host(ak-user-session-list) .pf-c-toolbar,
:host(ak-user-settings-mfa) .pf-c-toolbar,
:host(ak-user-consent-list) .pf-c-toolbar,
:host(ak-user-reputation-list) .pf-c-toolbar,
:host(ak-user-token-list) .pf-c-toolbar,
:host(ak-user-session-list) .pf-c-pagination,
:host(ak-user-settings-mfa) .pf-c-pagination,
:host(ak-user-consent-list) .pf-c-pagination,
:host(ak-user-reputation-list) .pf-c-pagination,
:host(ak-user-token-list) .pf-c-pagination {
    background-color: transparent !important;
    background: transparent !important;
    border: none !important;
    box-shadow: none !important;
    --pf-c-toolbar--BackgroundColor: transparent !important;
}

/* Toolbar Buttons (Refresh, Delete) */
:host(ak-user-session-list) .pf-c-toolbar .pf-c-button,
:host(ak-user-settings-mfa) .pf-c-toolbar .pf-c-button,
:host(ak-user-consent-list) .pf-c-toolbar .pf-c-button,
:host(ak-user-reputation-list) .pf-c-toolbar .pf-c-button,
:host(ak-user-token-list) .pf-c-toolbar .pf-c-button,
/* Fix specifically for ak-spinner-button which wraps the button */
:host(ak-user-session-list) ak-spinner-button .pf-c-button,
:host(ak-user-settings-mfa) ak-spinner-button .pf-c-button,
:host(ak-user-consent-list) ak-spinner-button .pf-c-button,
:host(ak-user-reputation-list) ak-spinner-button .pf-c-button,
:host(ak-user-token-list) ak-spinner-button .pf-c-button,
/* Target the shadow DOM part for spinner buttons */
:host(ak-user-session-list) ak-spinner-button::part(spinner-button),
:host(ak-user-settings-mfa) ak-spinner-button::part(spinner-button),
:host(ak-user-consent-list) ak-spinner-button::part(spinner-button),
:host(ak-user-reputation-list) ak-spinner-button::part(spinner-button),
:host(ak-user-token-list) ak-spinner-button::part(spinner-button) {
    background: rgba(255, 255, 255, 0.1) !important;
    border: 1px solid rgba(255, 255, 255, 0.2) !important;
    color: white !important;
    backdrop-filter: blur(5px);
    border-radius: 6px !important;
}

/* Fix pseudo-elements that cause the blue border in PatternFly */
:host(ak-user-session-list) ak-spinner-button::part(spinner-button)::after,
:host(ak-user-settings-mfa) ak-spinner-button::part(spinner-button)::after,
:host(ak-user-consent-list) ak-spinner-button::part(spinner-button)::after,
:host(ak-user-reputation-list) ak-spinner-button::part(spinner-button)::after,
:host(ak-user-token-list) ak-spinner-button::part(spinner-button)::after {
    border: none !important;
    border-color: transparent !important;
}

/* Secondary Actions in Token List (Create, etc) */
:host(ak-user-token-list) .pf-c-button.pf-m-secondary {
    background: rgba(255, 255, 255, 0.1) !important;
    border: 1px solid rgba(255, 255, 255, 0.2) !important;
    color: white !important;
}

:host(ak-user-token-list) .pf-c-button.pf-m-secondary::after {
    border: none !important;
    border-color: transparent !important;
}

/* Specific fix for "Enroll" dropdown toggle */
:host(ak-user-settings-mfa) .pf-c-dropdown__toggle.pf-m-primary {
    background: rgba(255, 255, 255, 0.1) !important;
    border: 1px solid rgba(255, 255, 255, 0.2) !important;
    color: white !important;
}

:host(ak-user-settings-mfa) .pf-c-dropdown__toggle.pf-m-primary:after {
    border: none !important;
}

/* Search Bar Styling */
:host(ak-table-search) .pf-c-form-control,
:host(ak-table-search) input[type="search"] {
    background-color: rgba(255, 255, 255, 0.08) !important;
    border: 1px solid rgba(255, 255, 255, 0.2) !important;
    color: white !important;
    border-radius: 6px !important;
}

/* Search Bar Hover - Use accent color */
:host(ak-table-search) .pf-c-form-control:hover,
:host(ak-table-search) input[type="search"]:hover {
    border-color: var(--ak-accent) !important;
}

/* ========================================= */
/* GLOBAL CHIP / BADGE FIX                   */
/* ========================================= */

/* ========================================= */
/* ========================================= */
/* SPECIFIC HOST CHIP FIX (USER INTERFACE)   */
/* ========================================= */

/* Target ak-chip ONLY within specific User Interface components */
/* This ensures Admin Interface chips are NEVER touched */

:host(ak-user-consent-list) ak-chip,
:host(ak-user-consent-list) ak-chip[theme="dark"],
:host(ak-user-token-list) ak-chip,
:host(ak-user-token-list) ak-chip[theme="dark"],
:host(ak-user-settings-mfa) ak-chip,
:host(ak-user-settings-mfa) ak-chip[theme="dark"],
:host(ak-user-session-list) ak-chip,
:host(ak-user-session-list) ak-chip[theme="dark"],
:host(ak-user-reputation-list) ak-chip,
:host(ak-user-reputation-list) ak-chip[theme="dark"],
/* FIX: Permission Chips */
:host(ak-user-consent-list) .pf-c-chip,
.pf-c-chip,
ak-chip {
    --pf-c-chip--BackgroundColor: #333333 !important;
    --pf-c-chip__text--Color: white !important;
    --pf-c-chip--Color: white !important;
    color: white !important;
}



:host(ak-user-consent-list) ak-chip::part(chip),
:host(ak-user-token-list) ak-chip::part(chip),
:host(ak-user-settings-mfa) ak-chip::part(chip),
:host(ak-user-session-list) ak-chip::part(chip),
:host(ak-user-reputation-list) ak-chip::part(chip) {
    background: #333333 !important;
    background-color: #333333 !important;
    background-image: none !important;
    color: white !important;
    border: 1px solid rgba(255, 255, 255, 0.2) !important;
    border-radius: 12px !important;
}

:host(ak-user-consent-list) ak-chip::part(chip)::before,
:host(ak-user-consent-list) ak-chip::part(chip)::after,
:host(ak-user-token-list) ak-chip::part(chip)::before,
:host(ak-user-token-list) ak-chip::part(chip)::after,
:host(ak-user-settings-mfa) ak-chip::part(chip)::before,
:host(ak-user-settings-mfa) ak-chip::part(chip)::after,
:host(ak-user-session-list) ak-chip::part(chip)::before,
:host(ak-user-session-list) ak-chip::part(chip)::after,
:host(ak-user-reputation-list) ak-chip::part(chip)::before,
:host(ak-user-reputation-list) ak-chip::part(chip)::after {
    display: none !important;
    content: none !important;
}

:host(ak-user-consent-list) ak-chip::part(chip):hover,
:host(ak-user-token-list) ak-chip::part(chip):hover,
:host(ak-user-settings-mfa) ak-chip::part(chip):hover,
:host(ak-user-session-list) ak-chip::part(chip):hover,
:host(ak-user-reputation-list) ak-chip::part(chip):hover {
    background-color: rgba(255, 255, 255, 0.2) !important;
}

:host(ak-user-session-list) .pf-c-toolbar .pf-c-button:hover,
:host(ak-user-settings-mfa) .pf-c-toolbar .pf-c-button:hover,
:host(ak-user-consent-list) .pf-c-toolbar .pf-c-button:hover,
:host(ak-user-reputation-list) .pf-c-toolbar .pf-c-button:hover,
:host(ak-user-token-list) .pf-c-toolbar .pf-c-button:hover,
:host(ak-user-session-list) ak-spinner-button .pf-c-button:hover,
:host(ak-user-settings-mfa) ak-spinner-button .pf-c-button:hover,
:host(ak-user-consent-list) ak-spinner-button .pf-c-button:hover,
:host(ak-user-reputation-list) ak-spinner-button .pf-c-button:hover,
:host(ak-user-token-list) ak-spinner-button .pf-c-button:hover,
:host(ak-user-session-list) ak-spinner-button::part(spinner-button):hover,
:host(ak-user-settings-mfa) ak-spinner-button::part(spinner-button):hover,
:host(ak-user-consent-list) ak-spinner-button::part(spinner-button):hover,
:host(ak-user-reputation-list) ak-spinner-button::part(spinner-button):hover,
:host(ak-user-token-list) ak-spinner-button::part(spinner-button):hover,
:host(ak-user-settings-mfa) .pf-c-dropdown__toggle.pf-m-primary:hover,
:host(ak-user-token-list) .pf-c-button.pf-m-secondary:hover,
:host(ak-user-consent-list) ak-chip:hover {
    background: rgba(255, 255, 255, 0.2) !important;
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}

:host(ak-user-consent-list) ak-chip::part(chip):hover {
    background-color: rgba(255, 255, 255, 0.2) !important;
}

/* ========================================= */
/* DASHBOARD APP ICONS REFINEMENT            */
/* ========================================= */

/* Reduce the size of large icons (User Dashboard) */
ak-app-icon[size="pf-m-lg"] {
    --icon-height: 3rem !important;
    /* Reduced from 4rem */
}

/* Rounded corners for App Icons to prevent sharp rectangles */
ak-app-icon::part(icon) {
    border-radius: 15px !important;
    overflow: hidden !important;
}

/* Ensure images cover the area nicely if they have backgrounds */
ak-app-icon::part(icon image) {
    border-radius: 15px !important;
}



.pf-c-label.pf-m-green {
    --pf-c-label--BackgroundColor: #1c1c1c !important;
    --pf-c-label__icon--Color: #4d9144;
    --pf-c-label__content--before--BorderColor: #474747 !important;
    --pf-c-label__content--Color: var(--pf-c-label--m-green__content--Color);
    --pf-c-label__content--before--BorderColor: var(--pf-c-label--m-green__content--before--BorderColor);
    --pf-c-label__content--link--hover--before--BorderColor: var(--pf-c-label--m-green__content--link--hover--before--BorderColor);
    --pf-c-label__content--link--focus--before--BorderColor: var(--pf-c-label--m-green__content--link--focus--before--BorderColor);
    --pf-c-label--m-outline__content--Color: var(--pf-c-label--m-outline--m-green__content--Color);
    --pf-c-label--m-outline__content--before--BorderColor: #3f453f !important;
    /* --pf-c-label--m-outline__content--link--hover--before--BorderColor: var(--pf-c-label--m-outline--m-green__content--link--hover--before--BorderColor); */
    --pf-c-label--m-outline__content--link--focus--before--BorderColor: var(--pf-c-label--m-outline--m-green__content--link--focus--before--BorderColor);
    --pf-c-label--m-editable__content--before--BorderColor: var(--pf-c-label--m-green__content--before--BorderColor);
    --pf-c-label--m-editable__content--hover--before--BorderColor: var(--pf-c-label--m-green__content--before--BorderColor);
    --pf-c-label--m-editable__content--focus--before--BorderColor: var(--pf-c-label--m-green__content--before--BorderColor);
}

.pf-c-label.pf-m-gray {
    --pf-c-label--BackgroundColor: #1a1a1a !important;
    --pf-c-label__icon--Color: #d91e1e !important;
    --pf-c-label__content--Color: var(--pf-c-label--m-gray__content--Color);
    --pf-c-label__content--before--BorderColor: var(--pf-c-label--m-gray__content--before--BorderColor);
    --pf-c-label__content--link--hover--before--BorderColor: var(--pf-c-label--m-gray__content--link--hover--before--BorderColor);
    --pf-c-label__content--link--focus--before--BorderColor: var(--pf-c-label--m-gray__content--link--focus--before--BorderColor);
}


/* ========================================= */
/* SOCIAL LOGIN BUTTONS (EXTERNAL IDP)       */
/* ========================================= */






.pf-c-login__main-footer-links {
    margin-top: 0 !important;
    position: relative !important;
    display: flex !important;
    justify-content: center !important;
    /* Ensure separator is full width on top */
    flex-wrap: wrap !important;
}

/* Custom CSS-only Separator "Ou" */
.pf-c-login__main-footer-links::before {
    content: var(--ak-social-separator-text) !important;
    display: block !important;
    width: 100% !important;
    text-align: center !important;
    font-size: 15px !important;
    color: #ffffff !important;
    opacity: 1 !important;
    margin-bottom: 20px !important;
    margin-top: 10px !important;
    font-weight: 400 !important;

    /* The Line Trick: Solid White & Balanced Gap for "Continuer avec" */
    background: linear-gradient(to right,
            rgba(255, 255, 255, 1) 0%,
            rgba(255, 255, 255, 1) 32%,
            transparent 32%,
            transparent 68%,
            rgba(255, 255, 255, 1) 68%,
            rgba(255, 255, 255, 1) 100%) !important;
    background-size: 100% 1px !important;
    background-position: center !important;
    background-repeat: no-repeat !important;
}

.pf-c-login__main-footer-links-item-link {
    background: rgba(255, 255, 255, 0.08) !important;
    border: 1px solid rgba(255, 255, 255, 0.2) !important;
    color: white !important;
    border-radius: 12px !important;
    padding: 10px 20px !important;
    transition: all 0.3s ease !important;
    display: flex !important;
    align-items: center !important;
    justify-content: center !important;
    text-decoration: none !important;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1) !important;
}

/* Resize the icon (FontAwesome or SVG) */
.pf-c-login__main-footer-links-item-link i,
.pf-c-login__main-footer-links-item-link svg {
    /* Revert to slightly larger icon since text is gone */
    font-size: 24px !important;
    width: 24px !important;
    height: 24px !important;
    text-align: center !important;
    display: flex !important;
    align-items: center !important;
    justify-content: center !important;
}

/* If no match found, fallback to aria-label or just blank */
content: attr(aria-label);
font-size: 14px !important;
font-weight: 500 !important;
color: rgba(255, 255, 255, 0.9) !important;
white-space: nowrap !important;
display: block !important;
text-transform: capitalize !important;
}

/* Ensure nice casing */
}

/* Resize the icon (FontAwesome or SVG) */
.pf-c-login__main-footer-links-item-link i,
.pf-c-login__main-footer-links-item-link svg {
    font-size: 18px !important;
    /* Reduce specific icon size */
    width: 18px !important;
    height: 18px !important;
    text-align: center !important;
    display: flex !important;
    align-items: center !important;
    justify-content: center !important;
}

.pf-c-login__main-footer-links-item-link:hover {
    background: rgba(255, 255, 255, 0.2) !important;
    transform: translateY(-2px) !important;
    box-shadow: 0 8px 15px rgba(0, 0, 0, 0.2) !important;
    border-color: rgba(255, 255, 255, 0.4) !important;
}

/* Ensure no default underlined link style */
.pf-c-login__main-footer-links-item-link:active,
.pf-c-login__main-footer-links-item-link:visited {
    color: white !important;
    text-decoration: none !important;
}

/* Specific fix for images inside social buttons */
.pf-c-login__main-footer-links-item-link img {
    margin-right: 8px !important;
    max-height: 20px !important;
}

/* ===== SOCIAL BUTTONS IN FOOTER (Discord, Google, etc.) ===== */
/* Horizontal layout with white glassmorphism matching login button */

/* Container for horizontal layout */
.pf-c-login__main-footer-links {
    display: flex !important;
    flex-direction: row !important;
    flex-wrap: wrap !important;
    justify-content: center !important;
    align-items: center !important;
    gap: 12px !important;
    margin-top: 10px !important;
}

.pf-c-login__main-footer-links-item {
    display: inline-flex !important;
    width: auto !important;
}


.pf-c-login__main-footer-links-item button {
    background: rgba(255, 255, 255, 0.15) !important;
    backdrop-filter: blur(10px) !important;
    -webkit-backdrop-filter: blur(10px) !important;
    border: 1px solid rgba(255, 255, 255, 0.3) !important;
    color: white !important;
    font-weight: 600 !important;
    border-radius: 12px !important;
    padding: 10px 16px !important;
    font-size: 14px !important;
    display: flex !important;
    align-items: center !important;
    justify-content: center !important;
    gap: 6px !important;
    min-width: 100px !important;
    max-width: 120px !important;
    height: auto !important;
    transition: all 0.3s ease !important;
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15), 0 2px 8px rgba(255, 255, 255, 0.1) !important;
    position: relative;
    overflow: hidden;
    margin: 0 !important;
    flex: 0 0 auto !important;
}

.pf-c-login__main-footer-links-item button img {
    width: 18px !important;
    height: 18px !important;
    margin-right: 0 !important;
    flex-shrink: 0;
    display: inline-block;
}

/* Remove specific Discord/Google colors - use white glassmorphism for all */
.pf-c-login__main-footer-links-item button:has(img[src*="discord"]),
.pf-c-login__main-footer-links-item button:has(img[src*="google"]) {
    background: rgba(255, 255, 255, 0.15) !important;
    border-color: rgba(255, 255, 255, 0.3) !important;
}

/* Make Discord icon white */
.pf-c-login__main-footer-links-item button:has(img[src*="discord"]) img {
    filter: brightness(0) invert(1);
}

/* Hover effect - identical to login button */
.pf-c-login__main-footer-links-item button:hover {
    background: rgba(255, 255, 255, 0.25) !important;
    border-color: rgba(255, 255, 255, 0.4) !important;
    transform: translateY(-2px) !important;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2), 0 4px 12px rgba(255, 255, 255, 0.15) !important;
    cursor: pointer;
}

/* Separator "ou" above buttons */
.pf-c-login__main-footer-links::before {
    content: "────────  ou  ────────";
    display: block;
    width: 100%;
    color: #b8bbc2;
    text-align: center;
    font-size: 14px;
    font-weight: 500;
    margin: 10px 0 15px 0;
    letter-spacing: 0.4px;
    opacity: 0.8;
}

/* ===== RESPONSIVE MOBILE ===== */
@media (max-width: 768px) {

    /* Force le background de la page sur mobile */
    .pf-c-page,
    body,
    .pf-c-login .pf-c-page,
    body:has(.pf-c-login) .pf-c-page {
        background: transparent !important;
        background-color: transparent !important;
        position: fixed !important;
        top: 0 !important;
        left: 0 !important;
        right: 0 !important;
        bottom: 0 !important;
        width: 100vw !important;
        height: 100vh !important;
        margin: 0 !important;
        padding: 0 !important;
        overflow: hidden !important;
    }

    /* Background flouté sur un pseudo-élément */
    .pf-c-page::before,
    body::before,
    .pf-c-login .pf-c-page::before,
    body:has(.pf-c-login)::before {
        content: '' !important;
        position: fixed !important;
        top: -10px !important;
        left: -10px !important;
        right: -10px !important;
        bottom: -10px !important;
        width: calc(100vw + 20px) !important;
        height: calc(100vh + 20px) !important;
        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
        background-image: var(--ak-flow-background) !important;
        background-size: cover !important;
        background-position: center !important;
        background-repeat: no-repeat !important;
        filter: blur(8px) !important;
        -webkit-filter: blur(8px) !important;
        z-index: -1 !important;
    }

    /* Force .pf-c-login à prendre toute la largeur sur mobile */
    .pf-c-login {
        width: 100vw !important;
        max-width: 100vw !important;
        min-width: 100vw !important;
        height: 100vh !important;
        min-height: 100vh !important;
        padding: 0 !important;
        margin: 0 !important;
        position: fixed !important;
        top: 0 !important;
        left: 0 !important;
        right: 0 !important;
        bottom: 0 !important;
        overflow-x: hidden !important;
        overflow-y: auto !important;
    }

    /* Supprime le scroll et margin en bas */
    body,
    html {
        overflow-x: hidden !important;
        margin: 0 !important;
        padding: 0 !important;
        width: 100vw !important;
        height: 100vh !important;
    }

    /* Box login plein écran sur mobile - TRANSPARENTE */
    .pf-c-login .pf-c-login__container,
    body .pf-c-login .pf-c-login__container,
    body:has(.pf-c-login) .pf-c-login__container {
        width: 100% !important;
        max-width: 100% !important;
        min-width: 100% !important;
        box-sizing: border-box !important;

        /* Mobile Centering Fix: Stop forcing full height/fixed pos */
        height: auto !important;
        min-height: 0 !important;
        position: relative !important;
        margin: auto !important;
        /* Enable centering */

        border-radius: 0 !important;
        border: none !important;
        padding: 20px !important;

        background: transparent !important;
        backdrop-filter: none !important;
        -webkit-backdrop-filter: none !important;
        box-shadow: none !important;

        /* Reset Fixed positioning props */
        top: auto !important;
        left: auto !important;
        right: auto !important;
        bottom: auto !important;

        z-index: 1 !important;
        overflow-y: visible !important;
    }

    .pf-c-login .pf-c-login__main,
    body .pf-c-login .pf-c-login__main,
    body:has(.pf-c-login) .pf-c-login__main {
        width: 100% !important;
        max-width: 100% !important;
        box-sizing: border-box !important;
        border-radius: 0 !important;
        border: none !important;
        padding: 30px 20px !important;
        min-height: auto !important;
        max-height: none !important;
        /* Complètement transparent - juste le contenu */
        background: transparent !important;
        backdrop-filter: none !important;
        -webkit-backdrop-filter: none !important;
        box-shadow: none !important;
        position: relative !important;
        z-index: 2 !important;

        /* FIX: Reset potential PatternFly margins that push it down */
        margin: auto !important;
        margin-top: auto !important;
        margin-bottom: auto !important;
    }

    /* Supprime les espaces du footer */
    .pf-c-login__footer,
    .pf-c-login__main-footer {
        padding-bottom: 0 !important;
        margin-bottom: 0 !important;
    }

    /* Boutons plus grands sur mobile */
    .pf-c-login .pf-c-form__actions button:first-child,
    .pf-c-login .pf-c-form__group-control button:first-child,
    :host(ak-stage-identification) .pf-c-button.pf-m-primary,
    :host(ak-stage-identification) button[type="submit"].pf-c-button.pf-m-primary {
        width: 100% !important;
        padding: 16px 24px !important;
        font-size: 16px !important;
    }

    /* Inputs plus grands */
    :host(ak-stage-identification) .pf-c-form-control,
    :host(ak-stage-password) .pf-c-form-control,
    :host(ak-stage-identification) input:not([type="checkbox"]),
    :host(ak-stage-password) input:not([type="checkbox"]) {
        padding: 14px 16px !important;
        font-size: 16px !important;
        min-height: 50px !important;
    }

    /* Boutons sociaux alignés horizontalement (max 4 par ligne) */
    .pf-c-login__main-footer-links {
        flex-direction: row !important;
        flex-wrap: wrap !important;
        justify-content: center !important;
        gap: 10px !important;
    }





    .pf-c-login__main-footer-links-item button {
        /* Compact buttons for row of 4 */
        width: auto !important;
        min-width: 60px !important;
        /* Ensure touch target */
        flex: 1 1 auto !important;
        /* Grow slightly to fill row */
        max-width: 80px !important;
        /* Don't get too big */

        font-size: 15px !important;
        padding: 10px 10px !important;
        /* Reduced padding */
    }

    .pf-c-login__main-footer-links-item button img {
        width: 18px !important;
        height: 18px !important;
    }

    /* Overlay de chargement plein écran sur mobile */
    ak-loading-overlay {
        border-radius: 0 !important;
    }

    /* ========================================= */
    /* === FIX DASHBOARD MOBILE (Background) === */
    /* ========================================= */

    /* ========================================= */
    /* === FIX DASHBOARD MOBILE (Background) === */
    /* ========================================= */

    /* ========================================= */
    /* === FIX DASHBOARD MOBILE (Background) === */
    /* ========================================= */

    /* Force l'image de fond visible sur html/body/dashboard */
    html,
    body,
    :host(ak-interface-user),
    :host(ak-library-impl),
    .pf-c-page {
        background-color: #151515 !important;
        /* fallback */
        background-image: var(--ak-flow-background) !important;
        background-size: cover !important;
        background-position: center !important;
        background-repeat: no-repeat !important;
        /* iOS Fix: Mobile scroll behavior to avoid jitter/locks */
        background-attachment: scroll !important;
        min-height: 100dvh !important;
        position: relative !important;
    }

    /* Fix Login Container Padding on Mobile */
    .pf-c-login .pf-c-login__container {
        padding: 10px 15px 20px 15px !important;
        min-width: auto !important;
        width: 100% !important;
        border: none !important;
        box-shadow: none !important;
    }

    .pf-c-login .pf-c-login__main {
        padding: 25px 20px !important;
        width: 100% !important;
        border-radius: 20px !important;
    }

    /* Mais garde la transparence sur les conteneurs intermédiaires */
    :host(ak-library-impl) .pf-c-page,
    :host(ak-library-impl) .pf-c-page__main,
    .pf-c-page__main,
    .pf-c-page__main-section,
    .pf-c-page__drawer,
    .pf-c-drawer__content,
    .pf-c-drawer__main,
    :host(ak-library-impl) .pf-l-gallery {
        background: transparent !important;
        background-color: transparent !important;
    }

    /* ========================================= */
    /* === FIX DASHBOARD MOBILE (Grid 2 cols) == */
    /* ========================================= */

    /* Force 2 colonnes pour la grille d'apps sur mobile */
    :host(ak-library-impl) .pf-l-gallery {
        display: grid !important;
        grid-template-columns: repeat(2, 1fr) !important;
        gap: 10px !important;
    }

    /* Ajuste la taille des cartes pour qu'elles rentrent */
    :host(ak-library-impl) .pf-c-card.pf-m-hoverable {
        min-height: 120px !important;
        padding: 10px !important;
        width: 100% !important;
        max-width: 100% !important;
    }

    /* Réduit un peu la taille de l'icône dans la carte si nécessaire */
    :host(ak-library-impl) .pf-c-card__body img {
        max-width: 40px !important;
        max-height: 40px !important;
    }
}

:host(ak-interface-user) > div > div > div {
  background: transparent !important;
  background-color: transparent !important;
}

Custom CSS Theme ALT

custom.css (zb Farben ändern oder Hintergrund auf allen Flows)

Quelle: https://github.com/goauthentik/authentik/discussions/4831

docker-compose.yml:

# docker-compose.yml bei server: und worker:
...
    volumes:
      - media:/media
      - custom-templates:/templates#   
      - /mnt/praxis-volume-01/docker-data/volumes/authentik_media/_data/public/praxis/custom.css:/web/dist/custom.css
      # oder
      - ./my-css-file.css:/web/dist/custom.css
    env_file:
...

custom.css:

/*** global ***/
:root {
  --ak-accent: #344c4a;
  --pf-global--Color--100: #5f7271;
  --pf-global--Color--200: #5f7271;
  --pf-global--primary-color--100: #8DAAA8;
  --pf-global--primary-color--200: #5f7271;
  --pf-global--primary-color--400: var(--ak-accent);
}

.pf-c-background-image {
  /*--ak-flow-background: url(https://picsum.photos/1920/1080) !important;*/
  --ak-flow-background: url("/media/public/praxis/dreamy_dark.png") !important;
}

.pf-c-login__main,
.pf-c-login__footer .pf-c-list,
.pf-c-page__sidebar {
  background-color: transparent !important;
  /* background-color: #2e2e2e6b !important; */
  backdrop-filter: blur(25px);
}

.pf-c-login__main::before,
.pf-c-login__footer .pf-c-list::before,
.pf-c-page__sidebar::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  z-index: -1;
  background-color: rgba(245, 0, 0, 1.0);
  opacity: 0.1;
}


/*** flows ***/
.pf-c-background-image {
  --pf-c-background-image--Filter: none;
}

.ak-login-container {
  /* fix #4830 */
  justify-content: initial;
}

.pf-c-login__header {
  margin-top: 10px;
}

/* give both the blur and color same radius */
.pf-c-login__main,
.pf-c-login__main::before {
  border-radius: 3px 3px 0 0;
}

.pf-c-login__footer .pf-c-list,
.pf-c-login__footer .pf-c-list::before {
  border-radius: 0 0 3px 3px;
}

.pf-c-login__main::before,
.pf-c-login__footer .pf-c-list::before {
  background-color: var(--pf-c-login__main--BackgroundColor);
}

.pf-c-login__footer {
  /* unset inverted colorscheme on footer since we're adding bg */
  --pf-global--Color--100: inherit;
  --pf-global--Color--200: inherit;
  --pf-global--BorderColor--100: inherit;
  --pf-global--primary-color--100: inherit;
  --pf-global--link--Color: inherit;
  --pf-global--link--Color--hover: inherit;
  --pf-global--BackgroundColor--100: inherit;
  display: none;
  /* For removing footer with Powered by Authentik link */
}

.pf-c-login__footer .pf-c-list {
  padding-top: var(--pf-c-login__footer--c-list--PaddingTop);
  padding-bottom: var(--pf-c-login__footer--c-list--PaddingTop);
}

@media (prefers-color-scheme: dark) {

  .pf-c-login__main::before,
  .pf-c-login__footer .pf-c-list::before {
    background-color: var(--ak-dark-background);
  }
}


/*** user interface ***/
.header input {
  border-bottom-color: var(--pf-global--primary-color--100);
}


/*** admin interface ***/
.pf-c-page__sidebar {
  backdrop-filter: blur(10px);
}

.pf-c-page__sidebar::before {
  background-color: var(--pf-global--BackgroundColor--dark-300);
}

@media (prefers-color-scheme: dark) {
  .pf-c-page__sidebar::before {
    background-color: var(--ak-dark-background-light);
  }

  .pf-c-nav {
    background-color: transparent;
  }
}

.pf-c-nav__link.pf-m-current::after,
.pf-c-nav__link.pf-m-current:hover::after,
.pf-c-nav__item.pf-m-current:not(.pf-m-expanded) .pf-c-nav__link::after {
  --pf-c-nav__link--m-current--after--BorderColor: var(--ak-accent) !important;
}

Proxy Provider

nach Anleitung von https://geekscircuit.com/set-up-authentik-sso-with-nginx-proxy-manager/ 

Wenn man eine Seite mit Authentik sichern möchte, die kein Userlogin hat (zb gethomepage/homepage dashboard)

https://www.youtube.com/watch?v=Nh1qiqCYDt4 

image.png

1. Authentik Domain in authentik Embedded Outpost ändern

um die eigene Domain festzulegen. Ist zb wichtig im Falle von Emails und Links, die versendet werden.

Anwendungen > Outpost > authentik Embedded Outpost > bearbeiten > Erweiterte Einstellungen > Konfiguration > authentik_host auf eigene Domain umstellen:

image.png

2. Proxy Provider erstellen 

Applications > Provider > new Proxy Provider > Forward Auth (single application) > externer host eingeben

image.png

3. Anwendung mit dem erstellten Provider erstellen

...

danach weiter wie beim 500 Internal Server Error vorgehen, also NPM mit custom configuration und alle Container in gleiches Docker-Network legen:

500 Internal Server Error

Lösung auf github gepostet: https://github.com/vineethmn/geekscomments/issues/1#issuecomment-2436487499 

I want to share my solution with getting rid the 500 internal server error because i did not find it anywhere online (i found out randomly):

#### TLDR:
- add NPM and Authentik to the same docker network
- use the Authentik server dockercontainer INTERNAL Port in NPM > advanced > custom config and not the one you exposed (when you chose to use for example 9006:9000 in your docker-compose.yml then redirect to 9000 anyway. I dont know why 9006 is not working)

#### LDR:

First I added the NPM and Authentik containers in the same docker network and added a hostname in my 

authentik docker-compose.yml
...
  server:
    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.0}
    hostname: authentik_server
    ports:
      - 9006:9000
      - 9446:9443
    networks:
      - reverseproxy_network
    volumes:
...
  postgresql:
    image: docker.io/library/postgres:16-alpine
    networks:
      - reverseproxy_network
    volumes:
...

networks:
  reverseproxy_network:
      name: reverseproxy_network
      driver: bridge

and 


npm docker-compose.yml
services:
  app:
    image: 'jc21/nginx-proxy-manager:latest'
    restart: always
    ports:
      - '80:80'
      - '81:81'
      - '443:443'
    volumes:
      - data:/data
      - letsencrypt:/etc/letsencrypt
    networks:
      - reverseproxy_network

volumes:
  data:
  letsencrypt:

networks:
  reverseproxy_network:
      name: reverseproxy_network
      driver: bridge

after that i used the NPM > Proxy Host > Advanced > Custom Nginx Configuration config by 
https://geekscircuit.com/set-up-authentik-sso-with-nginx-proxy-manager 
for my homepage proxy host: 
(you only need to change proxy_pass if your authentik-server hostname does not match with mine 'hostname: authentik_server')

image.png

custom nginx configuration
# Increase buffer size for large headers
# This is needed only if you get 'upstream sent too big header while reading response
# header from upstream' error when trying to access an application protected by goauthentik
proxy_buffers 8 16k;
proxy_buffer_size 32k;

location / {
    # Put your proxy_pass to your application here
    proxy_pass          $forward_scheme://$server:$port;

    # authentik-specific config
    auth_request        /outpost.goauthentik.io/auth/nginx;
    error_page          401 = @goauthentik_proxy_signin;
    auth_request_set $auth_cookie $upstream_http_set_cookie;
    add_header Set-Cookie $auth_cookie;

    # translate headers from the outposts back to the actual upstream
    auth_request_set $authentik_username $upstream_http_x_authentik_username;
    auth_request_set $authentik_groups $upstream_http_x_authentik_groups;
    auth_request_set $authentik_email $upstream_http_x_authentik_email;
    auth_request_set $authentik_name $upstream_http_x_authentik_name;
    auth_request_set $authentik_uid $upstream_http_x_authentik_uid;

    proxy_set_header X-authentik-username $authentik_username;
    proxy_set_header X-authentik-groups $authentik_groups;
    proxy_set_header X-authentik-email $authentik_email;
    proxy_set_header X-authentik-name $authentik_name;
    proxy_set_header X-authentik-uid $authentik_uid;
}

# all requests to /outpost.goauthentik.io must be accessible without authentication
location /outpost.goauthentik.io {
    proxy_pass          http://authentik_server:9000/outpost.goauthentik.io; # <<<< CHANGE HERE <<<<
    # ensure the host of this vserver matches your external URL you've configured
    # in authentik
    proxy_set_header    Host $host;
    proxy_set_header    X-Original-URL $scheme://$http_host$request_uri;
    add_header          Set-Cookie $auth_cookie;
    auth_request_set    $auth_cookie $upstream_http_set_cookie;

    # required for POST requests to work
    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
}

# Special location for when the /auth endpoint returns a 401,
# redirect to the /start URL which initiates SSO
location @goauthentik_proxy_signin {
    internal;
    add_header Set-Cookie $auth_cookie;
    return 302 /outpost.goauthentik.io/start?rd=$request_uri;
    # For domain level, use the below error_page to redirect to your authentik server with the full redirect path
    # return 302 https://authentik.company/outpost.goauthentik.io/start?rd=$scheme://$http_host$request_uri;
}


i also did not have to change anything on my authentik outpost:

image.png

Andere mit dem gleichen Problem:

https://geekscircuit.com/set-up-authentik-sso-with-nginx-proxy-manager/
https://github.com/goauthentik/authentik/issues/10010
https://www.reddit.com/r/selfhosted/comments/vs12ug/sso_with_authentik_and_nginx_proxy_manager/?tl=de
https://www.youtube.com/watch?v=Nh1qiqCYDt4&list=PLH73rprBo7vSkDq-hAuXOoXx2es-1ExOP 

Einladungen per Mail

User-Enrollment kann mit mit Flows aus dem Anhang dieses Artikels realisiert werden.

Quelle: https://www.youtube.com/watch?v=mGOTpRfulfQ 

https://docs.goauthentik.io/docs/add-secure-apps/flows-stages/flow/examples/flows (Enrollment-2-Stage.yaml in Abläufe importieren)

Flows

image.png

image.png

^ auf Reiehnfolge der Fields achten, damit username, Name, Email, Pw, pw nochmal richtig sortiert sind

image.png

Phasen

image.png

image.png

Danach auf Verzeichnis > Einladungen > Erstellen klicken und mit dem Flow einen Einladungslink erzeugen:

image.png

den Eintrag ausklappen, um den Link zu kopieren:

image.png

Docker-Compose .yamls

Netbird (VPN IT Infrastructure mit Wireguard und authentik)

Als Client am Beispiel mit Paperless-AI:

services:
  paperless-ai:
    image: clusterzx/paperless-ai:3.0.9
    container_name: paperless-ai
    #network_mode: host
    depends_on:
      - netbird
    restart: unless-stopped
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges=true
    environment:
      - PUID=1000
      - PGID=1000
      - PAPERLESS_AI_PORT=${PAPERLESS_AI_PORT:-3000}
      - RAG_SERVICE_URL=http://localhost:8000
      - RAG_SERVICE_ENABLED=true
      - PAPERLESS_URL=https://paperless-ai.MEINEDOMAIN.de
    ports:
      - "3057:${PAPERLESS_AI_PORT:-3000}"
    volumes:
      - data:/app/data

  netbird:
    image: netbirdio/netbird:latest
    container_name: paperless-ai-netbird
    hostname: fn-paperless-ai
    privileged: true
    #network_mode: host
    cap_add:
      - NET_ADMIN
      - SYS_ADMIN
      - SYS_RESOURCE
    volumes:
      #- /var/run/netbird:/var/run/netbird
      - nb-var-lib:/var/lib/netbird
      - nb-cfg:/etc/netbird
    environment:
      - NB_SETUP_KEY=FFE.........AA49
    restart: unless-stopped

volumes:
  data:
  nb-cfg:
  nb-var-lib:

Falls netbird up nicht richtig connectet:

sudo systemctl stop netbird
sudo systemctl disable netbird

sudo rm -rf /usr/local/bin/netbird
sudo rm -rf /etc/netbird
sudo rm -rf /var/lib/netbird

sudo apt remove netbird -y

curl -fsSL https://pkgs.netbird.io/install.sh | sudo bash
sudo apt update
sudo apt install netbird -y

# netbird up --allow-server-ssh --enable-ssh-root --setup-key ABC-123-DEF-456

ohne sudo

systemctl stop netbird
systemctl disable netbird

rm -rf /usr/local/bin/netbird
rm -rf /etc/netbird
rm -rf /var/lib/netbird

apt remove netbird -y

curl -fsSL https://pkgs.netbird.io/install.sh | bash
apt update
apt install netbird -y

# netbird up
# netbird up --allow-server-ssh --enable-ssh-root --setup-key ABC-123-DEF-456

Troubleshooting

Fremdes Subnetz nicht erreichbar

Falls eine IP-Adresse aus einem anderen Subnetz nicht erreichbar ist, kann es daran liegen, dass eine Docker-Bridge mit diesem Subnetz vorhanden ist.

Beispiel: Von einem Hetzner Server möchte ich das Subnetz 192.168.5.0/24 erreichen. Der Ping klappt nicht. 
Debuggen mit

ip route get 192.168.5.55

ergibt zb
root@fn-01:~# ip route get 192.168.5.55
192.168.5.55 dev br-ebe13ac6d23b src 192.168.0.1 uid 0 cache

weiteres debugging:

# Netbird Status
netbird status --detail

# Routing-Tabelle analysieren
ip route show
ip route get 192.168.5.55
# ergibt zb
# root@fn-01:~# ip route get 192.168.5.55
# 192.168.5.55 dev br-ebe13ac6d23b src 192.168.0.1 uid 0 cache
# ^^^^ DA IST DER FEHLER, die Docker-Netzwerk-Bridge br-ebe13ac6d23b sorgt für das Routing und schlägt damit fehl

# Netbird Interface prüfen
ip addr show wt0
sudo iptables -L -v -n | grep -A 10 wt0

# Netbird logs
sudo journalctl -u netbird -f
sudo systemctl status netbird

# Ping mit Details
ping -I wt0 192.168.5.55
traceroute 192.168.5.55
docker network ls

ergibt 

root@fn-01:~# docker network ls
NETWORK ID     NAME                        DRIVER    SCOPE
396e4a7a02a8   backrest_default            bridge    local
9f3a5b5f9017   beszel_default              bridge    local
7ff0d869b3a9   bitwarden_default           bridge    local
944ac8eea6b1   bridge                      bridge    local
8a7ecea723cd   bs_default                  bridge    local
56847cda68c7   heimdall_default            bridge    local
23c9d0945209   host                        host      local
dc6107d2a414   nextcloud-aio               bridge    local
10cf7e670c62   nextcloud_default           bridge    local
4918f3559a98   nginx_lowqart_default       bridge    local
1e7aaa5ae6b2   none                        null      local
6703ef3d8a62   ollama_default              bridge    local
15659627c007   paperless-ai_default        bridge    local
f02f912a0898   paperless_default           bridge    local
e29d199145fc   reverseproxy_network        bridge    local
9e294e453ecb   semaphore_default           bridge    local
f1dde6f30b1f   shlink_default              bridge    local
c7750a87eef4   syncthing_default           bridge    local
f0e08f4a4c7c   uptimekuma_default          bridge    local
c85d048db1b1   vikunja_default             bridge    local
57cd332f96b1   windmill_default            bridge    local
ebe13ac6d23b   zerobyte_default            bridge    local
# ^^^ fehlerhafte bridge
docker network inspect ebe13ac6d23b

ergebnis: die docker network bridge nimmt 192.168.0.0/20 als subnet, was 192.168.5.0/24 inkludiert. 

lösung für die Problem-Bridge:

docker-compose von zb zerobyte

...

      - /var/lib/docker:/backup-var-lib
      - /mnt/fn-volume-01:/backup-fn-volume-01
    networks:
      - zerobyte_network # <<< wichtig

volumes:
  app:

networks:
  zerobyte_network:
    driver: bridge
    ipam:
      config:
        - subnet: 172.69.19.0/24
          gateway: 172.69.19.1

danach wird die alte bridge mit 192.168.0.0/20 gelöscht und eine neue mit subnet 172.69.19.0/24 erstellt.

Dauerhafte lösung: siehe https://wiki.folkerts.it/books/docker/page/ip-kollidierung-bei-netbird-verhindern-192168x-und-101x

nano /etc/docker/daemon.json

{
  "default-address-pools": [
    {
      "base": "172.16.0.0/12",
      "size": 20
    }
  ],
  "data-root": "/mnt/fn-volume-01/docker-data",
  "log-opts": {
    "max-size": "100m",
    "max-file": "5"
  }
}
sudo systemctl restart docker

Netbird Cloud auf Unifi-Gateway installieren

https://git.shivering-isles.com/-/snippets/22 

install.sh

#!/bin/bash

set -e

if test $(ubnt-device-info firmware) \< "3.0.0"; then
    echo "Try the other UDM setup script for 1.x: https://git.shivering-isles.com/-/snippets/19" >&2
    exit 1
fi

mkdir -p /data/netbird

cd /data/netbird

if [[ -e ./netbird ]]; then
    mv ./netbird ./netbird.old
fi

NETBIRD_VERSION="$(curl -s https://api.github.com/repos/netbirdio/netbird/releases/latest | jq -r ".tag_name")"
curl -L https://github.com/netbirdio/netbird/releases/download/${NETBIRD_VERSION}/netbird_${NETBIRD_VERSION//v}_linux_arm64.tar.gz | tar xvzf -

./netbird service install || true # ignore error since it'll fail if it's already installed

# Due to outdated kernels by unifi, this is required otherwise newer versions of netbird fail to start
mkdir -p /etc/systemd/system/netbird.service.d/
cat >/etc/systemd/system/netbird.service.d/legacy.conf <<EOF
[Service]
Environment="NB_USE_LEGACY_ROUTING=true"
Environment="NB_DISABLE_CUSTOM_ROUTING=true"
EOF

systemctl daemon-reload

if systemctl is-active netbird.service; then
    systemctl restart netbird.service
fi

if ! systemctl is-enabled netbird.service; then
    systemctl enable --now netbird.service
fi

danach

chmod +x install.sh
./install.sh

curl -fsSL https://pkgs.netbird.io/install.sh | sh

/data/netbird/netbird -k <setup key> up

Netbird als Management-Server selbst hosten

Dieses Tutorial funktioniert noch nicht zu 100%. Ich bleibe immer bei netbird.domain.de/peers mit Ladeloop hängen

https://github.com/netbirdio/netbird/issues/3110 Client failed to connect to Self-Hosted NetBird server: failed while getting Management Service public key

https://github.com/netbirdio/netbird/issues/3007 Stuck on loading screen on "/peers" (Authentik)

https://github.com/netbirdio/netbird/issues/3007#issuecomment-2764264829 < hat geholfen

https://github.com/netbirdio/netbird/issues/3007#issuecomment-2564843380 < nginx-pm cfg

https://github.com/netbirdio/netbird/issues/2941 Request failed with status code 401 (Authentik) < scope api access & redirects

https://github.com/netbirdio/netbird/issues/2515 Unable to authenticate with Authentik SSO

https://github.com/netbirdio/netbird/issues/2510 Netbird with NGiNX Proxy Manager and Authentik

https://github.com/netbirdio/netbird/issues/2338 Can't access dashboard - Token Invalid, Authentik

https://github.com/netbirdio/netbird/issues/2043 error: failed while getting Management Service public key

https://github.com/netbirdio/netbird/issues/2043#issuecomment-2384470230 < nginx-pm cfg

https://github.com/netbirdio/netbird/issues/1962 netbird dashboard does not open properly

https://github.com/netbirdio/netbird/issues/1742 NGINX reverse proxy question

https://github.com/netbirdio/netbird/issues/1250 Authentik login not working: Login Error: User state: Unauthenticated

https://github.com/netbirdio/netbird/issues/536 Run netbird behind reverse proxy

https://docs.netbird.io/selfhosted/selfhosted-guide#step-2-prepare-configuration-files

https://docs.netbird.io/selfhosted/identity-providers#authentik

image.png

Folge dieser Anleitung: https://docs.netbird.io/selfhosted/selfhosted-guide 
Es wird ein Skript zur Verfügung gestellt, mit dem man eine docker-compose.yml nach eigenen Wünschen aus template Dateien erzeugen kann.
Anleitung ganz genau lesen!
VIDEO DAZU: https://www.youtube.com/watch?v=QQaRB1vL6Q8 

Vorschlag für NGINX Proxy Manager Advanced cfg aus gh issue https://github.com/netbirdio/netbird/issues/3110#issuecomment-2567362588 

# This is necessary so that grpc connections do not get closed early
# see https://stackoverflow.com/a/67805465
client_header_timeout 1d;
client_body_timeout 1d;

proxy_set_header        X-Real-IP $remote_addr;
proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header        X-Scheme $scheme;
proxy_set_header        X-Forwarded-Proto https;
proxy_set_header        X-Forwarded-Host $host;
grpc_set_header         X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header       Authorization $http_authorization;
grpc_set_header         Authorization $http_authorization;

# Proxy dashboard
location / {
    proxy_pass http://nb-dashboard:80;
}
# Proxy Signal
location /signalexchange.SignalExchange/ {
    grpc_pass grpc://nb-signal:80;
    grpc_set_header         Authorization $http_authorization;
    grpc_ssl_verify off;
    grpc_read_timeout 1d;
    grpc_send_timeout 1d;
    grpc_socket_keepalive on;
}
# Proxy Management http endpoint
location /api {
    proxy_pass http://nb-management:443;
}
# Proxy Management grpc endpoint
location /management.ManagementService/ {
    grpc_pass grpc://nb-management:443;
    grpc_set_header         Authorization $http_authorization;
    grpc_ssl_verify off;
    grpc_read_timeout 1d;
    grpc_send_timeout 1d;
    grpc_socket_keepalive on;
}

Docker-Compose .yamls

Collabora (Nextcloud Office)

services:
  collabora:
    image: collabora/code:24.04.9.2.1
    container_name: collabora
    environment:
      - aliasgroup1=https://nextcloud.DOMAIN.de
      - aliasgroup2=https://another.DOMAIN.de
      - aliasgroup3=https://another.DOMAIN.de
      #- server_name=collabora.DOMAIN.de
      - username=MYUSERNAME
      - password=MYPASSWORD
    ports:
      - '9980:9980'
    restart: unless-stopped

Reverse Proxy Einstellung NPM

image.png

image.png

danach http://DOCKERHOST:9980/browser/dist/admin/admin.html 
oder     https://collabora.DOMAIN.de/browser/dist/admin/admin.html nach reverse-proxy aktivierung

dann in der Nextcloud als App "Nextcloud Office" installieren: https://apps.nextcloud.com/apps/richdocuments
und dann unter Profilbild > Administratoreinstellungen > Verwaltung (linke Sidebar) > Office (die Domain dafuer ist zb https://nextcloud.DOMAIN.de/settings/admin/richdocuments ) die Collabora-URL eingeben

 

Quellen:

https://goneuland.de/collabora-office-online-mit-docker-compose-und-traefik-installieren/

https://www.erikdonner.dev/2024/02/08/online-office-collabora-fuer-nextcloud-mit-docker/

Docker-Compose .yamls

Outline (Wiki, Confluence Alternative)

outline docker-compose.yaml 

https://docs.getoutline.com/s/hosting/doc/docker-7pfeLP5a8t 

version: "3.2"
services:

  outline:
    restart: unless-stopped
    image: outlinewiki/outline:1.4.0
    env_file: stack.env
    ports:
      - "3000:3000"
    volumes:
      - storage:/var/lib/outline/data
    depends_on:
      - postgres
      - redis

  redis:
    restart: unless-stopped
    image: redis
    env_file: stack.env
    ports:
      - "6379:6379"
    volumes:
      - ./redis.conf:/redis.conf
    command: ["redis-server", "/redis.conf"]
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 30s
      retries: 3

  postgres:
    restart: unless-stopped
    image: postgres:18
    env_file: stack.env
    ports:
      - "5432:5432"
    volumes:
      - db:/var/lib/postgresql
    healthcheck:
      test: ["CMD", "pg_isready", "-d", "outline", "-U", "user"]
      interval: 30s
      timeout: 20s
      retries: 3
    environment:
      POSTGRES_USER: 'user'
      POSTGRES_PASSWORD: 'pass'
      POSTGRES_DB: 'outline'

#  https-portal:
#    image: steveltn/https-portal
#    env_file: stack.env
#    ports:
#      - '80:80'
#      - '443:443'
#    links:
#      - outline
#    restart: always
#    volumes:
#      - https-portal-data:/var/lib/https-portal
#    healthcheck:
#      test: ["CMD", "service", "nginx", "status"]
#      interval: 30s
#      timeout: 20s
#      retries: 3
#    environment:
#      DOMAINS: 'docs.mycompany.com -> http://outline:3000'
#      STAGE: 'production'
#      WEBSOCKET: 'true'
#      CLIENT_MAX_BODY_SIZE: '0'

volumes:
  #https-portal-data:
  storage:
  db:

stack.env (env-vars in portainer immer mit stack.env einbinden (siehe oben))

.env mit Authentik als OIDC

APP_NAME='MEIN WIKI'
NODE_ENV=production
SECRET_KEY=040..........65dba72 (openssl rand -hex 32 oder openssl rand 24 -base64)
UTILS_SECRET=7367ab...........e5e9ca (openssl rand -hex 32 oder openssl rand 24 -base64)
DATABASE_URL=postgres://pladmin:gi5ll.........tch@postgres:5432/outline #< PW AUS docker-compose.yml ÜBERNEHMEN!
PGSSLMODE=disable
POSTGRES_USER='pladmin'
POSTGRES_PASSWORD='gi.......tch'
POSTGRES_DB='outline'
REDIS_URL=redis://redis:6379
URL=https://wiki.MEINEDOMAIN.DE
PORT=3000
COLLABORATION_URL=
FILE_STORAGE=local
FILE_STORAGE_LOCAL_ROOT_DIR=/var/lib/outline/data
FILE_STORAGE_UPLOAD_MAX_SIZE=262144000
FORCE_HTTPS=true
ENABLE_UPDATES=true
WEB_CONCURRENCY=1
DEBUG=http
LOG_LEVEL=info
OIDC_CLIENT_ID=v7xa.......8L6T8
OIDC_CLIENT_SECRET=uSVUg...............................pVFVtuxYSJ7QPBNNN44S2LnLjMMxQxcZ8jjEj 
OIDC_AUTH_URI=https://auth.MEINEDOMAIN.DE/application/o/authorize/
OIDC_TOKEN_URI=https://auth.MEINEDOMAIN.DE/application/o/token/
OIDC_USERINFO_URI=https://auth.MEINEDOMAIN.DE/application/o/userinfo/
OIDC_LOGOUT_URI=https://auth.MEINEDOMAIN.DE/application/o/wiki/end-session/
OIDC_USERNAME_CLAIM=preferred_username
OIDC_DISPLAY_NAME=authentik
OIDC_SCOPES=openid profile email
DEFAULT_LANGUAGE=de_DE
SMTP_HOST=smtp.strato.de
SMTP_PORT=465
SMTP_USERNAME=info@MEINEDOMAIN.DE
SMTP_PASSWORD=MEIN........MAILPW
SMTP_FROM_EMAIL=info@MEINEDOMAIN.DE
SMTP_SECURE=true

Gruppen zwischen Outline und Authentik synchronisieren

https://github.com/burritosoftware/Outline-Authentik-Connector 

aktuell ist das Docker-Image nur für amd64 und nicht für ARM, man kann aber auch einfach n8n für die Automation anlegen, wenn man die webhooks von outline übernimmt

siehe github repo für Anleitung

outline-authentik-connector docker-compose.yml 

name: outline-authentik-connector
services:
  outline-authentik-connector:
    image: burritosoftware/outline-authentik-connector:1.1
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - 8430:80
    env_file: "stack.env"

.env

AUTHENTIK_URL=https://auth.MEINEDOMAIN.DE
AUTHENTIK_TOKEN=8lnL6Ke7u.................t7vmVYvEPJoe
OUTLINE_URL=https://wiki.MEINEDOMAIN.DE
OUTLINE_TOKEN=ol_api_UWBWl..................cKtMLIopMebfTks7
OUTLINE_WEBHOOK_SECRET=ol_whs_kMP....................1y3ORBL6E2K
DEBUG=False
# Seit v1.1 werden Gruppen mit dieser var automatisch hinzugefügt, wenn sie in Outline fehlen:
AUTO_CREATE_GROUPS=True

Anhang

sample.env für alle outline settings https://github.com/outline/outline/blob/main/.env.sample 

NODE_ENV=production

# This URL should point to the fully qualified, publicly accessible, URL. If using a
# proxy this will be the proxy's URL.
URL=

# The port to expose the Outline server on, this should match what is configured
# in your docker-compose.yml
PORT=3000

# See [documentation](docs/SERVICES.md) on running a separate collaboration
# server, for normal operation this does not need to be set.
COLLABORATION_URL=

# If using a Cloudfront/Cloudflare distribution or similar it can be set below.
# This will cause paths to javascript, stylesheets, and images to be updated to
# the hostname defined in CDN_URL. In your CDN configuration the origin server
# should be set to the same as URL.
CDN_URL=

# How many processes should be spawned. As a reasonable rule divide your servers
# available memory by 512 for a rough estimate
WEB_CONCURRENCY=1

# Generate a hex-encoded 32-byte random key. Use `openssl rand -hex 32` in your
# terminal to generate a random value.
SECRET_KEY=generate_a_new_key

# Generate a unique random key. The format is not important but you could still use
# `openssl rand -hex 32` in your terminal to generate a random value.
UTILS_SECRET=generate_a_new_key

# The default interface language. See translate.getoutline.com for a list of
# available language codes and their rough percentage translated.
DEFAULT_LANGUAGE=en_US


# ––––––––––––––––––––––––––––––––––––––
# –––––––––––––  DATABASE  –––––––––––––
# ––––––––––––––––––––––––––––––––––––––

# The database URL for your production database, including username, password, and database name.
DATABASE_URL=postgres://user:pass@postgres:5432/outline

# The in-memory database pool per-process settings. Ensure that the pool size that will not exceed
# the maximum number of connections allowed by your database. Defaults to 0 and 5.
DATABASE_CONNECTION_POOL_MIN=
DATABASE_CONNECTION_POOL_MAX=

# Uncomment this line if you will not use SSL for connecting to Postgres. This is acceptable
# if the database and the application are on the same machine.
# PGSSLMODE=disable


# ––––––––––––––––––––––––––––––––––––––
# ––––––––––––––  REDIS  –––––––––––––––
# ––––––––––––––––––––––––––––––––––––––

# The Redis URL for your environment you can either specify an ioredis compatible url or a Base64
# encoded configuration object.
# DOCS: https://docs.getoutline.com/s/hosting/doc/redis-LGM4BFXYp4
REDIS_URL=redis://redis:6379

# To enable horizontal scaling of the collaboration service you must provide a Redis URL, it may
# be the same as above, or a different server.
# DOCS: https://docs.getoutline.com/s/hosting/doc/horizontal-scaling-hkfU5Stao7
REDIS_COLLABORATION_URL=


# ––––––––––––––––––––––––––––––––––––––
# –––––––––––  FILE STORAGE  –––––––––––
# ––––––––––––––––––––––––––––––––––––––

# Specify what storage system to use. Possible value is one of "s3" or "local".
# For "local" images and document attachments will be saved on local disk, for "s3" they
# will be stored in an S3-compatible network store.
# DOCS: https://docs.getoutline.com/s/hosting/doc/file-storage-N4M0T6Ypu7
FILE_STORAGE=local

# If "local" is configured for FILE_STORAGE above, then this sets the parent directory under
# which all attachments/images are stored. Make sure that the process has permissions to
# create this path and also to write files to it.
FILE_STORAGE_LOCAL_ROOT_DIR=/var/lib/outline/data

# Maximum allowed size for the uploaded attachment.
FILE_STORAGE_UPLOAD_MAX_SIZE=262144000

# Override the maximum size of document imports, generally this should be lower
# than the document attachment maximum size.
FILE_STORAGE_IMPORT_MAX_SIZE=

# Override the maximum size of workspace imports, these can be especially large
# and the files are temporary being automatically deleted after a period of time.
FILE_STORAGE_WORKSPACE_IMPORT_MAX_SIZE=

# To support uploading of images for avatars and document attachments in a distributed
# architecture, an s3-compatible storage can be configured if FILE_STORAGE=s3 above.
AWS_ACCESS_KEY_ID=get_a_key_from_aws
AWS_SECRET_ACCESS_KEY=get_the_secret_of_above_key
AWS_REGION=xx-xxxx-x
AWS_S3_ACCELERATE_URL=
AWS_S3_UPLOAD_BUCKET_URL=http://s3:4569
AWS_S3_UPLOAD_BUCKET_NAME=bucket_name_here
AWS_S3_FORCE_PATH_STYLE=true
AWS_S3_ACL=private


# ––––––––––––––––––––––––––––––––––––––
# ––––––––––––––––  SSL  –––––––––––––––
# ––––––––––––––––––––––––––––––––––––––

# Base64 encoded private key and certificate for HTTPS termination. This is one
# of three ways to configure SSL and can be left empty.
# DOCS: https://docs.getoutline.com/s/hosting/doc/ssl-pzk7WO8d1n
SSL_KEY=
SSL_CERT=

# Auto-redirect to https in production. The default is true but you may set to
# false if you can be sure that SSL is terminated at an external loadbalancer.
FORCE_HTTPS=true


# ––––––––––––––––––––––––––––––––––––––
# ––––––––––  AUTHENTICATION  ––––––––––
# ––––––––––––––––––––––––––––––––––––––

# Third party signin credentials, at least ONE OF EITHER Google, Slack,
# Discord, or Microsoft is required for a working installation or you'll
# have no sign-in options.

# Slack sign-in provider
# DOCS: https://docs.getoutline.com/s/hosting/doc/slack-sgMujR8J9J
SLACK_CLIENT_ID=get_a_key_from_slack
SLACK_CLIENT_SECRET=get_the_secret_of_above_key

# Google sign-in provider
# DOCS: https://docs.getoutline.com/s/hosting/doc/google-hOuvtCmTqQ
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=

# Microsoft Entra / Azure AD sign-in provider
# DOCS: https://docs.getoutline.com/s/hosting/doc/microsoft-entra-UVz6jsIOcv
AZURE_CLIENT_ID=
AZURE_CLIENT_SECRET=
AZURE_RESOURCE_APP_ID=

# Discord sign-in provider
# DOCS: https://docs.getoutline.com/s/hosting/doc/discord-g4JdWFFub6
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
DISCORD_SERVER_ID=
DISCORD_SERVER_ROLES=

# Generic OIDC provider
# DOCS: https://docs.getoutline.com/s/hosting/doc/oidc-8CPBm6uC0I
OIDC_CLIENT_ID=
OIDC_CLIENT_SECRET=
OIDC_AUTH_URI=
OIDC_TOKEN_URI=
OIDC_USERINFO_URI=
OIDC_LOGOUT_URI=

# Specify which claims to derive user information from
# Supports any valid JSON path with the JWT payload
OIDC_USERNAME_CLAIM=preferred_username

# Display name for OIDC authentication
OIDC_DISPLAY_NAME=OpenID Connect

# Space separated auth scopes.
OIDC_SCOPES=openid profile email


# ––––––––––––––––––––––––––––––––––––––
# ––––––––––––––  EMAIL  –––––––––––––––
# ––––––––––––––––––––––––––––––––––––––

# To support sending outgoing transactional emails such as "document updated" or
# email sign-in you'll need to connect an SMTP server. Service can be configured
# with any service from this list: https://community.nodemailer.com/2-0-0-beta/setup-smtp/well-known-services/
# DOCS: https://docs.getoutline.com/s/hosting/doc/smtp-cqCJyZGMIB
SMTP_SERVICE=
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_FROM_EMAIL=


# ––––––––––––––––––––––––––––––––––––––
# ––––––––––  RATE LIMITER  ––––––––––––
# ––––––––––––––––––––––––––––––––––––––

# Whether the rate limiter is enabled or not
RATE_LIMITER_ENABLED=true

# Individual endpoints have hardcoded rate limits that are enabled
# with the above setting, however this is a global rate limiter
# across all requests
RATE_LIMITER_REQUESTS=1000
RATE_LIMITER_DURATION_WINDOW=60


# ––––––––––––––––––––––––––––––––––––––
# –––––––––––  INTEGRATIONS  –––––––––––
# ––––––––––––––––––––––––––––––––––––––

# GitHub integration allows previewing issue and pull request links
# DOCS: https://docs.getoutline.com/s/hosting/doc/github-GchT3NNxI9
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GITHUB_WEBHOOK_SECRET=
GITHUB_APP_NAME=
GITHUB_APP_ID=
GITHUB_APP_PRIVATE_KEY=

# Linear integration allows previewing issue links as rich mentions
LINEAR_CLIENT_ID=
LINEAR_CLIENT_SECRET=

# For a complete Slack integration with search and posting to channels the
# following configs are also needed in addition to Slack authentication:
# DOCS: https://docs.getoutline.com/s/hosting/doc/slack-G2mc8DOJHk
SLACK_VERIFICATION_TOKEN=your_token
SLACK_APP_ID=A0XXXXXXX
SLACK_MESSAGE_ACTIONS=true

# Figma integration allows previewing design files as rich mentions
FIGMA_CLIENT_ID=
FIGMA_CLIENT_SECRET=

# For Dropbox integration, follow these instructions to get the key https://www.dropbox.com/developers/embedder#setup
# and do not forget to whitelist your domain name in the app settings
DROPBOX_APP_KEY=

# Optionally enable Sentry (sentry.io) to track errors and performance,
# DOCS: https://docs.getoutline.com/s/hosting/doc/sentry-jxcFttcDl5
SENTRY_DSN=
SENTRY_TUNNEL=

# Enable importing pages from a Notion workspace
# DOCS: https://docs.getoutline.com/s/hosting/doc/notion-2v6g7WY3l3
NOTION_CLIENT_ID=
NOTION_CLIENT_SECRET=

# The Iframely integration allows previews of third-party content within Outline.
# For example, hovering over an external link will show a preview.
# DOCS: https://docs.getoutline.com/s/hosting/doc/iframely-HwLF1EZ9mo
IFRAMELY_URL=
IFRAMELY_API_KEY=


# ––––––––––––––––––––––––––––––––––––––
# –––––––––––––  DEBUGGING  ––––––––––––
# ––––––––––––––––––––––––––––––––––––––

# Have the installation check for updates by sending anonymized statistics to
# the maintainers
ENABLE_UPDATES=true

# Debugging categories to enable – you can remove the default "http" value if
# your proxy already logs incoming http requests and this ends up being duplicative
DEBUG=http

# Configure lowest severity level for server logs. Should be one of
# error, warn, info, http, verbose, debug, or silly
LOG_LEVEL=info

Docker-Compose .yamls

Vikunja

docker-compose.yml

version: '3'

services:
  vikunja:
    image: vikunja/vikunja:0.24.6
    environment:
      VIKUNJA_SERVICE_PUBLICURL: https://vikunja.MEINEDOMAIN.DE
      VIKUNJA_DATABASE_HOST: db
      VIKUNJA_DATABASE_PASSWORD: MEINDB-PW-123 (gleiches wie unten)
      VIKUNJA_DATABASE_TYPE: postgres
      VIKUNJA_DATABASE_USER: vikunja
      VIKUNJA_DATABASE_DATABASE: vikunja
      VIKUNJA_SERVICE_JWTSECRET: GCP.........bjd ("openssl rand 32 -base64" in shell eingeben)
      #EMAIL-Settings
      #VIKUNJA_MAILER_AUTHTYPE: plain
      #VIKUNJA_MAILER_SKIPTLSVERIFY: 1
      #VIKUNJA_MAILER_FORCESSL: 1
      VIKUNJA_SERVICE_ENABLEEMAILREMINDERS: 1
      VIKUNJA_MAILER_ENABLED: 1
      VIKUNJA_MAILER_HOST: smtp.MAILSERVER.DE
      VIKUNJA_MAILER_PORT: 587
      VIKUNJA_MAILER_USERNAME: USER@DOMAIN.DE
      VIKUNJA_MAILER_PASSWORD: MEINMAILPW123
      VIKUNJA_MAILER_FROMEMAIL: USER@DOMAIN.DE
      #Allow New User Registration (Für ersten User auf true setzen, User anlegen und danach wieder auf false)
      VIKUNJA_SERVICE_ENABLEREGISTRATION: true
      #JWT Token Expiration in Seconds (2592000 = 30 days, default is 3 days = 259200)
      VIKUNJA_SERVICE_JWTTTL: 2592000 
      #Defaultsettings  https://vikunja.io/docs/config-options/
      VIKUNJA_DEFAULTSETTINGS_EMAIL_REMINDERS_ENABLED: true
      VIKUNJA_DEFAULTSETTINGS_DISCOVERABLE_BY_NAME: true
      VIKUNJA_DEFAULTSETTINGS_DISCOVERABLE_BY_EMAIL: true
      VIKUNJA_DEFAULTSETTINGS_OVERDUE_TASKS_REMINDERS_ENABLED: true
      VIKUNJA_DEFAULTSETTINGS_WEEK_START: 1 #0=Sunday, 1=Monday, etc...
      VIKUNJA_DEFAULTSETTINGS_LANGUAGE: de-DE
      #Trello import
      VIKUNJA_MIGRATION_TRELLO_ENABLE: true
      VIKUNJA_MIGRATION_TRELLO_KEY: xxx
      #Prometheus Metrics (.../api/v1/metrics)
      VIKUNJA_METRICS_ENABLED: true
      #OIDC Usersearch
      VIKUNJA_SERVICE_ENABLEOPENIDTEAMUSERSEARCH: true
      #Weiteres
      VIKUNJA_SERVICE_CUSTOMLOGOURL: https://auth.MEINEAUTHENTIKDOMAIN.de/media/public/pr...ng/PL-Logo.png
    ports:
      - 3456:3456
    volumes:
      - app:/app/vikunja/files
      - config:/etc/vikunja/
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: MEINDB-PW-123 (gleiches wie oben)
      POSTGRES_USER: vikunja
    volumes:
      - db:/var/lib/postgresql/data
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -h localhost -U $$POSTGRES_USER"]
      interval: 2s

volumes:
  app:
  db:
  config:

Schreibrechte für files erteilen

(default ist root user mit uid 1000 für den container)

Für Schreibrechte muss man noch den root user 1000 für das App-Volume berechtigen:

chown 1000 $PWD/files

zb in ranger mit Shortcut s um ein Shell-cmd einzugeben:

image.png

sieht danach so aus:

image.png

Account anlegen

dann die subdomain anlegen, Reverserproxy konfigurieren und  domain aufrufen:

image.png

auf 'Account erstellen' und ersten bzw weitere Accounts erstellen.

Danach im Stack die env

VIKUNJA_SERVICE_ENABLEREGISTRATION=false

setzen, um weitere Anmeldungen zu verhindern.


Authentik verknüpfen

OBACHT: Mit dem aktuellsten unstable Release ändert sich die OIDC Syntax in der config.yml von Vikunja. Die alte Version für Vikunja 0.24.6 findest du weiter unten!

für die config.yml, das volume config: verwenden und dort eine config.yml anlegen:

image.png

mit dem inhalt:

ACHTUNG! SYNTAX IST FÜR NEUE UNSTABLE VERSION NACH 0.24.6! ALTE CONFIG WEITER UNTEN

# neue Syntax für unstable release nach 0.24.6
# https://vikunja.io/docs/openid/#step-2-configure-vikunja
auth:
  local:
    enabled: false
  openid:
    enabled: true
    redirecturl: https://vikunja.MEINEDOMAIN.DE/auth/openid/ #SLASH AM ENDE IST WICHTIG
    providers:
      AUTHENTIK:
        name: "Authentik"
        authurl: https://auth.MEINEDOMAIN.DE/application/o/vikunja/ #SLASH AM ENDE IST WICHTIG
        clientid: Bes........Hzg
        clientsecret: giQY...................A1b6XW
        scope: openid profile email vikunja_scope
        forceuserinfo: false # Optional: Set to true to always use UserInfo endpoint instead of ID token claims, defaults to false

ALTE SYNTAX zB FÜR VERSION 0.24.6:

auth:
  # Local authentication will let users log in and register (if enabled) through the db.
  # This is the default auth mechanism and does not require any additional configuration.
  local:
    # Enable or disable local authentication
    enabled: true
  # OpenID configuration will allow users to authenticate through a third-party OpenID Connect compatible provider.<br/>
  # The provider needs to support the `openid`, `profile` and `email` scopes.<br/>
  # **Note:** Some openid providers (like gitlab) only make the email of the user available through openid claims if they have set it to be publicly vis>  # If the email >  # **Note 2:** The frontend expects to be redirected after authentication by the third party
  # to <frontend-url>/auth/openid/<auth key>. Please make sure to configure the redirect url with your third party
  # auth service accordingly if you're using the default Vikunja frontend.
  # Take a look at the [default config file](https://github.com/go-vikunja/api/blob/main/config.yml.sample) for more information about how to configure >  openid:
  openid:
    # Enable or disable OpenID Connect authentication
    enabled: true
    # A list of enabled providers
    providers:
      # The name of the provider as it will appear in the frontend.
      - name: "authentik Login"
        # The auth url to send users to if they want to authenticate using OpenID Connect.
        authurl: https://auth.MEINEDOMAIN.DE/application/o/vikunja/
        # The client ID used to authenticate Vikunja at the OpenID Connect provider.
        clientid: x8GYDQBG8..........WPiN0n
        # The client secret used to authenticate Vikunja at the OpenID Connect provider.
        clientsecret: vdciH0h1L..........................................................dPHA9WBP6tGLD

        # https://vikunja.io/docs/openid#setup-in-authentik
        logouturl: https://auth.MEINEDOMAIN.DE/application/o/vikunja/end-session/"
        scope: openid email profile vikunja_scope

weiteres unter
https://docs.goauthentik.io/integrations/services/vikunja/
und um teams / groups zu syncen:
https://vikunja.io/docs/openid#setup-in-authentik 

easyVerein als IdP

Konfiguration bei easyVerein:

image.png

config.yml vom Vikunja Docker Volume config:
(NEUE SYNTAX!, siehe oben)

auth:
  local:
    enabled: false
  openid:
    enabled: true
    redirecturl: https://vikunja.folkerts.it/auth/openid/
    providers:
      AUTHENTIK:
        name: "Authentik"
        authurl: https://auth.folkerts.it/application/o/vikunja/
        clientid: Bes4h...fMRnMHzg
        clientsecret: giQYk.....................QUvSVtXIWCw3polnJw
        scope: openid profile email vikunja_scope
        forceuserinfo: false # Optional: Set to true to always use UserInfo endpoint instead of ID token claims, defaul>
      easyVerein:
        name: "easyVerein"
#        authurl: "https://easyverein.com/oauth2/authorize"
        authurl: https://easyverein.com/oauth2
        clientid: gwKKKZn7T......U7W4lt
        clientsecret: Z6jug02E2t9WDzmXXoZ....................rfi5iiOo1T88KDYs
        scope: openid myself custom
        forceuserinfo: false
        teamsclaim: groups  # Der Claim-Name für Gruppen
        teamcreateproperty: name  # Property für Teamnamen
        teamsyncenabled: true  # Teams automatisch synchronisieren

Vikunja Teams aus Authentik übernehmen

https://vikunja.io/docs/openid#setup-in-authentik 

in der config.yml (siehe oben) ist dafür bereits der scope vikunja_scope eingestellt. Der Custom Scope muss nun noch in Authentik angelegt werden:

image.png

neuer scope unter Customization > Eigenschaften (Properties auf eng?) > Erstellen > Scope Mapping >
Name: vikunja_scope
Bereichsname: vikunja_scope (?)
Ausdruck:

groupsDict = {"vikunja_groups": []}
for group in request.user.ak_groups.all():
  groupsDict["vikunja_groups"].append({"name": group.name, "oidcID": group.num_pk})
return groupsDict

> Fertig

Danach unter Anwendungen > Anbieter (Provider auf eng.) > Vikunja > Erweiterte Protokolleinstellungen > email, openid, profile, vikunja_scope mit gedrückter STRG-Taste und Mausklick auswählen:

image.png

> Aktualisieren

Vikunja Container neustarten, nochmal aus- und einloggen mit Authentik bei Vikunja, dann sollten die Teams (Gruppen) aus Authentik übernommen worden sein:

image.png

Docker-Compose .yamls

Jitsi Meet

image.png

https://jitsi.support/wiki/install-jitsi-meet-docker/#step-2-download-jitsi-meet-docker-files

oder 

https://jitsi.github.io/handbook/docs/devops-guide/devops-guide-docker

Installation

Jitsi Meet sollte nicht in Portainer installiert werden, weil ein Skript zur Passworderstellung benutzt und damit eine eigene .env erzeugt wird.

Git Repo Klonen

git clone https://github.com/jitsi/docker-jitsi-meet
cd docker-jitsi-meet

.env kopieren

cp env.example .env

Passwörter autogenerieren

./gen-passwords.sh

docker-compose.yml anpassen

ein paar kleine Änderungen habe ich für die yml gemacht:

#......
    web:
        image: jitsi/web:${JITSI_IMAGE_VERSION:-unstable}
        restart: ${RESTART_POLICY:-unless-stopped}
        ports:
            - '${HTTP_PORT}:80'
            - '${HTTPS_PORT}:443'
        volumes:
            - ${CONFIG}/web:/config:Z
            - ${CONFIG}/web/crontabs:/var/spool/cron/crontabs:Z
            - ${CONFIG}/transcripts:/usr/share/jitsi-meet/transcripts:Z
            - ${CONFIG}/web/load-test:/usr/share/jitsi-meet/load-test:Z
            - ${CONFIG}/web/images:/usr/share/jitsi-meet/images:Z # um zb das watermark.svg zu ersetzen
        labels:
#......
    # XMPP server
    prosody:
        image: jitsi/prosody:${JITSI_IMAGE_VERSION:-stable-10008}
        restart: ${RESTART_POLICY:-unless-stopped}
        expose:
            - '${XMPP_PORT:-5222}'
            - '${PROSODY_S2S_PORT:-5269}'
            - '5347'
            - '${PROSODY_HTTP_PORT:-5280}'
        ports:
            - '5222:5222'  # Ports weitergeleitet, damit eine zweite JVB auf Port 5222 zugreifen kann
            - '5347:5347'
        container_name: jitsi-prosody
        labels:
#...........

.env anpassen

# shellcheck disable=SC2034

################################################################################
################################################################################
# Welcome to the Jitsi Meet Docker setup!
#
# This sample .env file contains some basic options to get you started.
# The full options reference can be found here:
# https://jitsi.github.io/handbook/docs/devops-guide/devops-guide-docker
################################################################################
################################################################################


#
# Basic configuration options
#

# Directory where all configuration will be stored
#CONFIG=~/.jitsi-meet-cfg
CONFIG=/var/lib/docker/volumes/jitsimeet 
# ^ wähle hier das Standardverzeichnis für deine Docker Volumes, ansonsten werden alle Volumes im Root-Nutzerverzeichnis des Servers (~/.jitsi-meet-cfg) erstellt (welches meistens bei Backups ignoriert wird).
#Falls ein Hetzner-Volume auf dem Server eingehängt ist, kann zb das als Config-Directory verwendet werden:
#CONFIG=/mnt/praxis-volume-01/docker-data/volumes/jitsimeet

# Exposed HTTP port (will redirect to HTTPS port)
HTTP_PORT=8043

# Exposed HTTPS port
HTTPS_PORT=8443

# System time zone
TZ=Europe/Berlin

# Public URL for the web service (required)
# Keep in mind that if you use a non-standard HTTPS port, it has to appear in the public URL
PUBLIC_URL=https://jitsi.MEINE-DOMAIN.de

# Media IP addresses to advertise by the JVB
# This setting deprecates DOCKER_HOST_ADDRESS, and supports a comma separated list of IPs
# See the "Running behind NAT or on a LAN environment" section in the Handbook:
# https://jitsi.github.io/handbook/docs/devops-guide/devops-guide-docker#running-behind-nat-or-on-a-lan-environment
#JVB_ADVERTISE_IPS=192.168.1.1,1.2.3.4
JVB_ADVERTISE_IPS=116.xxx.xxx.143 #Public IP zb vom Hetzner Server, auf dem die JVB läuft
# oder
JVB_ADVERTISE_IPS=116.xxx.xxx.143, 10.20.0.5 #Public IP zb vom Hetzner Server, auf dem die JVB läuft PLUS zweiter Server mit JVB


#https://jitsi.github.io/handbook/docs/devops-guide/devops-guide-docker/#turn-server-configuration
#JVB_STUN_SERVERS=meet-jit-si-turnrelay.jitsi.net:443, stun.l.google.com:19302, stun1.l.google.com:19302, stun2.l.google.com:19302
JVB_STUN_SERVERS=turn.MEINEDOMAIN.de:443

TURN_CREDENTIALS=rqjmfcGhY
TURN_HOST=turn.MEINEDOMAIN.de
TURN_PORT=443
#TURNS_HOST=turn.praxisluebberding.de
#TURNS_PORT=443

TURN_TRANSPORT=tcp
ENABLE_TURN=1
ENABLE_P2P=1

# Beim starten mit docker compose -f docker-compose.yml -f log-analyser.yml -f prometheus.yml -f grafana.yml up -d
PROSODY_ENABLE_METRICS = true

#https://github.com/jitsi/docker-jitsi-meet/issues/992#issuecomment-811373266
#https://jitsi.github.io/handbook/docs/devops-guide/devops-guide-docker/#running-behind-a-reverse-proxy
#ENABLE_SCTP=1
#ENABLE_COLIBRI_WEBSOCKET=0
#ENABLE_XMPP_WEBSOCKET=0



#
# Memory limits for Java components
#

#JICOFO_MAX_MEMORY=3072m
#VIDEOBRIDGE_MAX_MEMORY=3072m

#
# JaaS Components (beta)
# https://jaas.8x8.vc
#

# Enable JaaS Components (hosted Jigasi)
# NOTE: if Let's Encrypt is enabled a JaaS account will be automatically created, using the provided email in LETSENCRYPT_EMAIL
#ENABLE_JAAS_COMPONENTS=0

#
# Let's Encrypt configuration
#

# Enable Let's Encrypt certificate generation
#ENABLE_LETSENCRYPT=1

# Domain for which to generate the certificate
#LETSENCRYPT_DOMAIN=meet.example.com

# E-Mail for receiving important account notifications (mandatory)
#LETSENCRYPT_EMAIL=alice@atlanta.net

# Use the staging server (for avoiding rate limits while testing)
#LETSENCRYPT_USE_STAGING=1


#
# Etherpad integration (for document sharing)
#

# Set the etherpad-lite URL in the docker local network (uncomment to enable)
#ETHERPAD_URL_BASE=http://etherpad.meet.jitsi:9001

# Set etherpad-lite public URL, including /p/ pad path fragment (uncomment to enable)
#ETHERPAD_PUBLIC_URL=https://etherpad.my.domain/p/

#
# Whiteboard integration
#

# Set the excalidraw-backend URL in the docker local network (uncomment to enable)
WHITEBOARD_COLLAB_SERVER_URL_BASE=http://excalidraw.jitsi_meet.jitsi

# Set the excalidraw-backend public URL (uncomment to enable)
WHITEBOARD_COLLAB_SERVER_PUBLIC_URL=https://excalidraw.MEINEDOMAIN.de



#
# Basic Jigasi configuration options (needed for SIP gateway support)
#

# SIP URI for incoming / outgoing calls
#JIGASI_SIP_URI=test@sip2sip.info

# Password for the specified SIP account as a clear text
#JIGASI_SIP_PASSWORD=passw0rd

# SIP server (use the SIP account domain if in doubt)
#JIGASI_SIP_SERVER=sip2sip.info

# SIP server port
#JIGASI_SIP_PORT=5060

# SIP server transport
#JIGASI_SIP_TRANSPORT=UDP


#
# Authentication configuration (see handbook for details)
#

# Enable authentication (will ask for login and password to join the meeting)
ENABLE_AUTH=1

# Enable guest access (if authentication is enabled, this allows for users to be held in lobby until registered user lets them in)
ENABLE_GUESTS=1

# Select authentication type: internal, jwt, ldap or matrix
AUTH_TYPE=internal

#https://jitsi.github.io/handbook/docs/devops-guide/devops-guide-docker/#features-configuration-configjs
ENABLE_WELCOME_PAGE=0
# Prejoin Page deaktiviert, weil Teilnehmer manchmal beim Moderator-Login auf Abbrechen drücken und dann auf der Prejoin-Page vereinsamen, wo man seinen Namen eingibt
ENABLE_PREJOIN_PAGE=0 
# Die Lobby aktiviert ein Moderator im Raum nach der Erföffnung über Sicherheitseinstellungen > Lobby aktivieren
ENABLE_LOBBY=1 
ENABLE_CLOSE_PAGE=0

# JWT authentication
#

# Application identifier
#JWT_APP_ID=my_jitsi_app_id

# Application secret known only to your token generator
#JWT_APP_SECRET=my_jitsi_app_secret

# (Optional) Set asap_accepted_issuers as a comma separated list
#JWT_ACCEPTED_ISSUERS=my_web_client,my_app_client

# (Optional) Set asap_accepted_audiences as a comma separated list
#JWT_ACCEPTED_AUDIENCES=my_server1,my_server2

# LDAP authentication (for more information see the Cyrus SASL saslauthd.conf man page)
#

# LDAP url for connection
#LDAP_URL=ldaps://ldap.domain.com/

# LDAP base DN. Can be empty
#LDAP_BASE=DC=example,DC=domain,DC=com

# LDAP user DN. Do not specify this parameter for the anonymous bind
#LDAP_BINDDN=CN=binduser,OU=users,DC=example,DC=domain,DC=com

# LDAP user password. Do not specify this parameter for the anonymous bind
#LDAP_BINDPW=LdapUserPassw0rd

# LDAP filter. Tokens example:
# %1-9 - if the input key is user@mail.domain.com, then %1 is com, %2 is domain and %3 is mail
# %s - %s is replaced by the complete service string
# %r - %r is replaced by the complete realm string
#LDAP_FILTER=(sAMAccountName=%u)

# LDAP authentication method
#LDAP_AUTH_METHOD=bind

# LDAP version
#LDAP_VERSION=3

# LDAP TLS using
#LDAP_USE_TLS=1

# List of SSL/TLS ciphers to allow
#LDAP_TLS_CIPHERS=SECURE256:SECURE128:!AES-128-CBC:!ARCFOUR-128:!CAMELLIA-128-CBC:!3DES-CBC:!CAMELLIA-128-CBC

# Require and verify server certificate
#LDAP_TLS_CHECK_PEER=1

# Path to CA cert file. Used when server certificate verify is enabled
#LDAP_TLS_CACERT_FILE=/etc/ssl/certs/ca-certificates.crt

# Path to CA certs directory. Used when server certificate verify is enabled
#LDAP_TLS_CACERT_DIR=/etc/ssl/certs

# Wether to use starttls, implies LDAPv3 and requires ldap:// instead of ldaps://
# LDAP_START_TLS=1


#
# Security
#
# Set these to strong passwords to avoid intruders from impersonating a service account
# The service(s) won't start unless these are specified
# Running ./gen-passwords.sh will update .env with strong passwords
# You may skip the Jigasi and Jibri passwords if you are not using those
# DO NOT reuse passwords
#

# XMPP password for Jicofo client connections
JICOFO_AUTH_PASSWORD=

# XMPP password for JVB client connections
JVB_AUTH_PASSWORD=

# XMPP password for Jigasi MUC client connections
JIGASI_XMPP_PASSWORD=

# XMPP password for Jigasi transcriber client connections
JIGASI_TRANSCRIBER_PASSWORD=

# XMPP recorder password for Jibri client connections
JIBRI_RECORDER_PASSWORD=

# XMPP password for Jibri client connections
JIBRI_XMPP_PASSWORD=

#
# Docker Compose options
#

# Container restart policy
#RESTART_POLICY=unless-stopped

# Jitsi image version (useful for local development)
#JITSI_IMAGE_VERSION=latest

ENABLE_TRANSCRIPTIONS=0

Konfigurationsdateien anpassen

Für config.js und interface_config.js müssen eigene custom-Dateien angelegt werden. Mehr dazu unter
https://jitsi.github.io/handbook/docs/devops-guide/devops-guide-docker/#jitsi-meet-configuration 

dazu werden im Docker Volume (CONFIG-Verzeichnis aus der .env siehe oben) zwei Dateien angelegt:

custom-config.js

config.defaultRemoteDisplayName= 'Teilnehmer';
config.defaultLocalDisplayName= 'Ich';

custom-interface_config.js

interfaceConfig.JITSI_WATERMARK_LINK= 'https://MEINEDOMAIN.de';
interfaceConfig.APP_NAME= 'Mein-Jitsi';

sieht dann so aus:

image.png

Container starten

docker-compose up -d

Mit Grafana, Prometheus, Loki und OpenTelemetry-Connector starten:

docker compose -f docker-compose.yml -f log-analyser.yml -f grafana.yml -f prometheus.yml -f transcriber.yml up -d

Mit OIDC-Connector (siehe unten)

docker compose -f jitsi-go-openid.yml -f docker-compose.yml -f log-analyser.yml -f grafana.yml -f prometheus.yml -f transcriber.yml up -d

Anpassungen für grafana.yml, prometheus.yml usw: überall container_name: jitsi-prometheus hinzugefügt, damit alles einheitlich benannt ist. Außerderm bei log-analyser.yml den hostname: jitsi-otel hinzugefügt, damit prometheus den hostname erkennt:

also docker-compose.yml, grafana.yml, prometheus.yml usw:

    web:
        image: jitsi/web:${JITSI_IMAGE_VERSION:-stable-10008}
        ...
        container_name: jitsi-web  # <-- container_name in allen ymls hinzugefügt
        labels:
            service: "jitsi-web"
        environment:
            - AMPLITUDE_ID
            - ANALYTICS_SCRIPT_URLS
        ...

log-analyser.yml:

  otel-collector:
    image: otel/opentelemetry-collector-contrib
    container_name: jitsi-otel
    hostname: otel
    volumes:
    ...

Prometheus kontrollieren: http://DOCKER-HOST:9090/targets oder Prometheus > Status > Target health:

image.png

OIDC-Connector mit Authentik

https://github.com/mod242/jitsi-go-openid  

unbedingt die .env anpassen, wenn OIDC verwendet werden soll:


ENABLE_AUTH=1
ENABLE_GUESTS=1
AUTH_TYPE=jwt
ENABLE_WELCOME_PAGE=0
ENABLE_PREJOIN_PAGE=0

ENABLE_LOBBY=1
ENABLE_CLOSE_PAGE=0

JWT_APP_ID=jitsi.MEINEDOMAIN.de
JWT_APP_SECRET=wAEJSlo........1qKMvdvxQKp

# benötigt und hardkodiert: jitsi
JWT_ACCEPTED_ISSUERS=jitsi
JWT_ACCEPTED_AUDIENCES=jitsi

TOKEN_AUTH_URL=https://jitsi-auth.MEINEDOMAIN.de/authenticate?state={state}&room={room}
JWT_AUTH_TYPE=token
JWT_TOKEN_AUTH_MODULE=token_verification

# wichtig für lobby automatisch aktivieren
XMPP_MUC_MODULES=lobby_autostart_on_owner

jitsi-go-openid.yml

 services:
  jitsi-go-openid:
    image: mod242/jitsi-go-openid:latest
    restart: unless-stopped
    environment:
      - 'JITSI_SECRET=wAEJSlo....vdvxQKp'           # Must match the jwt_secret from your Jitsi configuration
      - 'JITSI_URL=https://jitsi.MEINEDOMAIN.de/prefix-zb-kleines-passwort'      # Base URL of your Jitsi instance
      - 'JITSI_SUB=jitsi.MEINEDOMAIN.de'               # Must match the JWT_APP_ID from your Jitsi configuration
      - 'ISSUER_BASE_URL=https://authentik.MEINEDOMAIN.de/application/o/jitsi-auth/'         # Base URL of your OpenID Connect provider
      - 'BASE_URL=https://jitsi-auth.MEINEDOMAIN.de'        # Public base URL of this application (should run behind a reverse proxy)
      - 'CLIENT_ID=s5iM.......h2dy5d'            # Client ID from your OAuth provider
      - 'SECRET=hFiiaCEizhRZ......qffiDxxqhQve'                 # Client secret from your OAuth provider
      - 'PREJOIN=false'                # Whether the prejoin page should be displayed again after authentication
      - 'DEEPLINK=false'                # Whether the callback should use a deep link for redirect to ensure the originating client (Desktop, iOS, Android) is used
      - 'NAME_KEY=name'                # Key for the user's name from the OAuth token (defaults to 'name', but can be 'given_name' or any other key present in the token)
    ports:
      - "3001:3001"
    expose:
      - 3001
    networks:
      meet.jitsi: # -> Your Jitsi Network (if run co-located and exposed via Jitsi-Web)

reverse proxy mit neuer subdomain (in diesem fall jitsi-auth.MEINEDOMAIN.de) noch auf port 3001 leiten 

.env bearbeiten und Container neustarten

Wenn die .env bearbeitet wird, müssen die Container mit 

docker compose down
docker compose up -d

neugestartet werden. 

❗ Wenn nur docker compose restart ausgeführt wird, wird die aktualisierte .env nicht erneut eingelesen!

Firewall: Ports freischalten

Port 80 & 443 werden über den Reverse Proxy Manager geregelt, zusätzlich muss aber Port 10000 UDP freigeschaltet werden!
https://jitsi.github.io/handbook/docs/devops-guide/devops-guide-docker/#external-ports ansonsten passiert es, dass nur zwei Teilnehmer miteinander videokonferenzen können und alle stummgeschaltet und deren Videos deaktiviert werden, sobald drei oder mehre Teilnehmer im Jitsi Raum sind. 
Fehler im JVB (Jitsi Videobridge) Container: 
BridgeSelectionStrategy.select#127: Existing bridge does not have a relay, will not consider other bridges.
https://github.com/jitsi/docker-jitsi-meet/issues/1735 

Zweite JVB für Load-Balancer 

auf einem anderen Server:

services:
    jvb:
        image: jitsi/jvb:stable
        restart: unless-stopped
        ports:
            - '${JVB_PORT}:${JVB_PORT}/udp'
            - '${JVB_COLIBRI_PORT}:${JVB_COLIBRI_PORT}'
        volumes:
            - ${CONFIG}/jvb:/config:Z
        environment:
            - DOCKER_HOST_ADDRESS
            - XMPP_AUTH_DOMAIN
            - XMPP_INTERNAL_MUC_DOMAIN
            - XMPP_SERVER
            - JVB_AUTH_USER
            - JVB_AUTH_PASSWORD
            - JVB_BREWERY_MUC
            - JVB_PORT
            - JVB_COLIBRI_PORT
            - JVB_STUN_SERVERS
            - TZ
        networks:
            meet.jitsi:

networks:
    meet.jitsi:

.env

DOCKER_HOST_ADDRESS=server-04.MEINEDOMAIN.de
CONFIG=/var/lib/docker/volumes/jitsimeet
XMPP_SERVER=10.0.0.3
XMPP_AUTH_DOMAIN=auth.meet.jitsi
XMPP_INTERNAL_MUC_DOMAIN=internal-muc.meet.jitsi
JVB_AUTH_USER=jvb
JVB_AUTH_PASSWORD=a054c......da0d # GLEICHES PW WIE AUF HAUPTSERVER IN .env
JVB_BREWERY_MUC=jvbbrewery
JVB_PORT=10000
JVB_COLIBRI_PORT=8080
JVB_STUN_SERVERS=turn.MEINEDOMAIN.de:443
TZ=Europe/Berlin

Moderator Account erstellen

https://jitsi.github.io/handbook/docs/devops-guide/devops-guide-docker#authentication 

Wenn die Einstellungen aus meiner .env übernommen wurden, muss noch ein Moderator Account erstellt werden, um einen Jitsi Meet Raum eröffnen zu können. Dafür in die Shell des Containers "prosodyctl" gehen, entweder mit ctop, portainer (/bin/bash) oder über die docker cli:

docker compose exec prosody /bin/bash

danach in der shell des prosody containers:

Benutzer anlegen

prosodyctl --config /config/prosody.cfg.lua register <MODERATORNAME> meet.jitsi <MODERATORPASSWORT>

meet.jitsi ist dabei der Name des Netzwerks aus der docker-compose.yml

Benutzer entfernen

prosodyctl --config /config/prosody.cfg.lua unregister <MODERATORNAME> meet.jitsi

Benutzer auflisten

find /config/data/meet%2ejitsi/accounts -type f -exec basename {} .dat \;

Nginx Proxy Manager Konfiguration

image.png

image.png

image.png

location /xmpp-websocket {
    proxy_pass https://localhost:8043;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}
location /colibri-ws {
    proxy_pass https://localhost:8043;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

Whiteboard (ExcaliDraw) integrieren 

https://discussion.scottibyte.com/t/add-a-whiteboard-to-jitsi-meet/320

https://github.com/jitsi/excalidraw-backend 

git clone https://github.com/jitsi/excalidraw-backend.git
cd excalidraw-backend

touch docker-compose.yml

docker-compose.yml

version: "3.8"

services:
  excalidraw:
    build:
      context: .
      args:
        - NODE_ENV=development
    container_name: excalidraw
    ports:
      - "6421:80"
    restart: unless-stopped
    stdin_open: true
    networks:
      - jitsi_meet.jitsi
    healthcheck:
      disable: true
    environment:
      - NODE_ENV=development
    volumes:
      - ./:/opt/node_app/app:delegated
      - ./package.json:/opt/node_app/package.json
      - ./yarn.lock:/opt/node_app/yarn.lock

networks:
  jitsi_meet.jitsi:
      name: jitsi_meet.jitsi
      external: true
      driver: bridge
docker compose up --build -d

NPM Reverse Proxy Eintrag für excalidraw anlegen:

image.png

.env von Jitsi anpassen:

#
# Whiteboard integration
#

# Set the excalidraw-backend URL in the docker local network (uncomment to enable)
WHITEBOARD_COLLAB_SERVER_URL_BASE=http://excalidraw.jitsi_meet.jitsi

# Set the excalidraw-backend public URL (uncomment to enable)
WHITEBOARD_COLLAB_SERVER_PUBLIC_URL=https://excalidraw.DOMAIN.de

image.png

Obacht: damit die lokale URL funktioniert, müssen die jitsi container im gleichen Netzwerk wie excalidraw sein, daher auch in der docker-compose.yml von excalidraw das netzwerk von jitsi als external einpflegen. Am besten mit exec in den jitsi-web container gehen und prüfen, ob exclidraw erreichbar ist:

ping excalidraw -c 1
ping excalidraw.jitsi_meet.jitsi -c 1

image.png

image.png

image.png

nachdem die .env mit URLs versorgt wurde, nochmal in den jitsi docker Ordner wechseln und 

docker compose down
docker compose up -d

TURN / STUN Server einrichten (Coturn)

Weil einige Firewalls auf der Clientseite verhindern, sich mit UDP Port 10000 zu verbinden, um WebRTC durchzuschleusen, sollte ein TURN Server aufgesetzt werden, über den dann der Verkehr geleitet wird. Wenn UDP Ports möglich sind, stellt der STUN Server eine Peer2Peer Verbinung her. Wenn nicht, wird der Traffic über Port 443 geleitet, was bei jedem restriktiven Netzwerk funktionieren sollte.
Mehr dazu hier im Eintrag Coturn: https://wiki.folkerts.it/books/docker/page/coturn-stun-turn-server-fur-jitsi-meet  
⚠ Wichtig ⚠ Der TURN Server kann nicht ohne weiteres über einen Reverse Proxy geleitet werden, weil er nicht auf dem HTTP Protokoll beruht, sondern auf TCP und/oder TLS. Es sollte also am besten ein eigener Server sein, auf dem der Port 443 freigegeben werden kann, ohne auf einen Reverse Proxy zu zeigen.

Design anpassen

https://goneuland.de/jitsi-anpassungen-docker/

https://scheible.it/das-design-von-jitsi-meet-anpassen

Logo anpassen

    web:
        image: jitsi/web:${JITSI_IMAGE_VERSION:-stable-10008}
        ...
        volumes:
        ...
            - ${CONFIG}/web/load-test:/usr/share/jitsi-meet/load-test:Z
            - ${CONFIG}/web/images/watermark.svg:/usr/share/jitsi-meet/images/watermark.svg:Z # <-- Logo als .svg hinzufügen
        container_name: jitsi-web
        ...

UDP Buffer auf Host anpassen

https://jitsi.github.io/handbook/docs/devops-guide/devops-guide-docker/#adjust-udp-buffers 

If you are experiencing issues with UDP traffic, like synchronization issues, skipping frames and similar, or if you expect a high traffic and big conferences, you might want to adjust the UDP buffer sizes. You need to do that on the host system, that hosts the jvb container. To do so you can get this sysctl config file and save it in /etc/sysctl.d and load it via: sysctl --system.

also

nano /etc/sysctl.d/20-jvb-udp-buffers.conf

Inhalt:

# this sets the max, so that we can bump the JVB UDP single port buffer size.
net.core.rmem_max=10485760
net.core.netdev_max_backlog=100000

Danach laden und prüfen mit:

sysctl --system

Troubleshooting

Jitsi in Kubernetes, Port 10000 kann nicht freigeschaltet werden, stattdessen 30000 bis 32.000 irgendwas

https://stackoverflow.com/questions/71495351/how-to-setup-jitsi-meet-on-a-custom-kubernetes 

Docker-Compose .yamls

InvoiceNinja

Neu auf x86

https://github.com/invoiceninja/dockerfiles 

git clone https://github.com/invoiceninja/dockerfiles.git -b debian
cd dockerfiles/debian 

docker-compose.yml:

services:
  app:
    build:
      context: .
    image: invoiceninja/invoiceninja-debian:${TAG:-latest}
    restart: unless-stopped
    env_file:
      - ./.env
    volumes:
      - ./php/php.ini:/usr/local/etc/php/conf.d/invoiceninja.ini:ro
      - ./php/php-fpm.conf:/usr/local/etc/php-fpm.d/invoiceninja.conf:ro
      - ./supervisor/supervisord.conf:/etc/supervisor/conf.d/supervisord.conf:ro
      - public:/var/www/html/public
      - storage:/var/www/html/storage
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "php", "-v"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 60s

  nginx:
    image: nginx:alpine
    restart: unless-stopped
    ports:
      - "8013:80"
    volumes:
      - ./nginx:/etc/nginx/conf.d:ro
      - public:/var/www/html/public:ro
      - storage:/var/www/html/storage:ro
    depends_on:
      app:
        condition: service_healthy

  mysql:
    image: mysql:8
    restart: unless-stopped
    environment:
      MYSQL_DATABASE: ${DB_DATABASE}
      MYSQL_USER: ${DB_USERNAME}
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
    volumes:
      - mysql:/var/lib/mysql
    healthcheck:
      test:
        [
          "CMD",
          "mysqladmin",
          "ping",
          "-h",
          "localhost",
          "-u${MYSQL_USER}",
          "-p${MYSQL_PASSWORD}",
        ]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  redis:
    image: redis:alpine
    restart: unless-stopped
    volumes:
      - redis:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]

volumes:
  public:
  storage:
  mysql:
  redis:


für .env die var APP_KEY erzeugen mit

# If you haven't started the containers yet:
docker run --rm -it invoiceninja/invoiceninja-debian php artisan key:generate --show
# Or if your containers are already running:
docker compose exec app php artisan key:generate --show

Copy the entire string and insert in the .env file at APP_KEY=base64....=

 

! Außerdem noch IN_USER_EMAIL und IN_PASSWORD anpassen

.env

TAG=5.12.45

# IN application vars
#APP_URL=http://localhost:8012
APP_URL=https://invoiceninja.MEINEDOMAIN.it
APP_KEY=base64:+jpwe............EGPGKkG/fxkEU=
APP_ENV=production
APP_DEBUG=true
REQUIRE_HTTPS=false
PHANTOMJS_PDF_GENERATION=false
PDF_GENERATOR=snappdf
TRUSTED_PROXIES='*'


CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis

REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379

FILESYSTEM_DISK=debian_docker

# DB connection
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=ninja
DB_USERNAME=ninja
DB_PASSWORD=c94f68e.............6f0e9741
DB_ROOT_PASSWORD=d69f756............11207d
DB_CONNECTION=mysql

# Create initial user
# Default to these values if empty
IN_USER_EMAIL=admin@example.com
IN_PASSWORD=changeme!
# IN_USER_EMAIL=
# IN_PASSWORD=

# Mail options
MAIL_MAILER=log
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS='user@example.com'
MAIL_FROM_NAME='Self Hosted User'

# MySQL
MYSQL_ROOT_PASSWORD=d69f756............11207d
MYSQL_USER=ninja
MYSQL_PASSWORD=c94f68e12d.............0e6f0e9741
MYSQL_DATABASE=ninja

# GoCardless/Nordigen API key for banking integration
NORDIGEN_SECRET_ID=
NORDIGEN_SECRET_KEY=

IS_DOCKER=true
SCOUT_DRIVER=null
#SNAPPDF_CHROMIUM_PATH=/usr/bin/google-chrome-stable

Alt ab hier:

Erster Versuch auf Fernnetz

### AUF EIS GELEGT, PROBLEME MIT MYSQL ENVs?

services:
  server:
    image: nginx
    restart: unless-stopped
    #env_file: env
    volumes:
      # Vhost configuration
      #- /var/lib/docker/volumes/invoiceninja/config/caddy/Caddyfile:/etc/caddy/Caddyfiledocker-com
      - /var/lib/docker/volumes/invoiceninja/config/nginx/in-vhost.conf:/etc/nginx/conf.d/in-vhost.conf:ro
      - /var/lib/docker/volumes/invoiceninja/docker/app/public:/var/www/app/public:ro
    depends_on:
      - app
    # Run webserver nginx on port 80
    # Feel free to modify depending what port is already occupied
    ports:
      - "8006:80"
      #- "443:443"
    networks:
      - invoiceninja
    extra_hosts:
      - "in5.localhost:192.168.0.124 " #host and ip

  app:
    image: invoiceninja/invoiceninja:5.10
    #env_file: env
    restart: unless-stopped
    volumes:
      - /var/lib/docker/volumes/invoiceninja/config/hosts:/etc/hosts:ro
      - /var/lib/docker/volumes/invoiceninja/docker/app/public:/var/www/app/public:rw,delegated
      - /var/lib/docker/volumes/invoiceninja/docker/app/storage:/var/www/app/storage:rw,delegated
      - /var/lib/docker/volumes/invoiceninja/config/php/php.ini:/usr/local/etc/php/php.ini
      - /var/lib/docker/volumes/invoiceninja/config/php/php-cli.ini:/usr/local/etc/php/php-cli.ini

    depends_on:
      - db
    networks:
      - invoiceninja
    extra_hosts:
      - "in5.localhost:192.168.0.124 " #host and ip

  db:
    image: mysql:8
#    When running on ARM64 use MariaDB instead of MySQL
#    image: mariadb:10.4
#    For auto DB backups comment out image and use the build block below
#    build:
#      context: ./config/mysql
    restart: unless-stopped
    #env_file: env
    volumes:
      - /var/lib/docker/volumes/invoiceninja/docker/mysql/data:/var/lib/mysql:rw,delegated

      # remove comments for next 4 lines if you want auto sql backups
      #- /var/lib/docker/volumes/invoiceninja/docker/mysql/bak:/backups:rw
      #- /var/lib/docker/volumes/invoiceninja/config/mysql/backup-script:/etc/cron.daily/daily:ro
      #- /var/lib/docker/volumes/invoiceninja/config/mysql/backup-script:/etc/cron.weekly/weekly:ro
      #- /var/lib/docker/volumes/invoiceninja/config/mysql/backup-script:/etc/cron.monthly/monthly:ro
    networks:
      - invoiceninja
    extra_hosts:
      - "in5.localhost:192.168.0.124 " #host and ip

  # THIS IS ONLY A VALID CONFIGURATION FOR IN 4. DO NOT USE FOR IN 5.
  # cron:
  #   image: invoiceninja/invoiceninja:alpine-4
  #   volumes:
      # - /var/lib/docker/volumes/invoiceninja/docker/app/public:/var/www/app/public:rw,delegated
      # - /var/lib/docker/volumes/invoiceninja/docker/app/storage:/var/www/app/storage:rw,delegated
      # - /var/lib/docker/volumes/invoiceninja/docker/app/public/logo:/var/www/app/public/logo:rw,delegated
  #   entrypoint: |
  #     /bin/sh -c 'sh -s <<EOF
  #     trap "break;exit" SIGHUP SIGINT SIGTERM
  #     sleep 300s
  #     while /bin/true; do
  #       ./artisan ninja:send-invoices
  #       ./artisan ninja:send-reminders
  #       sleep 1d
  #     done
  #     EOF'
  #   networks:
  #     - invoiceninja
  #


networks:
  invoiceninja:

Zweiter Versuch auf Heimnetz

### AUF EIS GELEGT, PROBLEME MIT VOLUMEs FÜR DATEIEN (zb hosts)

services:
  server:
    image: nginx
    restart: unless-stopped
    #env_file: env
    volumes:
      # Vhost configuration
      #- /var/lib/docker/volumes/invoiceninja/config/caddy/Caddyfile:/etc/caddy/Caddyfiledocker-com
      - /var/lib/docker/volumes/invoiceninja/config/nginx/in-vhost.conf:/etc/nginx/conf.d/in-vhost.conf:ro
      - /var/lib/docker/volumes/invoiceninja/docker/app/public:/var/www/app/public:ro
    depends_on:
      - app
    # Run webserver nginx on port 80
    # Feel free to modify depending what port is already occupied
    ports:
      - "8006:80"
      #- "443:443"
    networks:
      - invoiceninja
    extra_hosts:
      - "in5.localhost:192.168.0.124 " #host and ip

  app:
    image: invoiceninja/invoiceninja:5.10
    #env_file: env
    restart: unless-stopped
    volumes:
      - /var/lib/docker/volumes/invoiceninja/config/hosts:/etc/hosts:ro
      - /var/lib/docker/volumes/invoiceninja/docker/app/public:/var/www/app/public:rw,delegated
      - /var/lib/docker/volumes/invoiceninja/docker/app/storage:/var/www/app/storage:rw,delegated
      - /var/lib/docker/volumes/invoiceninja/config/php/php.ini:/usr/local/etc/php/php.ini
      - /var/lib/docker/volumes/invoiceninja/config/php/php-cli.ini:/usr/local/etc/php/php-cli.ini

    depends_on:
      - db
    networks:
      - invoiceninja
    extra_hosts:
      - "in5.localhost:192.168.0.124 " #host and ip

  db:
    image: mysql:8
#    When running on ARM64 use MariaDB instead of MySQL
#    image: mariadb:10.4
#    For auto DB backups comment out image and use the build block below
#    build:
#      context: ./config/mysql
    restart: unless-stopped
    #env_file: env
    volumes:
      - /var/lib/docker/volumes/invoiceninja/docker/mysql/data:/var/lib/mysql:rw,delegated

      # remove comments for next 4 lines if you want auto sql backups
      #- /var/lib/docker/volumes/invoiceninja/docker/mysql/bak:/backups:rw
      #- /var/lib/docker/volumes/invoiceninja/config/mysql/backup-script:/etc/cron.daily/daily:ro
      #- /var/lib/docker/volumes/invoiceninja/config/mysql/backup-script:/etc/cron.weekly/weekly:ro
      #- /var/lib/docker/volumes/invoiceninja/config/mysql/backup-script:/etc/cron.monthly/monthly:ro
    networks:
      - invoiceninja
    extra_hosts:
      - "in5.localhost:192.168.0.124 " #host and ip

  # THIS IS ONLY A VALID CONFIGURATION FOR IN 4. DO NOT USE FOR IN 5.
  # cron:
  #   image: invoiceninja/invoiceninja:alpine-4
  #   volumes:
      # - /var/lib/docker/volumes/invoiceninja/docker/app/public:/var/www/app/public:rw,delegated
      # - /var/lib/docker/volumes/invoiceninja/docker/app/storage:/var/www/app/storage:rw,delegated
      # - /var/lib/docker/volumes/invoiceninja/docker/app/public/logo:/var/www/app/public/logo:rw,delegated
  #   entrypoint: |
  #     /bin/sh -c 'sh -s <<EOF
  #     trap "break;exit" SIGHUP SIGINT SIGTERM
  #     sleep 300s
  #     while /bin/true; do
  #       ./artisan ninja:send-invoices
  #       ./artisan ninja:send-reminders
  #       sleep 1d
  #     done
  #     EOF'
  #   networks:
  #     - invoiceninja
  #


networks:
  invoiceninja:

Docker-Compose .yamls

CheckMK

https://docs.checkmk.com/latest/en/introduction_docker.html 

name: <your project name>
services:
    check-mk-raw:
        container_name: monitoring
        restart: always
        image: checkmk/check-mk-raw:2.3.0-latest
        #? stdin_open: true
        #? tty: true
        ports:
            - 8080:5000
            - 8000:8000
        tmpfs: /opt/omd/sites/cmk/tmp:uid=1000,gid=1000
        volumes:
            - monitoring:/omd/sites
            - /etc/localtime:/etc/localtime:ro

volumes:
    monitoring:
        external: true
        name: monitoring

:8080 öffnen
"You will find the provisional password for the cmkadmin account in the logs that are written for this container"

altes Heimnetz:

version: '3.1'
services:
  controll:
    image: checkmk/check-mk-raw:latest
    tmpfs:
     - /opt/omd/sites/cmk/tmp:uid=1000,gid=1000
    ulimits:
      nofile: 1024
    container_name: checkmk
    restart: unless-stopped
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - monitoring:/omd/sites
    ports:
      - 8095:5000
      - 6557:6557

volumes:
  monitoring:
  

https://gist.github.com/FlorianHeigl/72dadde0e4f9fa4b8c1bcfc5c9e41b0f

---
version: '3'
services:
   checkmk:
     image: checkmk/check-mk-raw:2.0.0-latest
     ports:
       - "162:162/udp"
       - "514:514/udp"
       - "514:514/tcp"
       - "6557:6557/tcp"
       - "8080:5000/tcp"
     environment:
       - MAIL_RELAY_HOST=mailrelay.example.com
       - CMK_SITE_ID=cmknew
       - CMK_PASSWORD=XXX
       - CMK_LIVESTATUS_TCP=on
     volumes:
       - /etc/localtime:/etc/localtime:ro
       - cmknew:/omd/sites

volumes:
  cmknew:

Docker-Compose .yamls

Netdata

 

image.png

 

version: '3'
services:
  netdata:
    image: netdata/netdata
    container_name: netdata
    pid: host
    network_mode: host
    restart: unless-stopped
    cap_add:
      - SYS_PTRACE
      - SYS_ADMIN
    security_opt:
      - apparmor:unconfined
    volumes:
      - netdataconfig:/etc/netdata
      - netdatalib:/var/lib/netdata
      - netdatacache:/var/cache/netdata
      - /:/host/root:ro,rslave
      - /etc/passwd:/host/etc/passwd:ro
      - /etc/group:/host/etc/group:ro
      - /etc/localtime:/etc/localtime:ro
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /etc/os-release:/host/etc/os-release:ro
      - /var/log:/host/var/log:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro

volumes:
  netdataconfig:
  netdatalib:
  netdatacache:

 

Docker-Compose .yamls

OliveTin (Web-UI für Shell-Cmds)

OliveTin

image.png

image.png

https://github.com/OliveTin/OliveTin 

Eikes docker-compose.yml (mit Möglichkeit für docker exec cmds)

version: "3.8"
services:
  olivetin:
    container_name: olivetin
    image: jamesread/olivetin:2024.11.18
    user: root # wichtig für 'docker exec... Befehle in anderen Containern'
    volumes:
      - cfg:/config
      - /var/run/docker.sock:/var/run/docker.sock # wichtig für 'docker exec... Befehle in anderen Containern'
    ports:
      - "1337:1337"
    restart: unless-stopped

volumes:
  cfg:

Eikes config.yaml für Jitsi mit Authentik

# There is a built-in micro proxy that will host the webui and REST API all on
# one port (this is called the "Single HTTP Frontend") and means you just need
# one open port in the container/firewalls/etc.
#
# Listen on all addresses available, port 1337
listenAddressSingleHTTPFrontend: 0.0.0.0:1337

# Choose from INFO (default), WARN and DEBUG
logLevel: "DEBUG"

# Checking for updates https://docs.olivetin.app/update-checks.html
checkForUpdates: false

pageTitle: Shell-Aktionen
showFooter: false

# Actions are commands that are executed by OliveTin, and normally show up as
# buttons on the WebUI.
#
# Docs: https://docs.olivetin.app/create-your-first-action.html
actions:
  # This uses `popupOnStart: execution-dialog-stdout-only` to simply show just
  # the command output.
  - title: Checke Speicherplatz
    icon: disk
    shell: df -h /media
    popupOnStart: execution-dialog-stdout-only
    # ACL MUSS NICHT MEHR BEI JEDER AKTION EINGESTELLT WERDEN, WEIL
    # addToEveryAction: true
    # BEI DEN ACL EINSTELLUNGEN UNTEN
    # acls:
    #   - Authentik-Admins
    #   - AdminsNachUsername



  - title: Jitsi Moderatoren auflisten
    icon: <iconify-icon icon="mdi:user-search"></iconify-icon>
    shell: docker exec jitsi-prosody-1 sh -c "find /config/data/meet%2ejitsi/accounts -type f -exec basename {} .dat \; | sed -e 's/%2e/./g' -e 's/%2d/-/g' -e 's/%5f/_/g'"
    popupOnStart: execution-dialog-stdout-only


  - title: Jitsi Moderator anlegen (oder PW ändern)
    #shell: echo Test für execution-dialog-stdout-only: User: {{ beuntzername }} PW: {{ passwort }}
    shell: docker exec jitsi-prosody-1 sh -c "prosodyctl --config /config/prosody.cfg.lua register {{ beuntzername }} meet.jitsi {{ passwort }}"
    icon: <iconify-icon icon="mdi:user-add"></iconify-icon>
    timeout: 100
    popupOnStart: execution-dialog-stdout-only
    arguments:
      - name: beuntzername
        title: Benutzername
        type: ascii_identifier
        default: m.oderator
        description: Verwende nur "-", "_" und ".", keine anderen Sonderzeichen, Umlaute oder Leerzeichen
      - name: passwort
        title: Passwort
        type: ascii_identifier
        default: Passwort123
        description: PW abspeichern! Verwende nur "-", "_" und ".", keine anderen Sonderzeichen, Umlaute oder Leerzeichen


  - title: Jitsi Moderator löschen
    shell: docker exec jitsi-prosody-1 sh -c "prosodyctl --config /config/prosody.cfg.lua unregister {{ beuntzername }} meet.jitsi"
    icon: <iconify-icon icon="mdi:user-remove"></iconify-icon>
    timeout: 100
    popupOnStart: execution-dialog-stdout-only
    arguments:
      - name: beuntzername
        title: Benutzername
        type: ascii_identifier
        default: m.oderator
        description: Der Name des Moderators zb m.oderator

authRequireGuestsToLogin: true # Optional - depends if you want to "disable" guests.

authOAuth2RedirectURL: "https://olivetin.MEINEDOMAIN.de/oauth/callback"
authOAuth2Providers:
  authentik:
    name: authentik
    title: Authentik
    clientID: "GWF9.......YaHj"
    clientSecret: "3yOkhOMCA...............84xOpK2Y1gHzihyyMxkv5"
    authURL: "https://auth.MEINEDOMAIN.de/application/o/authorize/"
    tokenURL: "https://auth.MEINEDOMAIN.de/application/o/token/"
    whoamiURL: "https://auth.MEINEDOMAIN.de/application/o/userinfo/"
    usernameField: "preferred_username"
    icon: <iconify-icon icon="simple-icons:authentik"></iconify-icon>


defaultPermissions:
 view: false
 exec: false
 logs: true

# GROUPS KLAPPT LEIDER NICHT, SIEHE
# https://github.com/OliveTin/OliveTin/issues/477
# DAHER MATCHUSERNAMES MIT DEN EINZELEN AUTHENTIK-USERN (AdminsNachUsername)
accessControlLists:
  - name: Authentik-Admins
    matchUsergroups:
      - Authentik-Admins
    permissions:
      view: true
      exec: true
  - name: AdminsNachUsername
    addToEveryAction: true
    matchUserNames:
      - demo-admin
      - vorname.nachname
      - m.mustermann
    permissions:
      view: true
      exec: true

Authentik OIDC

https://docs.olivetin.app/oauth2-authentik.html 

Original config.yaml mit Beispielen

# There is a built-in micro proxy that will host the webui and REST API all on
# one port (this is called the "Single HTTP Frontend") and means you just need
# one open port in the container/firewalls/etc.
#
# Listen on all addresses available, port 1337
listenAddressSingleHTTPFrontend: 0.0.0.0:1337

# Choose from INFO (default), WARN and DEBUG
logLevel: "INFO"

# Checking for updates https://docs.olivetin.app/update-checks.html
checkForUpdates: false

# Actions are commands that are executed by OliveTin, and normally show up as
# buttons on the WebUI.
#
# Docs: https://docs.olivetin.app/create-your-first-action.html
actions:
  # This is the most simple action, it just runs the command and flashes the
  # button to indicate status.
  #
  # If you are running OliveTin in a container remember to pass through the
  # docker socket! https://docs.olivetin.app/action-container-control.html
  - title: Ping the Internet
    shell: ping -c 3 1.1.1.1
    icon: ping
    popupOnStart: execution-dialog-stdout-only

  # This uses `popupOnStart: execution-dialog-stdout-only` to simply show just
  # the command output.
  - title: Check disk space
    icon: disk
    shell: df -h /media
    popupOnStart: execution-dialog-stdout-only

  # This uses `popupOnStart: execution-dialog` to show a dialog with more
  # information about the command that was run.
  - title: check dmesg logs
    shell: dmesg | tail
    icon: logs
    popupOnStart: execution-dialog

  # This uses `popupOnStart: execution-button` to display a mini button that
  # links to the logs.
  - title: date
    shell: date
    timeout: 6
    icon: clock
    popupOnStart: execution-button

  # You are not limited to operating system commands, and of course you can run
  # your own scripts. Here `maxConcurrent` stops the script running multiple
  # times in parallel. There is also a timeout that will kill the command if it
  # runs for too long.
  - title: Run backup script
    shell: /opt/backupScript.sh
    shellAfterCompleted: "apprise -t 'Notification: Backup script completed' -b 'The backup script completed with code {{ exitCode}}. The log is: \n {{ output }} '"
    maxConcurrent: 1
    timeout: 10
    icon: backup
    popupOnStart: execution-dialog

  # When you want to prompt users for input, that is when you should use
  # `arguments` - this presents a popup dialog and asks for argument values.
  #
  # Docs: https://docs.olivetin.app/action-ping.html
  - title: Ping host
    shell: ping {{ host }} -c {{ count }}
    icon: ping
    timeout: 100
    popupOnStart: execution-dialog-stdout-only
    arguments:
      - name: host
        title: Host
        type: ascii_identifier
        default: example.com
        description: The host that you want to ping

      - name: count
        title: Count
        type: int
        default: 3
        description: How many times to do you want to ping?

  # OliveTin can control containers - docker is just a command line app.
  #
  # However, if you are running in a container you will need to do some setup,
  # see the docs below.
  #
  # Docs: https://docs.olivetin.app/action-container-control.html
  - title: Restart Docker Container
    icon: restart
    shell: docker restart {{ container }}
    arguments:
      - name: container
        title: Container name
        choices:
          - value: plex
          - value: traefik
          - value: grafana

  # There is a special `confirmation` argument to help against accidental clicks
  # on "dangerous" actions.
  #
  # Docs: https://docs.olivetin.app/confirmation.html
  - title: Delete old backups
    icon: ashtonished
    shell: rm -rf /opt/oldBackups/
    arguments:
      - type: confirmation
        title: Are you sure?!

  # This is an action that runs a script included with OliveTin, that will
  # download themes. You will still need to set theme "themeName" in your config.
  #
  # Docs: https://docs.olivetin.app/themes.html
  - title: Get OliveTin Theme
    shell: olivetin-get-theme {{ themeGitRepo }} {{ themeFolderName }}
    icon: theme
    arguments:
      - name: themeGitRepo
        title: Theme's Git Repository
        description: Find new themes at https://olivetin.app/themes
        type: url

      - name: themeFolderName
        title: Theme's Folder Name
        type: ascii_identifier

  # Sometimes you want to run actions on other servers - don't overcomplicate
  # it, just use SSH! OliveTin includes a helper to make this easier, which is
  # entirely optional. You can also setup SSH manually.
  #
  # Docs: https://docs.olivetin.app/action-ssh-easy.html
  # Docs: https://docs.olivetin.app/action-ssh.html
  - title: "Setup easy SSH"
    icon: ssh
    shell: olivetin-setup-easy-ssh
    popupOnStart: execution-dialog

  # Here's how to use SSH with the "easy" config, to restart a service on
  # another server.
  #
  # Docs: https://docs.olivetin.app/action-ssh-easy.html
  # Docs: https://docs.olivetin.app/action-service.html
  - title: Restart httpd on server1
    id: restart_httpd
    icon: restart
    timeout: 1
    shell: ssh -F /config/ssh/easy.cg root@server1 'service httpd restart'

  # Lots of people use OliveTin to build web interfaces for their electronics
  # projects. It's best to install OliveTin as a native package (eg, .deb), and
  # then you can use either a python script or the `gpio` command.
  - title: Toggle GPIO light
    shell: gpioset gpiochip1 9=1
    icon: light

  # There are several built-in shortcuts for the `icon` option, but you
  # can also just specify any HTML, this includes any unicode character,
  # or a <img = "..." /> link to a custom icon.
  #
  # Docs: https://docs.olivetin.app/icons.html
  #
  # Lots of people use OliveTin to easily execute ansible-playbooks. You
  # probably want a much longer timeout as well (so that ansible completes).
  #
  # Docs: https://docs.olivetin.app/ansible-playbook.html
  - title: "Run Automation Playbook"
    icon: '&#129302;'
    shell: ansible-playbook -i /etc/hosts /root/myRepo/myPlaybook.yaml
    timeout: 120

  # The following actions are "dummy" actions, used in a Dashboard. As long as
  # you have these referenced in a dashboard, they will not show up in the
  # `actions` view.
  - title: Ping hypervisor1
    shell: echo "hypervisor1 online"

  - title: Ping hypervisor2
    shell: echo "hypervisor2 online"

  - title: "{{ server.name }} Wake on Lan"
    shell: echo "Sending Wake on LAN to {{ server.hostname }}"
    entity: server

  - title: "{{ server.name }} Power Off"
    shell: "echo 'Power Off Server: {{ server.hostname }}'"
    entity: server

  - title: Ping All Servers
    shell: "echo 'Ping all servers'"
    icon: ping

  - title: Start {{ container.Names }}
    icon: box
    shell: docker start {{ container.Names }}
    entity: container
    trigger: Update container entity file

  - title: Stop {{ container.Names }}
    icon: box
    shell: docker stop {{ container.Names }}
    entity: container
    trigger: Update container entity file

  # Lastly, you can hide actions from the web UI, this is useful for creating
  # background helpers that execute only on startup or a cron, for updating
  # entity files.

  # - title: Update container entity file
  #   shell: 'docker ps -a --format json > /etc/OliveTin/entities/containers.json'
  #   hidden: true
  #   execOnStartup: true
  #   execOnCron: '*/1 * * * *'

# An entity is something that exists - a "thing", like a VM, or a Container
# is an entity. OliveTin allows you to then dynamically generate actions based
# around these entities.
#
# This is really useful if you want to generate wake on lan or poweroff actions
# for `server` entities, for example.
#
# A very popular use case that entities were designed for was for `container`
# entities - in a similar way you could generate `start`, `stop`, and `restart`
# container actions.
#
# Entities are just loaded fome files on disk, OliveTin will also watch these
# files for updates while OliveTin is running, and update entities.
#
# Entities can have properties defined in those files, and those can be used
# in your configuration as variables. For example; `container.status`,
# or `vm.hostname`.
#
# Docs: http://docs.olivetin.app/entities.html
entities:
  # YAML files are the default expected format, so you can use .yml or .yaml,
  # or even .txt, as long as the file contains valid a valid yaml LIST, then it
  # will load properly.
  #
  # Docs: https://docs.olivetin.app/entities.html
  - file: entities/servers.yaml
    name: server

  - file: entities/containers.json
    name: container

# Dashboards are a way of taking actions from the default "actions" view, and
# organizing them into groups - either into folders, or fieldsets.
#
# The only way to properly use entities, are to use them with a `fieldset` on
# a dashboard.
dashboards:
  # Top level items are dashboards.
  - title: My Servers
    contents:
      - title: All Servers
        type: fieldset
        contents:
          # The contents of a dashboard will try to look for an action with a
          # matching title IF the `contents: ` property is empty.
          - title: Ping All Servers

          # If you create an item with some "contents:", OliveTin will show that as
          # directory.
          - title: Hypervisors
            contents:
              - title: Ping hypervisor1
              - title: Ping hypervisor2

      # If you specify `type: fieldset` and some `contents`, it will show your
      # actions grouped together without a folder.
      - type: fieldset
        entity: server
        title: 'Server: {{ server.hostname }}'
        contents:
          # By default OliveTin will look for an action with a matching title
          # and put it on the dashboard.
          #
          # Fieldsets  also support `type: display`, which can display arbitary
          # text. This is useful for displaying things like a container's state.
          - type: display
            title: |
              Hostname: <strong>{{ server.name }}</strong>
              IP Address: <strong>{{ server.ip }}</strong>

          # These are the actions (defined above) that we want on the dashboard.
          - title: '{{ server.name }} Wake on Lan'
          - title: '{{ server.name }} Power Off'

  # This is the second dashboard.
  - title: My Containers
    contents:
      - title: 'Container {{ container.Names }} ({{ container.Image }})'
        entity: container
        type: fieldset
        contents:
          - type: display
            title: |
              {{ container.RunningFor }} <br /><br /><strong>{{ container.State }}</strong>

          - title: 'Start {{ container.Names }}'
          - title: 'Stop {{ container.Names }}'

config.yaml Beispiel für Jitsi Meet Moderator anlegen

actions:
  - title: Jitsi Moderator anlegen
      #shell: echo {{ beuntzername }} meet.jitsi {{ passwort }}
      shell: docker exec jitsi-prosody-1 sh -c "prosodyctl --config /config/prosody.cfg.lua register {{ beuntzername }} meet.jitsi {{ passwort }}"
      icon: ping
      timeout: 100
      popupOnStart: execution-dialog-stdout-only
      arguments:
        - name: beuntzername
          title: Benutzername
          type: ascii_identifier
          default: m.oderator
          description: Der Name des Moderators zb m.oderator
  
        - name: passwort
          title: Passwort
          type: ascii_identifier
          default: Passwort123
          description: Das Passwort (keine Leerzeichen, PW abspeichern!)

Docker-Compose .yamls

Beszel (Simples Server Monitoring)

https://github.com/henrygd/beszel 

image.png

image.png

Obacht: Beszel ist nicht für Remote-Server geeignet, sondern nur für IP-Adressen, die übers LAN erreichbar sind. Am besten mit einem Mesh-VPN wie Tailscale oder Netbird lösen

beszel (Server)

services:
  beszel:
    image: henrygd/beszel
    container_name: beszel
    restart: unless-stopped
    ports:
      - 8090:8090
    volumes:
      - data:/beszel_data

volumes:
  data:

über "System hinzufügen" das fertige docker-compose.yml für den zu überwachenden Docker-Host kopieren:

image.png

beszel (Agent) 

# BEISPIEL FÜR AGENT, NICHT BLIND KOPIEREN
services:
  beszel-agent:
    image: "henrygd/beszel-agent"
    container_name: "beszel-agent"
    restart: unless-stopped
    network_mode: host
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      # monitor other disks / partitions by mounting a folder in /extra-filesystems
      # - /mnt/disk/.beszel:/extra-filesystems/sda1:ro
    environment:
      PORT: 45876
      KEY: "ssh-ed25519 AAAAC3N........................lhJ6jc/+LcGSAwKYlfEt"

Docker-Compose .yamls

Duplicati (Backup Tool)

 

 

---
services:
  duplicati:
    image: lscr.io/linuxserver/duplicati:2.0.8
    container_name: duplicati
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Berlin
      - CLI_ARGS= #optional
    volumes:
      - config:/config
      - backups:/backups
      - /home/:/source
    ports:
      - 8200:8200
    restart: unless-stopped

volumes:
  config:
  backups:

 

Docker-Compose .yamls

Coturn (STUN / TURN Server für Jitsi Meet)

Weil einige Firewalls auf der Clientseite verhindern, sich mit UDP Port 10000 zu verbinden, um WebRTC durchzuschleusen, sollte ein TURN Server aufgesetzt werden, über den dann der Verkehr geleitet wird. Wenn UDP Ports möglich sind, stellt der STUN Server eine Peer2Peer Verbinung her. Wenn nicht, wird der Traffic über Port 443 geleitet, was bei jedem restriktiven Netzwerk funktionieren sollte.

⚠ Wichtig ⚠ Der TURN Server kann nicht ohne weiteres über einen Reverse Proxy geleitet werden, weil er nicht auf dem HTTP Protokoll beruht, sondern auf TCP und/oder TLS. Es sollte also am besten ein eigener Server sein, auf dem der Port 443 freigegeben werden kann, ohne auf einen Reverse Proxy zu zeigen.

 

https://hub.docker.com/r/coturn/coturn

https://doganbros.com/blog/turn-server-setup-for-jitsi-on-ubuntu-20-04-tls 

jits meet .env

JVB_STUN_SERVERS=turn.DOMAIN.de:443

TURN_CREDENTIALS=rqj........GhY
TURN_HOST=turn.DOMAIN.de
TURN_PORT=443
#TURNS_HOST=turn.DOMAIN.de
#TURNS_PORT=443

TURN_TRANSPORT=tcp
ENABLE_TURN=1
ENABLE_P2P=1

certbot installieren

sudo apt update
sudo apt install certbot
sudo certbot certonly --standalone --preferred-challenges http -d turn.DOMAIN.de
sudo ufw allow 443

coturn docker-compose.yml

services:
    coturn:
        network_mode: host
        #networks:
         # - jitsi_meet.jitsi
        container_name: coturn
        image: coturn/coturn
        restart: unless-stopped
        volumes:
          - /etc/letsencrypt/live/turn.DOMAIN.de/fullchain.pem:/etc/letsencrypt/live/turn.DOMAIN.de/fullchain.pem
          - /etc/letsencrypt/live/turn.DOMAIN.de/privkey.pem:/etc/letsencrypt/live/turn.DOMAIN.de/privkey.pem
        tmpfs:
          - /var/lib/coturn
        #ports:
          #- 80:3478
          #- 80:3478/udp
          #- 443:5349
          #- 443:5349/udp
          #- 5349:5349
          #- 5349:5349/udp
          #- 3478:3478
          #- 3478:3478/udp
          #- 80:80
          #- 80:80/udp
          #- 443:443
          #- 443:443/udp
        command:
          - --log-file=stdout
          - --verbose
          - --cert=/etc/letsencrypt/live/turn.DOMAIN.de/fullchain.pem
          - --pkey=/etc/letsencrypt/live/turn.DOMAIN.de/privkey.pem
          - --min-port=49160
          - --max-port=49200
          - --listening-port=443
          #- --tls-listening-port=443
          - --fingerprint
          - --no-multicast-peers
          #- --no-udp-relay
          #- --no-udp
          #- --no-tcp-relay
          #- --no-tcp
          - --no-cli
          - --no-tlsv1
          - --no-tlsv1_1
          - --external-ip=116.203.93.143
          - --static-auth-secret=rqj...........[openssl rand -base64 32]...............cGhY
          - --use-auth-secret
          - --realm=turn.DOMAIN.de

#networks:
#  jitsi_meet.jitsi:
#      name: jitsi_meet.jitsi
#      external: true
#      driver: bridge

config testen:

secret=rqjw...........cGhY && time=$(date +%s) && expiry=8400 && username=$(( $time + $expiry )) &&echo username:$username && echo password : $(echo -n $username | openssl dgst -binary -sha1 -hmac $secret | openssl base64)

image.png

und bei trickle-ice  IM FIREFOX (chrome klappt nicht gut) angeben: 

https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/ 

image.png

Docker-Compose .yamls

Stirling PDF


version: '3.3'
services:
  stirling-pdf:
    restart: unless-stopped
    image: stirlingtools/stirling-pdf:0.38.0
    ports:
      - '8080:8080'
    volumes:
      - trainingData:/usr/share/tessdata # Required for extra OCR languages
      - extraConfigs:/configs
      - customFiles:/customFiles/
      - logs:/logs/
      - pipeline:/pipeline/
    environment:
      - DOCKER_ENABLE_SECURITY=false
      - LANGS='de_DE'
      - SYSTEM_DEFAULTLOCALE=de-DE
      - SYSTEM_SHOWUPDATE=false
      - SYSTEM_GOOGLEVISIBILITY=false
      - UI_APPNAME=S-PDF
      - UI_HOMEDESCRIPTION=Stirling PDF
      - UI_APPNAMENAVBAR=S-PDF
      

volumes:
  trainingData:
  extraConfigs:
  customFiles:
  logs:
  pipeline:

Für deutsches OCR muss die Datei deu.traineddata heruntergeladen und in's Volume trainingData kopiert werden

https://github.com/tesseract-ocr/tessdata_fast/blob/main/deu.traineddata 

image.png

Docker-Compose .yamls

DocuSeal (Docusign Alternative)

https://github.com/docusealco/docuseal

services:
  app:
    depends_on:
      postgres:
        condition: service_healthy
    image: docuseal/docuseal:1.9.8
    ports:
      - 3000:3000
    volumes:
      - app:/data/docuseal
    environment:
      - FORCE_SSL=${HOST}
      - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/docuseal

  postgres:
    image: postgres:15
    volumes:
      - './pg_data:/var/lib/postgresql/data'
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: docuseal
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  app:
#
#  caddy:
#    image: caddy:latest
#    command: caddy reverse-proxy --from $HOST --to app:3000
#    ports:
#      - 80:80
#      - 443:443
#      - 443:443/udp
#    volumes:
#      - .:/data
#    environment:
#      - HOST=${HOST}

Beispiel für API-Call zum Unterschreiben

curl --location 'https://sign.DEINEDOMAIN.de/api/submissions' \
       --header 'X-Auth-Token: zFTAbKT36yB...............g3E2af' \
       --data-raw '{
         "template_id": 2,
         "submitters": [
           {
             "name": "Eike Fo",
             "role": "Erste Partei",
             "email": "MAIL@ADRESSE.DE",
             "values": {
               "Name AusstellerIn": "Eike Test"
             }
           },
           { "role": "Second Submitter", "email": "MAIL@ADRESSE.DE" }
         ]
       }'

 

Docker-Compose .yamls

Grafana + Prometheus + Loki

docker-compose.yml

services:
  grafana:
    image: grafana/grafana-oss:11.5.1-ubuntu
    container_name: grafana
    hostname: grafana
    restart: unless-stopped
    environment:
      - GF_SERVER_ROOT_URL=https://grafana.MEINEDOMAIN.de
      - GF_PLUGINS_PREINSTALL=grafana-clock-panel
      - GF_SECURITY_ADMIN_USER=admin
     #- GF_SECURITY_ADMIN_PASSWORD=admin
      - GF_SMTP_ENABLED=true
      - GF_SMTP_HOST=smtp.strato.de:587
      - GF_SMTP_USER=it@MEINEDOMAIN.de
      - GF_SMTP_PASSWORD=MEIN-MAIL-PW
      - GF_SMTP_FROM_ADDRESS=grafana@MEINEDOMAIN.de
      - GF_SMTP_FROM_NAME=Grafana Alerts
    ports:
      - 3030:3000
    volumes:
      - grafana:/var/lib/grafana
    networks:
      grafana-monitoring:

  prometheus:
    image: prom/prometheus:v3.1.0
    container_name: prometheus
    hostname: prometheus
    restart: unless-stopped
    ports:
      - 9090:9090
    volumes:
      - prometheus:/etc/prometheus
    networks:
      grafana-monitoring:

  cadvisor:
    image: gcr.io/cadvisor/cadvisor:v0.47.2
    devices:
        - /dev/kmsg
    privileged: true
    container_name: cadvisor
    hostname: cadvisor
    restart: unless-stopped
    ports:
        - 8088:8080
    volumes:
        - /dev/disk/:/dev/disk:ro
        - /mnt/praxis-volume-01/docker-data/:/var/lib/docker:ro
        - /sys:/sys:ro
        - /var/run:/var/run:ro
        - /:/rootfs:ro
    networks:
      grafana-monitoring:


volumes:
  grafana:
  prometheus:

networks:
  grafana-monitoring:
    driver: bridge

Prometheus Node Exporter

Für alle Hardware-Infos des Nodes (CPU, RAM, Netzwerk etc...)

https://github.com/prometheus/node_exporter 

https://prometheus.io/docs/guides/node-exporter/ 

docker-compose.yml

services:
  node_exporter:
    image: quay.io/prometheus/node-exporter:v1.9.0
    container_name: node_exporter
    command:
      - '--path.rootfs=/host'
    network_mode: host
    pid: host
    restart: unless-stopped
    volumes:
      - '/:/host:ro,rslave'

Prometheus.yml (Prometheus Container)

Danach in das Prometheus Volume gehen und die Prometheus.yml bearbeiten:

# my global config
global:
  scrape_interval: 5s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
  evaluation_interval: 5s # Evaluate rules every 15 seconds. The default is every 1 minute.
  # scrape_timeout is set to the global default (10s).

# Alertmanager configuration
alerting:
  alertmanagers:
    - static_configs:
        - targets:
          # - alertmanager:9093

# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
  # - "first_rules.yml"
  # - "second_rules.yml"

# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
  # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
  - job_name: "prometheus"

    # metrics_path defaults to '/metrics'
    # scheme defaults to 'http'.

    static_configs:
      - targets: ["localhost:9090"]
  - job_name: 'cadvisor'
    static_configs:
      #- targets: ['cadvisor:8080'] 
      # ^ LÄUFT LEIDER NICHT, HOSTNAME KANN NUR KURZ AM ANFANG VOM PROMETHEUS CONTANER MIT NSLOOKUP CADVISOR GEFUNDEN WERDEN, DANACH NICHT MEHR... ICH MACH ES EINFACH UEBER DIE EXTERNE IP ADRESSE. 
      # docker exec -it prometheus /bin/sh
      # nslookup cadvisor
      # Bemerke dass der Host Port 8088 auf intern 8080 umleitet. Wenn du also cadvisor versuchst, nimm den internen Port 8080 und nicht 8088 so wie ich
      - targets: ['hetzner-01.MEINEDOMAIN.de:8088']
      # oder 
      - targets: ['LOKALE-VPS-IP-ZB_10.0.0.3:8088']
  - job_name: node
    static_configs:
      - targets: ['10.0.0.3:9100', '10.0.0.4:9100']

Grafana Dashboards

Node Exporter Full

https://grafana.com/grafana/dashboards/1860-node-exporter-full/ 

image.png

Docker Container (cadvisor)

https://grafana.com/grafana/dashboards/19908-docker-container-monitoring-with-prometheus-and-cadvisor/ 

image.png

Jitsi Meet 

https://grafana.com/grafana/dashboards/12282-jitsi-meet-system/

image.png

Docker-Compose .yamls

Lokale KI mit Ollama + open-WebUI

Quelle: https://www.youtube.com/watch?v=vW29hgdb05I 

abgeändert für AMD Ryzen AI Max+ 395 (dockerimage ollama/ollama:rocm und devices)

2026-03-24

als erstes amdgpu installieren mit 

https://amdgpu-install.readthedocs.io/en/latest/install-overview.html#install-script 

amdgpu-install

danach 

 nano /etc/default/grub

[...]
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash amdgpu.noretry=0"
[...]
sudo update-grub
sudo reboot

auf diese Änderung basiert die env-var HSA_XNACK: 1 in der folgenden docker-compose, erst damit klappte bei meinem Strix Halo die Erkennung der Graka

services:
  ollama:
    image:
        ollama/ollama:0.18.2-rocm
    restart: unless-stopped
    container_name: ollama
    environment:
      #OLLAMA_FLASH_ATTENTION: true
      # setting Context length to 8192 for longer data
      #OLLAMA_CONTEXT_LENGTH: 8192
      #HSA_OVERRIDE_GFX_VERSION: 11.5.1
      #HIP_VISIBLE_DEVICES: 0 # Standard-GPU (0 = erste GPU)
      #ROCR_VISIBLE_DEVICES: "34632"
      OLLAMA_DEBUG: 1
      HSA_XNACK: 1
    volumes:
      - ollama:/root/.ollama
    devices:
      - "/dev/kfd:/dev/kfd"
      - "/dev/dri:/dev/dri"
    group_add:
      - video
      #- render
    ports:
      - "11434:11434"


  open-webui:
    image: ghcr.io/open-webui/open-webui:latest
    container_name: open-webui
    volumes:
      - open-webui:/app/backend/data
    depends_on:
      - ollama
    ports:
      - 3000:8080
    environment:
      - WEBUI_URL=https://ki.MEINEDOMAIN.DE
      - ENABLE_OAUTH_PERSISTENT_CONFIG=false
      - OAUTH_PROVIDER_NAME=Authentik 
      - OAUTH_CLIENT_ID=k4I...NF
      - OAUTH_CLIENT_SECRET=JNnJp31ZqcM5Uu...GWl7DHw5qCi0hz0
      - OPENID_PROVIDER_URL=https://auth.MEINEDOMAIN.DE/application/o/<MEIN-SLUG-NAME>/.well-known/openid-configuration
      - OPENID_REDIRECT_URI=https://ki.MEINEDOMAIN.DE/oauth/oidc/callback
      
      # Allows auto-creation of new users using OAuth. Must be paired with ENABLE_LOGIN_FORM=false.
      - ENABLE_OAUTH_SIGNUP=true

      # Disables user/password login form. Required when ENABLE_OAUTH_SIGNUP=true.
      # Nachtrag von Eike: Required stimmt nicht ganz, es geht auch ENABLE_OAUTH_SIGNUP=true UND ENABLE_LOGIN_FORM=true
      - ENABLE_LOGIN_FORM=true
      
      - OAUTH_MERGE_ACCOUNTS_BY_EMAIL=true
      - OAUTH_SCOPES=openid email profile
      - OLLAMA_BASE_URL=http://ollama:11434
      - WEBUI_SECRET_KEY=3ba8.....<openssl rand -hex 24>...3a70124
    extra_hosts:
      - host.docker.internal:host-gateway
    restart: unless-stopped

volumes:
  ollama:
  open-webui:

 

Mit Authentik verbinden (OIDC)

https://integrations.goauthentik.io/miscellaneous/open-webui/ 

https://docs.openwebui.com/features/access-security/auth/sso/ 

https://docs.openwebui.com/troubleshooting/sso/ 

Einfach provider in Authentik einstellen mit standard settings (email openid profile) und als redirect-uri strict die gleiche URI wie in der env var OPENID_REDIRECT_URI aus der docker-compose:

https://ki.MEINEDOMAIN.DE/oauth/oidc/callback

 

 

2026-03-22 (mit image von rjmalagon)

Problem: ROCm im neuesten rjmalagon-image (v0.18.0.2) ist nicht rocm 7.x sondern 6.x enthalten.
Das aktuellste ROCm-7 Image (optm-rocm7-latest) hat zwar rocm 7.x, ist allerdings zu alt, um neuere MoE Modelle zu unterstützen wie qwen3.5 oder nemotron-cascade-2. Lösung siehe oben mit ollama aktuell und HSA_XNACK=1

https://community.frame.work/t/running-ollama-in-docker-on-our-framework-desktop-using-the-gpu/75662/9 < ollama compose part aus diesem thread

https://github.com/rjmalagon/ollama-linux-amd-apu < dieses image

https://github.com/phueper/ollama-linux-amd-apu/tree/apu-optimizer < anderer fork

https://www.reddit.com/r/ollama/comments/1nt5fcr/how_do_i_get_ollama_to_use_the_igpu_on_the_amd_ai/ 

https://github.com/rjmalagon/ollama-linux-amd-apu/issues/24 

https://github.com/ollama/ollama/pull/13000 

docker-compose.yml

services:
  ollama:
    image:
        ghcr.io/rjmalagon/ollama-linux-amd-apu:optm-rocm7-latest
    restart: unless-stopped
    environment:
      OLLAMA_FLASH_ATTENTION: true
      OLLAMA_DEBUG: 0
      # setting Context length to 8192 for longer data
      OLLAMA_CONTEXT_LENGTH: 8192
    volumes:
      - ollama_storage:/root/.ollama
    devices:
      - "/dev/kfd:/dev/kfd"
      - "/dev/dri:/dev/dri"
    group_add:
      - video
    ports:
      - "11434:11434"

  open-webui:
    image: ghcr.io/open-webui/open-webui:latest
    container_name: open-webui
    volumes:
      - open-webui:/app/backend/data
    depends_on:
      - ollama
    ports:
      - 3000:8080
    environment:
      - 'OLLAMA_BASE_URL=http://ollama:11434'
      - 'WEBUI_SECRET_KEY='
    extra_hosts:
      - host.docker.internal:host-gateway
    restart: unless-stopped

volumes:
  ollama_storage:
  open-webui:

alt (mit ollama image)

services:
  ollama:
    # Uncomment below for GPU support
    # deploy:
    #   resources:
    #     reservations:
    #       devices:
    #         - driver: nvidia
    #           count: 1
    #           capabilities:
    #             - gpu
    
    # AMD GPU:
    devices:
      - /dev/kfd
      - /dev/dri
    volumes:
      - ollama:/root/.ollama
    # Uncomment below to expose Ollama API outside the container stack
    # ports:
    #   - 11434:11434
    container_name: ollama
    pull_policy: always
    tty: true
    restart: unless-stopped
    #image: ollama/ollama
    # AMD GPU:
    image: ollama/ollama:rocm
    #networks:
    #  - ollama-network


  open-webui:
#    build:
#      context: .
#      args:
#       OLLAMA_BASE_URL: '/ollama'
#      dockerfile: Dockerfile
    image: ghcr.io/open-webui/open-webui:latest
    container_name: open-webui
    volumes:
      - open-webui:/app/backend/data
    depends_on:
      - ollama
    ports:
      - 3000:8080
    environment:
      - 'OLLAMA_BASE_URL=http://ollama:11434'
      - 'WEBUI_SECRET_KEY='
    extra_hosts:
      - host.docker.internal:host-gateway
    restart: unless-stopped
    #networks:
    #  - ollama-network
    #  - proxy
    #labels:
    #  - "traefik.enable=true"
    #  - "traefik.docker.network=proxy"
    #  - "traefik.http.routers.ollama.entrypoints=http"
    #  - "traefik.http.routers.ollama.rule=Host(`ollama.jimsgarage.co.uk`)"
    #  - "traefik.http.middlewares.ollama-https-redirect.redirectscheme.scheme=https"
    #  - "traefik.http.routers.ollama.middlewares=ollama-https-redirect"
    #  - "traefik.http.routers.ollama-secure.entrypoints=https"
    #  - "traefik.http.routers.ollama-secure.rule=Host(`ollama.jimsgarage.co.uk`)"
    #  - "traefik.http.routers.ollama-secure.tls=true"
    #  - "traefik.http.routers.ollama-secure.tls.certresolver=cloudflare"
    #  - "traefik.http.routers.ollama-secure.service=ollama"
    #  - "traefik.http.services.ollama.loadbalancer.server.port=8080"

volumes:
  ollama:
  open-webui:

#networks:
  #ollama-network:
  #proxy:
  #  external: true

Docker-Compose .yamls

Hoarder (Bookmark-Senke)

 

services:
  web:
    image: ghcr.io/hoarder-app/hoarder:${HOARDER_VERSION:-release}
    restart: unless-stopped
    volumes:
      # By default, the data is stored in a docker volume called "data".
      # If you want to mount a custom directory, change the volume mapping to:
      # - /path/to/your/directory:/data
      - data:/data
    ports:
      - 3023:3000
    env_file:
      - stack.env
    environment:
      MEILI_ADDR: http://meilisearch:7700
      BROWSER_WEB_URL: http://chrome:9222
      # OPENAI_API_KEY: ...

      # You almost never want to change the value of the DATA_DIR variable.
      # If you want to mount a custom directory, change the volume mapping above instead.
      DATA_DIR: /data # DON'T CHANGE THIS
  chrome:
    image: gcr.io/zenika-hub/alpine-chrome:123
    restart: unless-stopped
    command:
      - --no-sandbox
      - --disable-gpu
      - --disable-dev-shm-usage
      - --remote-debugging-address=0.0.0.0
      - --remote-debugging-port=9222
      - --hide-scrollbars
  meilisearch:
    image: getmeili/meilisearch:v1.11.1
    restart: unless-stopped
    env_file:
      - stack.env
    environment:
      MEILI_NO_ANALYTICS: "true"
    volumes:
      - meilisearch:/meili_data

volumes:
  meilisearch:
  data:

.env

DATA_DIR=/data
MEILI_ADDR=http://127.0.0.1:7700
MEILI_MASTER_KEY=m2l...........VU3e
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=8Fz..............c6qU
DISABLE_SIGNUPS=true
OPENAI_API_KEY=sk-svcacct-PZe_..........BO9AA

 

Docker-Compose .yamls

Nextcloud AIO

Nextcloud All-In-One Manual-Install 

Manual-install statt Auto-install mit AIO-Webinterface, um Möglichkeiten wie admin-pw, container-versionen, SSE, S3 Primary Storage usw bearbeiten zu können und es zu einem späteren Zeitpunkt besser zu Kubernetes/Swarm übertragen zu können.

Quelle: https://github.com/nextcloud/all-in-one/tree/main/manual-install 

Aktuellste Images: https://github.com/orgs/nextcloud-releases/packages?repo_name=all-in-one 

Befehle

# nextcloud-aio repo klonen
git clone https://github.com/nextcloud/all-in-one.git
cd all-in-one/manual-install

# dateien kopieren und umbenennen (auch damit sie beim updaten nicht überschrieben werden)
cp sample.conf .env
cp latest.yml compose.yaml

# bearbeiten
nano .env
nano compose.yaml

# mit docker hochfahren
# sudo docker compose --profile collabora --profile talk --profile talk-recording --profile clamav --profile imaginary --profile fulltextsearch --profile whiteboard up
docker compose --profile collabora --profile clamav --profile imaginary --profile fulltextsearch --profile whiteboard up -d --force-recreate

Allgemeine Befehle zb für Updates der Container

# Container stoppen
docker compose --profile collabora --profile clamav --profile imaginary --profile fulltextsearch --profile whiteboard down

# Updaten
docker compose --profile collabora --profile clamav --profile imaginary --profile fulltextsearch --profile whiteboard pull

# Container neustarten
docker compose --profile collabora --profile clamav --profile imaginary --profile fulltextsearch --profile whiteboard up -d --force-recreate -t 30

Resultierende Dateien

Eikes .env
(updated 2026-01-21)

Änderungen: Secrets & PWs, optionale Container enabled, Nextcloud PHP Memory auf 2048MB statt 512, Apache-Additional-Network für reverse-proxy von anderem server, skip-domain-validation für einfacheres debugging, pgadmin logindaten

# SECRETS UND PWs MIT 'openssl rand -hex 24' erstellen!

DATABASE_PASSWORD=c9ba4dc404          # TODO! This needs to be a unique and good password!
FULLTEXTSEARCH_PASSWORD=5a2e38d          # TODO! This needs to be a unique and good password!
IMAGINARY_SECRET=b8e9d34b1          # TODO! This needs to be a unique and good password!
NC_DOMAIN=nextcloud.MEINEDOMAIN.de          # TODO! Needs to be changed to the domain that you want to use for Nextcloud.
NEXTCLOUD_PASSWORD=e1dae5          # TODO! This is the password of the initially created Nextcloud admin with username "admin".
ONLYOFFICE_SECRET=aaf8          # TODO! This needs to be a unique and good password!
RECORDING_SECRET=88ff5          # TODO! This needs to be a unique and good password!
REDIS_PASSWORD=0aaff3b99c          # TODO! This needs to be a unique and good password!
SIGNALING_SECRET=98467          # TODO! This needs to be a unique and good password!
TALK_INTERNAL_SECRET=4d67b6       # TODO! This needs to be a unique and good password!
TIMEZONE=Europe/Berlin          # TODO! This is the timezone that your containers will use.
TURN_SECRET=b1ebb03d63          # TODO! This needs to be a unique and good password!
WHITEBOARD_SECRET=d7135          # TODO! This needs to be a unique and good password!

CLAMAV_ENABLED="yes"          # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically.
COLLABORA_ENABLED="yes"          # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically.
FULLTEXTSEARCH_ENABLED="yes"          # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically.
IMAGINARY_ENABLED="yes"          # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically.
ONLYOFFICE_ENABLED="no"          # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically.
TALK_ENABLED="no"          # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically.
TALK_RECORDING_ENABLED="no"          # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically.
WHITEBOARD_ENABLED="yes"          # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically.

APACHE_IP_BINDING=0.0.0.0          # This can be changed to e.g. 127.0.0.1 if you want to run AIO behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else) and if that is running on the same host and using localhost to connect
APACHE_MAX_SIZE=17179869184          # This needs to be an integer and in sync with NEXTCLOUD_UPLOAD_LIMIT
APACHE_PORT=11000          # Changing this to a different value than 443 will allow you to run it behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else).
ADDITIONAL_COLLABORA_OPTIONS=['--o:security.seccomp=true']          # You can add additional collabora options here by using the array syntax.
COLLABORA_DICTIONARIES="de_DE en_GB en_US es_ES fr_FR it nl pt_BR pt_PT ru"        # You can change this in order to enable other dictionaries for collabora
FULLTEXTSEARCH_JAVA_OPTIONS="-Xms512M -Xmx512M"          # Allows to adjust the fulltextsearch java options.
INSTALL_LATEST_MAJOR=no        # Setting this to yes will install the latest Major Nextcloud version upon the first installation
NEXTCLOUD_ADDITIONAL_APKS=imagemagick        # This allows to add additional packages to the Nextcloud container permanently. Default is imagemagick but can be overwritten by modifying this value.
NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS=imagick        # This allows to add additional php extensions to the Nextcloud container permanently. Default is imagick but can be overwritten by modifying this value.
NEXTCLOUD_DATADIR=nextcloud_aio_nextcloud_data          # You can change this to e.g. "/mnt/ncdata" to map it to a location on your host. It needs to be adjusted before the first startup and never afterwards!
NEXTCLOUD_MAX_TIME=3600          # This allows to change the upload time limit of the Nextcloud container
NEXTCLOUD_MEMORY_LIMIT=2048M          # This allows to change the PHP memory limit of the Nextcloud container
NEXTCLOUD_MOUNT=/mnt/          # This allows the Nextcloud container to access directories on the host. It must never be equal to the value of NEXTCLOUD_DATADIR!
NEXTCLOUD_STARTUP_APPS="deck twofactor_totp tasks calendar contacts notes user_oidc external files_accesscontrol files_linkeditor drawio"        # Allows to modify the Nextcloud apps that are installed on starting AIO the first time
NEXTCLOUD_TRUSTED_CACERTS_DIR=/usr/local/share/ca-certificates/my-custom-ca          # Nextcloud container will trust all the Certification Authorities, whose certificates are included in the given directory.
NEXTCLOUD_UPLOAD_LIMIT=16G          # This allows to change the upload limit of the Nextcloud container
REMOVE_DISABLED_APPS=no        # Setting this to no keep Nextcloud apps that are disabled via their switch and not uninstall them if they should be installed in Nextcloud.
TALK_PORT=3478          # This allows to adjust the port that the talk container is using. It should be set to something higher than 1024! Otherwise it might not work!
UPDATE_NEXTCLOUD_APPS="no"          # When setting to "yes" (with quotes), it will automatically update all installed Nextcloud apps upon container startup on saturdays.

APACHE_ADDITIONAL_NETWORK=""
SKIP_DOMAIN_VALIDATION=true

PGADMIN_DEFAULT_EMAIL=MAIL@MEINEDOMAIN.de
PGADMIN_DEFAULT_PASSWORD=MEIN-PGADMIN-WEBUI-PW (oder mit <openssl rand -hex 24> erzeugt)

# seit Update auf Nextcloud Hub 10 (Nextcloud 31.x) startete das entrypoint.sh Skript nicht mehr, weil es die Redis-DB nicht erreichen konnte weil die var REDIS_PORT fehlte. In der docker-compose.yml bzw. compose.yml unbedingt im Container nextcloud-aio-nextcloud als environment abspeichern. siehe compose.yml weiter unten im wiki
REDIS_PORT=6379

Eikes compose.yaml
(updated 2026-01-21)

Änderungen:

name: ncaio

services:
  nextcloud-aio-apache:
    depends_on:
      nextcloud-aio-onlyoffice:
        condition: service_started
        required: false
      nextcloud-aio-collabora:
        condition: service_started
        required: false
      nextcloud-aio-talk:
        condition: service_started
        required: false
      nextcloud-aio-notify-push:
        condition: service_started
        required: false
      nextcloud-aio-whiteboard:
        condition: service_started
        required: false
      nextcloud-aio-nextcloud:
        condition: service_started
        required: false
    image: ghcr.io/nextcloud-releases/aio-apache:latest
    user: "33"
    init: true
    healthcheck:
      start_period: 0s
      test: /healthcheck.sh
      interval: 30s
      timeout: 30s
      start_interval: 5s
      retries: 3
    ports:
      - ${APACHE_IP_BINDING}:${APACHE_PORT}:${APACHE_PORT}/tcp
      - ${APACHE_IP_BINDING}:${APACHE_PORT}:${APACHE_PORT}/udp
    environment:
      - NC_DOMAIN
      - NEXTCLOUD_HOST=nextcloud-aio-nextcloud
      - APACHE_HOST=nextcloud-aio-apache
      - COLLABORA_HOST=nextcloud-aio-collabora
      - TALK_HOST=nextcloud-aio-talk
      - APACHE_PORT
      - ONLYOFFICE_HOST=nextcloud-aio-onlyoffice
      - TZ=${TIMEZONE}
      - APACHE_MAX_SIZE
      - APACHE_MAX_TIME=${NEXTCLOUD_MAX_TIME}
      - NOTIFY_PUSH_HOST=nextcloud-aio-notify-push
      - WHITEBOARD_HOST=nextcloud-aio-whiteboard
    volumes:
      - nextcloud_aio_nextcloud:/var/www/html:ro
      - nextcloud_aio_apache:/mnt/data:rw
    restart: unless-stopped
    read_only: true
    tmpfs:
      - /var/log/supervisord
      - /var/run/supervisord
      - /usr/local/apache2/logs
      - /tmp
      - /home/www-data
    cap_drop:
      - NET_RAW

  nextcloud-aio-database:
    image: ghcr.io/nextcloud-releases/aio-postgresql:latest
    user: "999"
    init: true
    healthcheck:
      start_period: 0s
      test: /healthcheck.sh
      interval: 30s
      timeout: 30s
      start_interval: 5s
      retries: 3
    expose:
      - "5432"
    volumes:
      - nextcloud_aio_database:/var/lib/postgresql/data:rw
      - nextcloud_aio_database_dump:/mnt/data:rw
    environment:
      - POSTGRES_PASSWORD=${DATABASE_PASSWORD}
      - POSTGRES_DB=nextcloud_database
      - POSTGRES_USER=nextcloud
      - TZ=${TIMEZONE}
      - PGTZ=${TIMEZONE}
    stop_grace_period: 1800s
    restart: unless-stopped
    shm_size: 268435456
    read_only: true
    tmpfs:
      - /var/run/postgresql
    cap_drop:
      - NET_RAW

  nextcloud-aio-nextcloud:
    depends_on:
      nextcloud-aio-database:
        condition: service_started
        required: false
      nextcloud-aio-redis:
        condition: service_started
        required: false
      nextcloud-aio-clamav:
        condition: service_started
        required: false
      nextcloud-aio-fulltextsearch:
        condition: service_started
        required: false
      nextcloud-aio-talk-recording:
        condition: service_started
        required: false
      nextcloud-aio-imaginary:
        condition: service_started
        required: false
      nextcloud-aio-pgadmin:
        condition: service_started
        required: false
    image: ghcr.io/nextcloud-releases/aio-nextcloud:latest
    init: true
    healthcheck:
      start_period: 0s
      test: /healthcheck.sh
      interval: 30s
      timeout: 30s
      start_interval: 5s
      retries: 3
    expose:
      - "9000"
      - "9001"
    volumes:
      - nextcloud_aio_nextcloud:/var/www/html:rw
      - ${NEXTCLOUD_DATADIR}:/mnt/ncdata:rw
      - ${NEXTCLOUD_MOUNT}:${NEXTCLOUD_MOUNT}:rw
      - ${NEXTCLOUD_TRUSTED_CACERTS_DIR}:/usr/local/share/ca-certificates:ro
    environment:
      - NEXTCLOUD_HOST=nextcloud-aio-nextcloud
      - POSTGRES_HOST=nextcloud-aio-database
      - POSTGRES_PORT=5432
      - POSTGRES_PASSWORD=${DATABASE_PASSWORD}
      - POSTGRES_DB=nextcloud_database
      - POSTGRES_USER=nextcloud
      - REDIS_HOST=nextcloud-aio-redis
      - REDIS_HOST_PASSWORD=${REDIS_PASSWORD}
      - APACHE_HOST=nextcloud-aio-apache
      - APACHE_PORT
      - NC_DOMAIN
      - ADMIN_USER=admin
      - ADMIN_PASSWORD=${NEXTCLOUD_PASSWORD}
      - NEXTCLOUD_DATA_DIR=/mnt/ncdata
      - OVERWRITEHOST=${NC_DOMAIN}
      - OVERWRITEPROTOCOL=https
      - TURN_SECRET
      - SIGNALING_SECRET
      - ONLYOFFICE_SECRET
      - NEXTCLOUD_MOUNT
      - CLAMAV_ENABLED
      - CLAMAV_HOST=nextcloud-aio-clamav
      - ONLYOFFICE_ENABLED
      - COLLABORA_ENABLED
      - COLLABORA_HOST=nextcloud-aio-collabora
      - TALK_ENABLED
      - ONLYOFFICE_HOST=nextcloud-aio-onlyoffice
      - UPDATE_NEXTCLOUD_APPS
      - TZ=${TIMEZONE}
      - TALK_PORT
      - IMAGINARY_ENABLED
      - IMAGINARY_HOST=nextcloud-aio-imaginary
      - CLAMAV_MAX_SIZE=${APACHE_MAX_SIZE}
      - PHP_UPLOAD_LIMIT=${NEXTCLOUD_UPLOAD_LIMIT}
      - PHP_MEMORY_LIMIT=${NEXTCLOUD_MEMORY_LIMIT}
      - FULLTEXTSEARCH_ENABLED
      - FULLTEXTSEARCH_HOST=nextcloud-aio-fulltextsearch
      - FULLTEXTSEARCH_PORT=9200
      - FULLTEXTSEARCH_USER=elastic
      - FULLTEXTSEARCH_INDEX=nextcloud-aio
      - PHP_MAX_TIME=${NEXTCLOUD_MAX_TIME}
      - TRUSTED_CACERTS_DIR=${NEXTCLOUD_TRUSTED_CACERTS_DIR}
      - STARTUP_APPS=${NEXTCLOUD_STARTUP_APPS}
      - ADDITIONAL_APKS=${NEXTCLOUD_ADDITIONAL_APKS}
      - ADDITIONAL_PHP_EXTENSIONS=${NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS}
      - INSTALL_LATEST_MAJOR
      - TALK_RECORDING_ENABLED
      - RECORDING_SECRET
      - TALK_RECORDING_HOST=nextcloud-aio-talk-recording
      - FULLTEXTSEARCH_PASSWORD
      - REMOVE_DISABLED_APPS
      - IMAGINARY_SECRET
      - WHITEBOARD_SECRET
      - WHITEBOARD_ENABLED
      - REDIS_PORT=${REDIS_PORT}
    stop_grace_period: 600s
    restart: unless-stopped
    cap_drop:
      - NET_RAW

  nextcloud-aio-notify-push:
    image: ghcr.io/nextcloud-releases/aio-notify-push:latest
    user: "33"
    init: true
    healthcheck:
      start_period: 0s
      test: /healthcheck.sh
      interval: 30s
      timeout: 30s
      start_interval: 5s
      retries: 3
    expose:
      - "7867"
    volumes:
      - nextcloud_aio_nextcloud:/nextcloud:ro
    environment:
      - NC_DOMAIN
      - NEXTCLOUD_HOST=nextcloud-aio-nextcloud
      - TZ=${TIMEZONE}
      - REDIS_HOST=nextcloud-aio-redis
      - REDIS_HOST_PASSWORD=${REDIS_PASSWORD}
      - POSTGRES_HOST=nextcloud-aio-database
      - POSTGRES_PORT=5432
      - POSTGRES_PASSWORD=${DATABASE_PASSWORD}
      - POSTGRES_DB=nextcloud_database
      - POSTGRES_USER=nextcloud
    restart: unless-stopped
    read_only: true
    cap_drop:
      - NET_RAW

  nextcloud-aio-redis:
    image: ghcr.io/nextcloud-releases/aio-redis:latest
    user: "999"
    init: true
    healthcheck:
      start_period: 0s
      test: /healthcheck.sh
      interval: 30s
      timeout: 30s
      start_interval: 5s
      retries: 3
    expose:
      - "6379"
    environment:
      - REDIS_HOST_PASSWORD=${REDIS_PASSWORD}
      - TZ=${TIMEZONE}
    volumes:
      - nextcloud_aio_redis:/data:rw
    restart: unless-stopped
    read_only: true
    cap_drop:
      - NET_RAW

  nextcloud-aio-collabora:
    command: ${ADDITIONAL_COLLABORA_OPTIONS}
    image: ghcr.io/nextcloud-releases/aio-collabora:latest
    init: true
    healthcheck:
      start_period: 60s
      test: /healthcheck.sh
      interval: 30s
      timeout: 30s
      start_interval: 5s
      retries: 9
    expose:
      - "9980"
    environment:
      - aliasgroup1=https://${NC_DOMAIN}:443
      - extra_params=--o:ssl.enable=false --o:ssl.termination=true --o:mount_jail_tree=false --o:logging.level=warning --o:logging.level_startup=warning --o:home_mode.enable=true --o:remote_font_config.url=https://${NC_DOMAIN}/apps/richdocuments/settings/fonts.json --o:net.post_allow.host[0]=.+
      - dictionaries=${COLLABORA_DICTIONARIES}
      - TZ=${TIMEZONE}
      - server_name=${NC_DOMAIN}
      - DONT_GEN_SSL_CERT=1
    restart: unless-stopped
    profiles:
      - collabora
    cap_add:
      - MKNOD
      - SYS_ADMIN
      - CHOWN
    cap_drop:
      - NET_RAW

  nextcloud-aio-talk:
    image: ghcr.io/nextcloud-releases/aio-talk:latest
    user: "1000"
    init: true
    healthcheck:
      start_period: 0s
      test: /healthcheck.sh
      interval: 30s
      timeout: 30s
      start_interval: 5s
      retries: 3
    ports:
      - ${TALK_PORT}:${TALK_PORT}/tcp
      - ${TALK_PORT}:${TALK_PORT}/udp
    expose:
      - "8081"
    environment:
      - NC_DOMAIN
      - TALK_HOST=nextcloud-aio-talk
      - TURN_SECRET
      - SIGNALING_SECRET
      - TZ=${TIMEZONE}
      - TALK_PORT
      - INTERNAL_SECRET=${TALK_INTERNAL_SECRET}
    restart: unless-stopped
    profiles:
      - talk
      - talk-recording
    read_only: true
    tmpfs:
      - /var/log/supervisord
      - /var/run/supervisord
      - /opt/eturnal/run
      - /conf
      - /tmp
    cap_drop:
      - NET_RAW

  nextcloud-aio-talk-recording:
    image: ghcr.io/nextcloud-releases/aio-talk-recording:latest
    user: "122"
    init: true
    healthcheck:
      start_period: 0s
      test: /healthcheck.sh
      interval: 30s
      timeout: 30s
      start_interval: 5s
      retries: 3
    expose:
      - "1234"
    environment:
      - NC_DOMAIN
      - TZ=${TIMEZONE}
      - RECORDING_SECRET
      - INTERNAL_SECRET=${TALK_INTERNAL_SECRET}
    volumes:
      - nextcloud_aio_talk_recording:/tmp:rw
    shm_size: 2147483648
    restart: unless-stopped
    profiles:
      - talk-recording
    read_only: true
    tmpfs:
      - /conf
    cap_drop:
      - NET_RAW

  nextcloud-aio-clamav:
    image: ghcr.io/nextcloud-releases/aio-clamav:latest
    user: "100"
    init: false
    healthcheck:
      start_period: 60s
      test: /healthcheck.sh
      interval: 30s
      timeout: 30s
      start_interval: 5s
      retries: 9
    expose:
      - "3310"
    environment:
      - TZ=${TIMEZONE}
      - MAX_SIZE=${NEXTCLOUD_UPLOAD_LIMIT}
    volumes:
      - nextcloud_aio_clamav:/var/lib/clamav:rw
    restart: unless-stopped
    profiles:
      - clamav
    read_only: true
    tmpfs:
      - /tmp
      - /var/log/clamav
      - /run/clamav
      - /var/log/supervisord
      - /var/run/supervisord
    cap_drop:
      - NET_RAW

  nextcloud-aio-onlyoffice:
    image: ghcr.io/nextcloud-releases/aio-onlyoffice:latest
    init: true
    healthcheck:
      start_period: 60s
      test: /healthcheck.sh
      interval: 30s
      timeout: 30s
      start_interval: 5s
      retries: 9
    expose:
      - "80"
    environment:
      - TZ=${TIMEZONE}
      - JWT_ENABLED=true
      - JWT_HEADER=AuthorizationJwt
      - JWT_SECRET=${ONLYOFFICE_SECRET}
    volumes:
      - nextcloud_aio_onlyoffice:/var/lib/onlyoffice:rw
    restart: unless-stopped
    profiles:
      - onlyoffice
    cap_drop:
      - NET_RAW

  nextcloud-aio-imaginary:
    image: ghcr.io/nextcloud-releases/aio-imaginary:latest
    user: "65534"
    init: true
    healthcheck:
      start_period: 0s
      test: /healthcheck.sh
      interval: 30s
      timeout: 30s
      start_interval: 5s
      retries: 3
    expose:
      - "9000"
    environment:
      - TZ=${TIMEZONE}
      - IMAGINARY_SECRET
    restart: unless-stopped
    cap_add:
      - SYS_NICE
    cap_drop:
      - NET_RAW
    profiles:
      - imaginary
    read_only: true
    tmpfs:
      - /tmp

  nextcloud-aio-fulltextsearch:
    image: ghcr.io/nextcloud-releases/aio-fulltextsearch:latest
    init: false
    healthcheck:
      start_period: 60s
      test: /healthcheck.sh
      interval: 10s
      timeout: 5s
      start_interval: 5s
      retries: 5
    expose:
      - "9200"
    environment:
      - TZ=${TIMEZONE}
      - ES_JAVA_OPTS=${FULLTEXTSEARCH_JAVA_OPTIONS}
      - bootstrap.memory_lock=true
      - cluster.name=nextcloud-aio
      - discovery.type=single-node
      - logger.level=WARN
      - http.port=9200
      - xpack.license.self_generated.type=basic
      - xpack.security.enabled=false
      - FULLTEXTSEARCH_PASSWORD
    volumes:
      - nextcloud_aio_elasticsearch:/usr/share/elasticsearch/data:rw
    restart: unless-stopped
    profiles:
      - fulltextsearch
    cap_drop:
      - NET_RAW

  nextcloud-aio-whiteboard:
    image: ghcr.io/nextcloud-releases/aio-whiteboard:latest
    user: "65534"
    init: true
    healthcheck:
      start_period: 0s
      test: /healthcheck.sh
      interval: 30s
      timeout: 30s
      start_interval: 5s
      retries: 3
    expose:
      - "3002"
    tmpfs:
      - /tmp
    environment:
      - TZ=${TIMEZONE}
      - NEXTCLOUD_URL=https://${NC_DOMAIN}
      - JWT_SECRET_KEY=${WHITEBOARD_SECRET}
      - STORAGE_STRATEGY=redis
      - REDIS_HOST=nextcloud-aio-redis
      - REDIS_HOST_PASSWORD=${REDIS_PASSWORD}
      - BACKUP_DIR=/tmp
    restart: unless-stopped
    profiles:
      - whiteboard
    read_only: true
    cap_drop:
      - NET_RAW

  nextcloud-aio-pgadmin:
#    container_name: nextcloud-aio_pgadmin
    image: dpage/pgadmin4:9.6.0
    volumes:
       - pgadmin:/root/.pgadmin
    ports:
      - 5050:80
    environment:
      - PGADMIN_DEFAULT_EMAIL=${PGADMIN_DEFAULT_EMAIL}
      - PGADMIN_DEFAULT_PASSWORD=${PGADMIN_DEFAULT_PASSWORD}
    restart: unless-stopped


volumes:
  nextcloud_aio_apache:
    name: nextcloud_aio_apache
  nextcloud_aio_clamav:
    name: nextcloud_aio_clamav
  nextcloud_aio_database:
    name: nextcloud_aio_database
  nextcloud_aio_database_dump:
    name: nextcloud_aio_database_dump
  nextcloud_aio_elasticsearch:
    name: nextcloud_aio_elasticsearch
  nextcloud_aio_nextcloud:
    name: nextcloud_aio_nextcloud
  nextcloud_aio_onlyoffice:
    name: nextcloud_aio_onlyoffice
  nextcloud_aio_redis:
    name: nextcloud_aio_redis
  nextcloud_aio_talk_recording:
    name: nextcloud_aio_talk_recording
  nextcloud_aio_nextcloud_data:
    name: nextcloud_aio_nextcloud_data
  pgadmin:
    name: nextcloud_aio_pgadmin

networks:
  default:
    driver: bridge

Config.php

...

  'trusted_proxies' =>
  array (
    0 => '127.0.0.1',
    1 => '::1',
    2 => '10.0.0.0/24',
    10 => '172.21.0.0/16',
  ),

...

  'trusted_domains' =>
  array (
    0 => 'localhost',
    1 => 'nextcloud.MEINEDOMAIN.de',
    2 => 'dev.nextcloud.MEINEDOMAIN.de',
  ),
    
...

  'share_folder' => '/',
  'skeletondirectory' => '',

  'defaultapp' => 'files',
  'default_language' => 'de',
  'default_locale' => 'de',
  'default_phone_region' => 'DE',
  'default_timezone' => 'Europe/Berlin',
  'knowledgebaseenabled' => false,
  'lost_password_link' => 'disabled',
  'simpleSignUpLink.shown' => false,
  'loglevel' => 2,
  'mail_from_address' => 'admin',
  'mail_smtpmode' => 'smtp',
  'mail_sendmailmode' => 'smtp',
  'mail_domain' => 'MEINEDOMAIN.de',
  'mail_smtphost' => 'smtp.strato.de',
  'mail_smtpauth' => 1,
  'mail_smtpport' => '587',
  'mail_smtpname' => 'admin@MEINEDOMAIN.de',
  'mail_smtppassword' => 'meintollespw123',
  'maintenance' => false,
  'default_charset' => 'UTF-8',
  'sharing.force_share_accept' => 'false',
  'sharing.enable_share_accept' => 'false',
  'forbidden_filename_characters' =>
  array (
    0 => '?',
    1 => '<',
    2 => '>',
    3 => ':',
    4 => '*',
    5 => '|',
    6 => '"',
    7 => '/',
    8 => '§',
    9 => '^',
  ),
)

Installierte Apps

siehe .env Datei oben:

NEXTCLOUD_STARTUP_APPS="... user_oidc external files_accesscontrol files_linkeditor drawio" 

Deaktivierte Apps

Im Menu der UI per Hand deaktiviert:

Weitere Befehle

im nextcloud container mit docker exec -it nc-nextcloud-aio-nextcloud-1 /bin/bash

php occ maintenance:repair --include-expensive
php occ files:scan --all
# https://github.com/nextcloud/user_oidc?tab=readme-ov-file#disable-other-login-methods
docker exec -it -u 33 nc-nextcloud-aio-nextcloud-1 /bin/bash
php occ config:app:set --value=0 user_oidc allow_multiple_user_backends

Logs

...

Update

Um die Nextcloud-AIO Manual Installation Variante upzudaten, bedarf es dem update-yaml.sh Skript im Repo mit der Anleitung dazu:

https://github.com/nextcloud/all-in-one/blob/main/manual-install/readme.md#how-to-update 

How to update?
Since the AIO containers may change in the future, it is highly recommended to strictly follow the following procedure whenever you want to upgrade your containers.

  1. If your previous copy of sample.conf is named my.conf, run mv -vn my.conf .env in order to rename the file to .env.
  2. Run sudo docker compose down to stop all running containers
  3. Back up all important files and folders
  4. If your compose file is still named docker-compose.yml rename it to compose.yaml by running mv -vn docker-compose.yml compose.yaml
  5. Run git pull in order to get the updated yaml files from the repository. Now bring your compose.yaml file up-to-date with the updated one from the repository. You can use diff compose.yaml latest.yml for comparing. ⚠️ Please note: Starting with AIO v5.1.0, ipv6 networking will be enabled by default, so make sure to either enable it first by following steps 1 and 2 of https://github.com/nextcloud/all-in-one/blob/main/docker-ipv6-support.md and then proceed with the steps below or disable ipv6 networking by editing the compose.yaml file and removing ipv6 from the network.
  6. Also have a look at the sample.conf if any variable was added or renamed and add that to your conf file as well. Here may help the diff command as well.
  7. After the file update was successful, simply run sudo docker compose pull to pull the new images.
  8. At the end run sudo docker compose up in order to start and update the containers with the new configuration.

2026-01-20 Fehler: redis möchte nicht starten

WARNING Memory overcommit must be enabled! Without it, a background save or replication may fail under low memory condition. Being disabled, it can also cause failures without low memory condition

Lösung:

https://github.com/nextcloud/all-in-one/discussions/1731

# auf dem host außerhalb docker:
echo "vm.overcommit_memory = 1" | sudo tee /etc/sysctl.d/nextcloud-aio-memory-overcommit.conf
sysctl "vm.overcommit_memory=1"
# restart aio container zb mit
docker compose --profile collabora --profile clamav --profile imaginary --profile fulltextsearch --profile whiteboard up -d --force-recreate

2026-01-21 Fehler: Nextcloud Container findet Redis-Service nicht

logs:

Waiting for Redis to start...
nc: service "" unknown
Waiting for Redis to start...
nc: service "" unknown
Waiting for Redis to start...
nc: service "" unknown
Waiting for Redis to start...
nc: service "" unknown
Waiting for Redis to start...
nc: service "" unknown
Waiting for Redis to start...
nc: service "" unknown
Waiting for Redis to start...

Lösung: REDIS_PORT=6379 als env-var in compose.yml bei service nextcloud-aio-nextcloud hinzufügen. siehe .env Datei oben mit Kommentar

2026-01-21 Fehler: Collabora - Mount jail self-test fails inside Docker

image.png

Lösung bisher nur ein Workaround:

https://forum.collaboraonline.com/t/mount-jail-self-test-fails-inside-docker-because-collabora-runs-the-test-as-uid-65534-without-capabilities-forces-fallback-mode/4263 

2026-02-04 Fehler fulltextsearch fehlt

Waiting for Fulltextsearch to become available...
nc: service "" unknown
Waiting for Fulltextsearch to become available...
nc: service "" unknown
Fulltextsearch did not start in time. Skipping initialization and disabling fulltextsearch apps.
No such app enabled: fulltextsearch
No such app enabled: fulltextsearch_elasticsearch
No such app enabled: files_fulltextsearch

Lösung: elasticsearch volume leeren und mit docker compose up -d --force-recreate neustarten

Danach fulltextsearch neu indizieren mit 

php occ app:enable fulltextsearch
php occ app:enable fulltextsearch_elasticsearch
php occ app:enable files_fulltextsearch
php occ fulltextsearch:test
php occ fulltextsearch:index

  

Security-Check

https://scan.nextcloud.com/ 

image.png

Docker-Compose .yamls

Nexterm (SSH, VNC, RDP über Web-UI)

docker-compose.yml

services:
  nexterm:
    image: germannewsmaker/nexterm:1.0.3-OPEN-PREVIEW
    ports:
      - "6989:6989"
    restart: unless-stopped
    volumes:
      - data:/app/data
    environment:
      ENCRYPTION_KEY: f0a8e...<openssl rand -hex 32>...c226

volumes:
  data:

Docker-Compose .yamls

pgAdmin (PostgreSQL Web-UI)

docker-compose.yml

services:
  pgadmin:
    container_name: pgadmin
    image: dpage/pgadmin4:9.9.0
    volumes:
       - pgadmin:/root/.pgadmin
       - storage:/var/lib/pgadmin/storage
    ports:
      - 5050:80
    restart: unless-stopped
    env_file:
      - stack.env

  postgres:
      container_name: postgresql
      image: postgres:13.22
      environment:
          # username postgres
          - POSTGRES_PASSWORD=nC8.........8m5t
      ports:
          - '5432:5432'


volumes:
  pgadmin:
  storage:

.env

PGADMIN_DEFAULT_EMAIL=MAIL@MEINEDOMAIN.de
PGADMIN_DEFAULT_PASSWORD=MEIN-PGADMIN-WEBUI-PW (oder mit <openssl rand -hex 24> erzeugt)

 

image.png

 

image.png

Docker-Compose .yamls

Gotify

services:
  gotify:
    image: gotify/server
    ports:
      - 8066:80
    environment:
      GOTIFY_DEFAULTUSER_PASS: 'admin'
    volumes:
      - app:/app/data
    # to run gotify as a dedicated user:
    # sudo chown -R 1234:1234 ./gotify_data
    # user: "1234:1234"

volumes:
  app:

 

Docker-Compose .yamls

Gitea

Anleitung

https://docs.gitea.com/installation/install-with-docker 

https://integrations.goauthentik.io/integrations/services/gitea/ 

https://docs.gitea.com/administration/config-cheat-sheet 

Runner:

https://docs.gitea.com/usage/actions/act-runner#install-with-the-docker-image 

https://docs.gitea.com/usage/actions/act-runner#configuration 

docker-compose.yml mit Runner

update mit v1.25.4 am 03.02.2026

services:
  server:
    image: docker.gitea.com/gitea:1.25.4
    container_name: gitea
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - GITEA__database__DB_TYPE=postgres
      - GITEA__database__HOST=db:5432
      - GITEA__database__NAME=gitea
      - GITEA__database__USER=gitea
      - GITEA__database__PASSWD=<openssl rand -hex 24>
    restart: always
    networks:
      - gitea
    volumes:
      - app:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "3046:3000"
      - "222:22"
    depends_on:
      - db

  db:
    image: docker.io/library/postgres:14
    restart: unless-stopped
    environment:
      - POSTGRES_USER=gitea
      - POSTGRES_PASSWORD=<openssl rand -hex 24>
      - POSTGRES_DB=gitea
    networks:
      - gitea
    volumes:
      - db:/var/lib/postgresql/data

  runner:
    image: docker.io/gitea/act_runner:0.2.13
    environment:
      CONFIG_FILE: /config.yaml
      GITEA_INSTANCE_URL: "https://git.MEINEDOMAIN.DE"
      GITEA_RUNNER_REGISTRATION_TOKEN: "r5ES....ptLTD" # Token aus gitea web-ui Einstellungen > Actions > Runner > create Runner 
      GITEA_RUNNER_NAME: "gitea-runner-01"
      GITEA_RUNNER_LABELS: "ubuntu-22.04:host"
    volumes:
      - /mnt/fn-volume-01/docker-data/volumes/gitea_runner-cfg/config.yaml:/config.yaml
      - runner-data:/data
      - /var/run/docker.sock:/var/run/docker.sock

networks:
  gitea:
#    external: false
    
volumes:
  app:
  db:
  runner-data:
  

SSH einrichten

Im meiner docker-compose.yml oben ist port 22 im Container auf Port 222 vom Host gemappt. Daher muss noch im Container in der app.ini der SSH-Port angepasst werden:

nano /var/lib/docker/volumes/gitea_app/_data/gitea/conf
[server]
...
SSH_PORT = 222
SSH_LISTEN_PORT = 222
...

image.png

Danach den Gitea-Container neustarten und in einem Repo unter Code > SSH die Adresse checken, ob der Port übernommen wurde:

image.png

Nicht vergessen, den Port in der Firewall zu öffnen, in meinem Fall Hetzner, ansonsten auch in der ufw

image.png

Um den eigenen SSH-Key verwenden zu können, muss der Public-Key natürlich noch in Gitea eingepflegt werden unter

Profilbild > Einstellungen > SSH / GPG-Schlüssel > Schlüssel hinzufügen > .pub-Key einfügen und benennen

image.png

ssh testen mit

ssh -p 222 -i /home/BENUTZER/.ssh/id_MEINNAME_ed25519 git@git.DEINEDOMAIN.de

wenn successfully authenticated, testen mit git clone:

image.png

Falls ein anderer ssh-key angegeben werden muss, kann dies in der ~/.ssh/config eingestellt werden (Datei anlegen, falls nicht vorhanden):

~ ❯ cat /home/eike/.ssh/config

Host git.MEINEDOMAIN.de
  HostName git.MEINEDOMAIN.de
  Port 222
  User git
  IdentityFile ~/.ssh/id_MEINNAME_ed25519

OIDC-Settings in Authentik

app.ini Configuration

https://docs.gitea.com/administration/config-cheat-sheet 

nano /var/lib/docker/volumes/gitea_app/_data/gitea/conf/app.ini:

[service]
# ...
DISABLE_REGISTRATION = false
REQUIRE_SIGNIN_VIEW = true
ALLOW_ONLY_EXTERNAL_REGISTRATION = true
ENABLE_AUTO_REGISTRATION = true
# ...

[openid]
ENABLE_OPENID_SIGNIN = true
ENABLE_OPENID_SIGNUP = true

# ...

Anwendungen > Provider > gitea

image.png

image.png

Anwendungen > Anwendungen > Gitea

image.png

Customization > Eigenschaften > Scope Mapping

gitea scope für gruppen gituser, gitadmin und gitrestricted

image.png

gitea_claims = {}

if request.user.ak_groups.filter(name="gituser").exists():
    gitea_claims["gitea"]= "user"
if request.user.ak_groups.filter(name="gitadmin").exists():
    gitea_claims["gitea"]= "admin"
if request.user.ak_groups.filter(name="gitrestricted").exists():
    gitea_claims["gitea"]= "restricted"

return gitea_claims

Verzeichnis > Gruppen

image.png

OIDC-Settings in Gitea

Administration > Identität & Zugriff > Authentifizierungsquellen > Neu:

image.png

image.png

groß- und kleinbuchstaben beachten, alles so nennen wie in anleitung (auch scope, gruppen, claims, slug bei provider usw)

Docker-Compose .yamls

Rustdesk

Anleitung

https://rustdesk.com/docs/en/self-host/rustdesk-server-oss/docker/ 

https://integrations.goauthentik.io/integrations/services/rustdesk-pro/ 

Ports öffnen

Be sure to open these ports in the firewall:

If you do not need web client support, the corresponding ports 2111821119 can be disabled.

docker-compose.yml (free plan)

services:
  hbbs:
    container_name: rustdesk-hbbs
    image: rustdesk/rustdesk-server:1.1.14
#    environment:
#      - ALWAYS_USE_RELAY=Y
    command: hbbs
    volumes:
      - app:/root
    network_mode: "host"
    depends_on:
      - hbbr
    restart: unless-stopped

  hbbr:
    container_name: rustdesk-hbbr
    image: rustdesk/rustdesk-server:1.1.14
    command: hbbr
    volumes:
      - app:/root
    network_mode: "host"
    restart: unless-stopped

volumes:
  app:

docker-compose.yml (pro plan, 20€ mit OIDC und Webconsole)

einfach 

image: rustdesk/rustdesk-server:1.1.14

mit 

image: docker.io/rustdesk/rustdesk-server-pro:1.6.1

ersetzen

Rustdesk-Console unter http://<rustdesk-server-ip>:21114 erreichbar.
The default administrator username/password is admin/test1234:

OIDC (Pro-only)

https://integrations.goauthentik.io/integrations/services/rustdesk-pro/ 

Docker-Compose .yamls

DDNS-Updater

DynamicDNS DDNS Updater Strato

https://github.com/qdm12/ddns-updater

https://hub.docker.com/r/qmcgaw/ddns-updater/tags 

image.png

version: "3.7"
services:
  ddns-updater:
    image: qmcgaw/ddns-updater:v2.9
    container_name: ddns-updater
    network_mode: bridge
    ports:
      - 8003:8000/tcp
    volumes:
      - app:/updater/data
    environment:
      - CONFIG={"settings":[{"provider":"strato","domain":"subdomain.DOMAIN.de","password":"MEINPASSWORT","ip_version":"ipv4","ipv6_suffix":""}]}
      - PERIOD=5m
      - UPDATE_COOLDOWN_PERIOD=5m
      - PUBLICIP_FETCHERS=all
      - PUBLICIP_HTTP_PROVIDERS=all
      - PUBLICIPV4_HTTP_PROVIDERS=all
      - PUBLICIPV6_HTTP_PROVIDERS=all
      - PUBLICIP_DNS_PROVIDERS=all
      - PUBLICIP_DNS_TIMEOUT=3s
      - HTTP_TIMEOUT=10s

      # Web UI
      - LISTENING_ADDRESS=:8000
      - ROOT_URL=/

      # Backup
      - BACKUP_PERIOD=0 # 0 to disable
      - BACKUP_DIRECTORY=/updater/data

      # Other
      - LOG_LEVEL=info
      - LOG_CALLER=hidden
      - SHOUTRRR_ADDRESSES=
    restart: always

volumes:
  app:

Docker-Compose .yamls

Trilium Notes

# Running `docker-compose up` will create/use the "trilium-data" directory in the user home
# Run `TRILIUM_DATA_DIR=/path/of/your/choice docker-compose up` to set a different directory
# To run in the background, use `docker-compose up -d`
services:
  trilium:
    # Optionally, replace `latest` with a version tag like `v0.90.3`
    # Using `latest` may cause unintended updates to the container
    image: triliumnext/notes:v0.95.0
    # Restart the container unless it was stopped by the user
    restart: unless-stopped
    environment:
      - TRILIUM_DATA_DIR=/home/node/trilium-data
    ports:
      # By default, Trilium will be available at http://localhost:8080
      # It will also be accessible at http://<host-ip>:8080
      # You might want to limit this with something like Docker Networks, reverse proxies, or firewall rules,
      # however be aware that using UFW is known to not work with default Docker installations, see:
      # https://docs.docker.com/engine/network/packet-filtering-firewalls/#docker-and-ufw
      - '8089:8080'
    volumes:
      # Unless TRILIUM_DATA_DIR is set, the data will be stored in the "trilium-data" directory in the home directory.
      # This can also be changed with by replacing the line below with `- /path/of/your/choice:/home/node/trilium-data
      #- ${TRILIUM_DATA_DIR:-~/trilium-data}:/home/node/trilium-data
      - data:/home/node/trilium-data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro

volumes:
  data:

Docker-Compose .yamls

CUPS Druckerserver

https://hub.docker.com/r/anujdatar/cups 

https://github.com/anujdatar/cups-docker 

services:
    cups:
        image: anujdatar/cups:25.10.01
        container_name: cups
        network_mode: host #optional, falls etwas mit discovern der Geraete nicht klappt
        restart: unless-stopped
        ports:
            - "631:631"
        devices:
            - /dev/bus/usb:/dev/bus/usb
        environment:
            - CUPSADMIN=admin
            - CUPSPASSWORD=MEINPW
            - TZ=Europe/Berlin
        volumes:
            - app:/etc/cups
            #- eigene-treiber:/opt/eigene-treiber
            #- spool:/var/spool/cups
        # Folgendes CMD nur wenn man eigene Treiber als .deb im Volume eingepflegt hat:
        #command: sh -c "dpkg -i /opt/eigene-treiber/cnijfilter2_6.00-1_amd64.deb && apt-get install -f -y && /entrypoint.sh"
        
volumes:
  app:
  #eigene-treiber:
  #spool:

Falls Port 631 vom System already in use

sudo systemctl stop cups
sudo systemctl disable cups

sudo apt remove cups

#optional
sudo systemctl stop cups-browsed
sudo systemctl disable cups-browsed

Mit CUPS verbundenen Drucker in Windows einfügen

https://techblog.paalijarvi.fi/2020/05/25/making-windows-10-to-print-to-a-cups-printer-over-the-network/ 

image.png

pnputil verwenden um Treiber zu installieren:

https://www.reddit.com/r/PowerShell/comments/14k0wkb/addprinter_ugh/ 

für ansible aus https://stackoverflow.com/questions/69673238/adding-a-printer-with-powershell:

- name: install Sharp MX3070N driver
  win_shell: pnputil /add-driver "C:\sharp-mx3070n\su0emenu.inf" /install

- name: add printer port
  win_shell: Add-PrinterPort -Name "printer3" -PrinterHostAddress "yourprintersIP"

- name: add printer driver
  win_shell: Add-PrinterDriver -Name "SHARP MX-3070N PCL6" # if this string is not known, grab from .inf file

- name: add printer
  win_shell: Add-Printer -Name "sharpmx3070n" -DriverName "SHARP MX-3070N PCL6" -PortName "printer3"

- name: black and white printing
  win_shell: Set-PrintConfiguration -PrinterName "sharpmx3070n" -Color 0 # B&W

- name: single-sided printing
  win_shell: Set-PrintConfiguration -PrinterName "sharpmx3070n" -DuplexingMode 'OneSided'

Ansible-Playbook PL

printer_install.yml

---
- name: Add CUPS printer for current site only
  hosts: windows_targets
  
  tasks:
    - name: Detect site by local IPv4
      ansible.windows.win_powershell:
        script: |
          $ips = Get-NetIPAddress -AddressFamily IPv4 | Where-Object {
            $_.IPAddress -notmatch '^169\.254\.' -and $_.IPAddress -notmatch '^127\.'
          } | Select-Object -ExpandProperty IPAddress

          $site = 'NONE'
          foreach ($ip in $ips) {
            if ($ip -match '^192\.168\.200\.\d+$') { $site = 'HWS'; break }
            if ($ip -match '^10\.53\.1\.\d+$')     { $site = 'FBS'; break }
            if ($ip -match '^10\.26\.1\.\d+$')     { $site = 'SPS'; break }
          }

          @{site=$site; ips=$ips} | ConvertTo-Json -Compress
      register: netinfo
      changed_when: false

    - name: Save site_info (parsed)
      ansible.builtin.set_fact:
        site_info: "{{ (netinfo.output | default([])) | join('') | from_json }}"

    - name: Save site_id
      ansible.builtin.set_fact:
        site_id: "{{ site_info.site | default('NONE') }}"


    - name: Install printers for Standort HWS
      ansible.builtin.include_tasks: printer_HWS.yml
      when: site_id == 'HWS'

    - name: Install printers for Standort FBS
      ansible.builtin.include_tasks: printer_FBS.yml
      when: site_id == 'FBS'

    - name: Install printers for Standort SPS
      ansible.builtin.include_tasks: printer_SPS.yml
      when: site_id == 'SPS'

löst zb printer_HWS.yml aus:

printer_HWS.yml

- name: Install printers for HWS
  block:
    - name: Ensure Internet Printing Client is enabled (best-effort)
      ansible.windows.win_powershell:
        script: |
          $candidates = @('Printing-InternetPrinting-Client','Printing-Internet Printing-Client')
          foreach ($c in $candidates) {
            $f = Get-WindowsOptionalFeature -Online -FeatureName $c -ErrorAction SilentlyContinue
            if ($f) {
              if ($f.State -ne 'Enabled') { Enable-WindowsOptionalFeature -Online -FeatureName $c -All -NoRestart | Out-Null }
              break
            }
          }
          '{"ok":true}'
      changed_when: false
      failed_when: false

    - name: Add all printers (force) with Microsoft IPP Class Driver
      ansible.windows.win_powershell:
        script: |
          $PrinterName = "{{ item.printer_name }}"
          $PrinterURL  = "{{ item.printer_url }}"
          $DriverName  = "{{ item.ipp_driver }}"
          try {
            Add-Printer -Name $PrinterName -PortName $PrinterURL -DriverName $DriverName -ErrorAction Stop
            '{"changed":true,"action":"Add-Printer","name":' + (ConvertTo-Json $PrinterName -Compress) + '}'
          } catch {
            '{"changed":false,"action":"ignored","name":' + (ConvertTo-Json $PrinterName -Compress) +
            ',"error":' + (ConvertTo-Json $_.Exception.Message -Compress) + '}'
          }
      loop: "{{ printers }}"
      register: add_results
      changed_when: (item.output[0] | default('{}') | from_json).changed | default(false)
      failed_when: false

    - name: Summary (per printer)
      ansible.builtin.debug:
        msg: "{{ ((item.output | first) | default('{}')) | from_json }}"
      loop: "{{ add_results.results | default([]) }}"

  vars:
    printers:
      - { printer_name: "HWS PM1 ✳️|⚫", printer_url: "http://192.168.200.11:631/printers/Brother-MFC-L2750DW_PM1_SW", ipp_driver: "Microsoft IPP Class Driver" }
      - { printer_name: "HWS PM1 🦑|⚫", printer_url: "http://192.168.200.11:631/printers/Canon-GM4050_PM1_SW", ipp_driver: "Microsoft IPP Class Driver" }
      - { printer_name: "HWS PM2 ✳️|⚫", printer_url: "http://192.168.200.11:631/printers/Brother-MFC-L2710DW_PM2_SW", ipp_driver: "Microsoft IPP Class Driver" }
      - { printer_name: "HWS PM3 🦑|⚫", printer_url: "http://192.168.200.11:631/printers/Canon-GM4050_PM3_SW", ipp_driver: "Microsoft IPP Class Driver" }
      - { printer_name: "HWS 1.OG ✳️|🎨", printer_url: "http://192.168.200.11:631/printers/Brother-DCP-L3560CDW_1.OG_Farbe", ipp_driver: "Microsoft IPP Class Driver" }

Name und Benutzer anzeigen

Wenn man Name des Auftrags und Benutzer in der CUPS Web-UI sehen möchte und anstatt dem hier...

image.png

... lieber das hier sehen möchte ....

image.png

... muss die Konfigurationsdatei cupsd.conf wie folgt ändern:

Entweder direkt im Dockervolume oder CUPS Web-UI > Konfigurationsdatei bearbeiten:

image.png

folgende Stelle finden:

...
<Policy default>
  JobPrivateAccess default
  JobPrivateValues default
...

und in 

...
<Policy default>
  JobPrivateAccess all
  JobPrivateValues none
...

ändern:

image.png

Änderungen speichern klicken und neustart abwarten. Dann unter Drucker > Auftäge nachsehen ob es geklappt hat.

image.png

Canon Pixma GM4050: Keine Netzwerkfreigabe

Problem: Ein Canon Drucker meldet in CUPS, dass keine Netzwerkfreigabe möglich ist, obwohl das Häkchen gesetzt wurde:

image.png

Lösung: in docker-compose den network_mode: host einstellen und den Drucker ueber CUPS > Verwaltung > Neuen Drucker suchen. Am besten vorher die Treiber installieren (siehe unten) damit entweder ueber socket:// oder cnijbe2:// (Canontreiber) verbunden werden kann:

image.png

Canon PIXMA GM4050: Druckertreiber nachinstallieren

  1. Druckertreiber von offizieller Seite herunterladen, fuer Linux x64 (.deb oder .rpm)
  2. eigenes Volume dafuer einbinden (siehe oben Docker Compose)
  3. .deb in volume kopieren
  4. command hinzufuegen um nach jeden Container-Start die Treiber zu installieren

image.png

docker-compose.yaml

[...]
            - TZ=Europe/Berlin
        volumes:
            - app:/etc/cups
            - eigene-treiber:/opt/eigene-treiber
        command: sh -c "dpkg -i /opt/eigene-treiber/cnijfilter2_6.00-1_amd64.deb && apt-get install -f -y && /entrypoint.sh"

volumes:
  app:
  eigene-treiber:

Drucker mit Socket hinzufuegen:

socket://<DRUCKER-IP>

Treiber auswaehlen:

image.png

 

Druckertreiber installieren

## Powershell-CMDs
### Treibername aus .inf Dateien herausfinden, nach Pattern filtern (GM4000 in diesem Fall für den GM4050 Drucker)
Get-Content "D:\Entwicklung\Praxis Maxis\Druckertreiber\Canon_GM4050\*.inf" | Select-String -Pattern "GM4000" -Context 1,2

### Druckertreiber installieren mit pnputil
pnputil /add-driver "D:\Entwicklung\Praxis Maxis\Druckertreiber\Canon_GM4050\*.inf" /subdirs /install

### Druckertreiber hinzufügen
Add-PrinterDriver -Name "Canon GM4000 series"

### Drucker hinzufügen mit CUPS-Server in Docker und richtigem Treibername
Add-Printer -Name 'HWS PM3 🦑|⚫' -PortName 'http://192.168.200.11:631/printers/Canon-GM4050_PM3_SW' -DriverName  'Canon GM4000 series'

### Falls ein Fehler auftaucht, probieren den Spooler-Service bei jedem Windows-Neustart neuzustarten mit
Restart-Service -Name Spooler -Force

Docker-Compose .yamls

scanservjs (Scanner Web-UI)

version: "3.8"

services:
  scanservjs:
    image: sbs20/scanservjs:v3.0.4
    container_name: scanservjs
    privileged: true
    #network_mode: host #BENOETIGT BEIM SUCHEN MIT 'airscan-discover fuer eSCL und WSD Scanner'
    ports:
      - "8098:8080"
    environment:
      #DELIMITER: '|'
      #SANED_NET_HOSTS: "10.0.100.30;10.0.100.31"
      #AIRSCAN_DEVICES: '"HWS PM1 ✳️⚫" = "http://192.168.200.18/eSCL"|"HWS PM2 ✳️⚫" = "http://192.168.200.15/eSCL"|"HWS 1.OG ✳️🎨" = "http://192.168.200.119/eSCL"'
      #AIRSCAN_DEVICES='"Canon MFD" = "http://192.168.0.10/eSCL";"EPSON MFD" = "http://192.168.0.11/eSCL"'
      #AIRSCAN_DEVICES: |
      #  "Canon MFD" = "http://192.168.0.10/eSCL";
      #  "EPSON MFD" = "http://192.168.0.11/eSCL"
      #PIXMA_HOSTS: "192.168.200.33"
      #SCANIMAGE_LIST_IGNORE: "true"
      #DEVICES: "net:10.0.100.30:plustek:libusb:001:003;net:10.0.100.31:plustek:libusb:001:003;airscan:e0:Canon TR8500 series;airscan:e1:EPSON Cool Series"
      OCR_LANG: "deu+eng"
    volumes:
      - /var/run/dbus:/var/run/dbus
      - scans:/var/lib/scanservjs/output
      - scans-backup:/var/lib/scanservjs/output-backup
      - cfg:/etc/scanservjs
      - saned:/etc/sane.d
    restart: unless-stopped

volumes:
  scans:
  scans-backup:
  cfg:
  saned:

Problem: Es werden immer nur die ersten Trennzeichen erkannt, das heißt Scanner PM2 und  1.OG fallen unter ein Device, siehe Log:

# Insert airscan devices
if [ ! -z "$AIRSCAN_DEVICES" ]; then
  devices=$(echo $AIRSCAN_DEVICES | sed "s/$DELIMITER/\n/")
  for device in $devices; do
    sed -i "/^\[devices\]/a $device" /etc/sane.d/airscan.conf
  done
fi
+ [ ! -z "HWS PM1 ✳️⚫" = "http://192.168.200.18/eSCL"|"HWS PM2 ✳️⚫" = "http://192.168.200.15/eSCL"|"HWS 1.OG ✳️🎨" = "http://192.168.200.119/eSCL" ]
+ echo "HWS PM1 ✳️⚫" = "http://192.168.200.18/eSCL"|"HWS PM2 ✳️⚫" = "http://192.168.200.15/eSCL"|"HWS 1.OG ✳️🎨" = "http://192.168.200.119/eSCL"
+ sed s/|/\n/
+ devices="HWS PM1 ✳️⚫" = "http://192.168.200.18/eSCL"
"HWS PM2 ✳️⚫" = "http://192.168.200.15/eSCL"|"HWS 1.OG ✳️🎨" = "http://192.168.200.119/eSCL"
+ sed -i /^\[devices\]/a "HWS PM1 ✳️⚫" = "http://192.168.200.18/eSCL" /etc/sane.d/airscan.conf
+ sed -i /^\[devices\]/a "HWS PM2 ✳️⚫" = "http://192.168.200.15/eSCL"|"HWS 1.OG ✳️🎨" = "http://192.168.200.119/eSCL" /etc/sane.d/airscan.conf
# Insert pixma hosts
if [ ! -z "$PIXMA_HOSTS" ]; then
  hosts=$(echo $PIXMA_HOSTS | sed "s/$DELIMITER/\n/")
  for host in $hosts; do
    echo "bjnp://$host" >> /etc/sane.d/pixma.conf

Lösung: wie oben im docker-compose, den Ordner /etc/sane.d als Volume mounten und die airscan.conf darin per Hand bearbeiten:

[devices]
"HWS PM1 ✳️⚫" = "http://192.168.200.30/eSCL"
"HWS PM1 🦑⚫" = "http://192.168.200.31:80/wsd/scanservice.cgi", wsd
"HWS PM2 ✳️⚫" = "http://192.168.200.32/eSCL"
"HWS PM3 🦑⚫" = "http://192.168.200.33:80/wsd/scanservice.cgi", wsd
"HWS 1.OG ✳️⚫🔵🔴🟡" = "http://192.168.200.34/eSCL"

image.png

image.png

Testen, ob eSCL funktioniert

URL des Druckers mit Anhang aufrufen:

http://<DRUCKER-IP>/eSCL/ScannerCapabilities

http://<DRUCKER-IP>/eSCL/ScannerStatus

zb http://10.53.1.20/eSCL/ScannerCapabilities  

WSD-Scanner suchen

In Docker Container execen und 

airscan-discover

ausfuehren.

ACHTUNG! Zum discovern eines zb Canon Pixma GM4050 bzw GM4000 series muss network_mode: host im docker-compose.yml aktiviert sein. Danach funktioniert das scannen aber auch ohne network_mode: host

[...]
    container_name: scanservjs
    privileged: true
    network_mode: host
    ports:
[...]
> airscan-discover
[devices]
  Brother DCP-L3560CDW series [b42200db2103] = http://192.168.200.34:80/eSCL/, eSCL
  Brother DCP-L3560CDW series [b42200db2103] = https://192.168.200.34:443/eSCL/, eSCL
  Brother DCP-L3560CDW series [b42200db2103] = http://192.168.200.34:80/WebServices/ScannerService, WSD
  Brother DCP-L3560CDW series [b42200db2103] = http://[FE80::7697:79FF:FEEA:4635%252]:80/WebServices/ScannerService, WSD
  Brother MFC-L2710DW series = http://192.168.200.32:80/eSCL/, eSCL
  Brother MFC-L2710DW series = http://192.168.200.32:80/WebServices/ScannerService, WSD
  Brother MFC-L2710DW series = http://[FE80::4ED5:77FF:FE6D:67EF%252]:80/WebServices/ScannerService, WSD
  Brother MFC-L2750DW series = http://192.168.200.30:80/eSCL/, eSCL
  Brother MFC-L2750DW series = http://192.168.200.30:80/WebServices/ScannerService, WSD
  Brother MFC-L2750DW series = http://[FE80::BEF4:D4FF:FE44:75D%252]:80/WebServices/ScannerService, WSD
  CANON INC. GM4000 series = http://192.168.200.33:80/wsd/scanservice.cgi, WSD
  CANON INC. GM4000 series = http://[fe80::6e3c:7cff:fe0e:c3da%252]:80/wsd/scanservice.cgi, WSD
  CANON INC. GM4000 series = http://192.168.200.31:80/wsd/scanservice.cgi, WSD
  CANON INC. GM4000 series = http://[fe80::6e3c:7cff:fe0f:db71%252]:80/wsd/scanservice.cgi, WSD

image.png

danach die sane.d/airscan.conf anpassen: 

image.png

und

scanimage -L

 eingeben:

image.png

config anpassen und in config.local.js umbenennen:

Eikes Beispiel:

[...]
  afterDevices(devices) {
    const deviceNames = {
      'airscan:e0:FBS Friedrich UG ✳️|⚫🔵🔴🟡': 'FBS Friedrich UG ✳️|⚫🔵🔴🟡',
      'airscan:e1:FBS EG-PM ✳️|⚫': 'FBS EG-PM ✳️|⚫',
      'airscan:e2:FBS Friedrich I. ✳️|⚫🔵🔴🟡': 'FBS Friedrich I. ✳️|⚫🔵🔴🟡',
      'airscan:e3:FBS Friedrich II. ✳️|⚫🔵🔴🟡': 'FBS Friedrich II. ✳️|⚫🔵🔴🟡',
      'airscan:e4:FBS Friedrich III. ✳️|⚫🔵🔴🟡': 'FBS Friedrich III. ✳️|⚫🔵🔴🟡'
    };

    devices
      .filter(d => d.id in deviceNames)
      .forEach(d => d.name = deviceNames[d.id]);

    devices
      .filter(d => d.id.includes('FBS'))
      .forEach(device => {
        device.features['--mode'].default = 'Color';
        device.features['--resolution'].default = 150;
        device.features['--resolution'].options = [75, 150, 300, 600];
        device.features['--brightness'].default = 0;
        device.features['--contrast'].default = 5;
        device.features['-x'].default = 192;
        device.features['-y'].default = 272;
        /* device.settings.pipeline.default = 'PNG'; */
        device.settings.pipeline.default = '@:pipeline.ocr | PDF (JPG | @:pipeline.high-quality)';
        /* Disable batch modes if they are not available on your printer: */
        /*device.settings.batchMode.options = ['none', 'manual']; */
    });
  },

  async afterScan(fileInfo) {
[...]

https://medium.com/@davidclaeys/scanservjs-make-your-own-scan-server-21539f64265c 

module.exports = {
  afterDevices(devices) {
 const deviceNames = {
      /*
        'device id':'device name'
      */
      'airscan:e0:Hp Envy Pro 6442': 'Hp Envy Pro 6442'
    };
 
/*
  replace the id in the filter
*/
 devices
      .filter(d => d.id == 'airscan:e0:Hp Envy Pro 6442')
      .forEach(device => {
        device.features['--resolution'].default = 400;
        device.features['--resolution'].options = [100, 150, 200, 300, 400, 600];
        /*
          Disable batch modes if they are not available on your printer
        */
         device.settings.batchMode.options = ['none', 'manual'];
         /*
         Specify the default pipeline
         */
         device.settings.pipeline.default = ['PNG'];
      });
 
  devices
      .filter(d => d.id in deviceNames)
      .forEach(d => d.name = deviceNames[d.id]);
  }
};

escl:// automatische Suche deaktivieren

escl.conf umbenennen:

image.png

Scans autom. backupen und löschen

Weil die Scans von jedem zugänglich sind, der die Seite aufrufen kann, sollten die Scans gelöscht und zusätzlich gebackupt werden.
Wie oben in der docker-compose.yml beschrieben, ein Volume scans-backup anlegen. Dazu dieses Skript zb unter 

/home/user/scanservjs-backup.sh

anlegen und mit chmod +x /home/user/scanservjs-backup.sh ausführbar machen:

#!/bin/bash

# Scanservjs Backup und Bereinigungsskript
# Erstellt Backups und löscht alte Dateien (älter als 60 Minuten)

# Verzeichnisse definieren
SOURCE_DIR="/var/lib/docker/volumes/scanservjs_scans"
BACKUP_DIR="/var/lib/docker/volumes/scanservjs_scans-backup"
LOG_FILE="/var/log/scanservjs-backup.log"

# Zeitstempel für Logging
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')

# Logging-Funktion
log_message() {
    echo "[$TIMESTAMP] $1" | tee -a "$LOG_FILE"
}

# Skript starten
log_message "=== Backup-Skript gestartet ==="

# Prüfen ob Quellverzeichnis existiert
if [ ! -d "$SOURCE_DIR" ]; then
    log_message "FEHLER: Quellverzeichnis $SOURCE_DIR existiert nicht!"
    exit 1
fi

# Backup-Verzeichnis erstellen falls nicht vorhanden
if [ ! -d "$BACKUP_DIR" ]; then
    mkdir -p "$BACKUP_DIR"
    log_message "Backup-Verzeichnis $BACKUP_DIR wurde erstellt"
fi

# Dateien kopieren (mit rsync für effizientes Backup)
log_message "Starte Backup von $SOURCE_DIR nach $BACKUP_DIR"
rsync -av --progress "$SOURCE_DIR/" "$BACKUP_DIR/" >> "$LOG_FILE" 2>&1

if [ $? -eq 0 ]; then
    log_message "Backup erfolgreich abgeschlossen"
else
    log_message "FEHLER beim Backup!"
    exit 1
fi

# Alte Dateien löschen (älter als 60 Minuten)
log_message "Suche nach Dateien älter als 60 Minuten in $SOURCE_DIR"

# Zähler für gelöschte Dateien
DELETED_COUNT=0

# Dateien finden und löschen
while IFS= read -r -d '' file; do
    log_message "Lösche: $file"
    rm -f "$file"
    if [ $? -eq 0 ]; then
        ((DELETED_COUNT++))
    else
        log_message "FEHLER beim Löschen von: $file"
    fi
done < <(find "$SOURCE_DIR" -type f -mmin +60 -print0)

log_message "Anzahl gelöschter Dateien: $DELETED_COUNT"
log_message "=== Backup-Skript beendet ==="
echo "" >> "$LOG_FILE"

exit 0

dann mit crontab -e minütlich laufen lassen:

* * * * * /home/user/scanservjs-backup.sh

Docker-Compose .yamls

Semaphore UI (Ansible)

Achtung, Runners nur in PRO Version (50€ mtl.)

https://semaphoreui.com/install/docker/2_16/

community-edition

services:
    semaphore:
        restart: always
        ports:
            - 3000:3000
        image: semaphoreui/semaphore:v2.16.45
        environment:
            SEMAPHORE_DB_DIALECT: bolt
            SEMAPHORE_ADMIN: admin
            SEMAPHORE_ADMIN_PASSWORD: MEINPW123
            SEMAPHORE_ADMIN_NAME: admin
            SEMAPHORE_ADMIN_EMAIL: it@MEINEDOMAIN.de
            SEMAPHORE_SCHEDULE_TIMEZONE: Europe/Berlin
            SEMAPHORE_USE_REMOTE_RUNNER: "True"
            SEMAPHORE_RUNNER_REGISTRATION_TOKEN: "D3o/h.........QAdqU="

        volumes:
            - cfg:/etc/semaphore
            - tmp:/tmp/semaphore
            - data:/var/lib/semaphore
            
    runner01:
        image: semaphoreui/runner:v2.16.45
        environment:
            - SEMAPHORE_RUNNER_PRIVATE_KEY_FILE=/var/lib/semaphore/config.runner.key
            - SEMAPHORE_RUNNER_REGISTRATION_TOKEN=D3o/hQ..........VdqU=
            - SEMAPHORE_RUNNER_TOKEN=12rew..........bqj8=
            - SEMAPHORE_WEB_ROOT=http://192.168.200.11:3000
        volumes:
            - cfg-run01:/var/lib/semaphore

volumes:
    cfg:
    tmp:
    data:
    cfg-run01:

pro-edition (15€/monat)

Pro = mehrere Runner + Terraform + opentofu

Obacht: Beim umstellen von Free auf Pro muss der Browsercache gelöscht werden, damit unten links der Button 'activate Subscription' erscheint 

services:
    semaphore:
        restart: always
        ports:
            - 3000:3000
        #image: semaphoreui/semaphore:v2.16.45
        image: public.ecr.aws/semaphore/pro/server:v2.16.43
        environment:
            SEMAPHORE_DB_DIALECT: bolt
            SEMAPHORE_ADMIN: admin
            SEMAPHORE_ADMIN_PASSWORD: MEINPW123
            SEMAPHORE_ADMIN_NAME: admin
            SEMAPHORE_ADMIN_EMAIL: it@MEINEDOMAIN.de
            SEMAPHORE_SCHEDULE_TIMEZONE: Europe/Berlin
            SEMAPHORE_USE_REMOTE_RUNNER: "True"
            SEMAPHORE_RUNNER_REGISTRATION_TOKEN: "D3o/h.........QAdqU="

        volumes:
            - cfg:/etc/semaphore
            - tmp:/tmp/semaphore
            - data:/var/lib/semaphore
            
    runner01:
        #image: semaphoreui/runner:v2.16.45
        image: public.ecr.aws/semaphore/pro/runner:v2.16.43
        environment:
            - SEMAPHORE_RUNNER_PRIVATE_KEY_FILE=/var/lib/semaphore/config.runner.key
            - SEMAPHORE_RUNNER_REGISTRATION_TOKEN=D3o/hQ..........VdqU=
            - SEMAPHORE_RUNNER_TOKEN=12rew..........bqj8=
            - SEMAPHORE_WEB_ROOT=http://192.168.200.11:3000
        volumes:
            - cfg-run01:/var/lib/semaphore
    runner02:
        image: semaphoreui/runner:v2.16.7
        environment:
            - SEMAPHORE_RUNNER_PRIVATE_KEY_FILE=/var/lib/semaphore/config.runner.key
            - SEMAPHORE_RUNNER_REGISTRATION_TOKEN=D3o/hQ..........dqU=
            - SEMAPHORE_RUNNER_TOKEN=wM7...........6S8=
            - SEMAPHORE_WEB_ROOT=http://192.168.200.11:3000
        volumes:
            - cfg-run02:/var/lib/semaphore
volumes:
    cfg:
    tmp:
    data:
    cfg-run01:
    cfg-run02:

Runner mit config.runner.key erzeugen

Beim Erzeugen eines neuen Runners über die Semaphore-UI muss der private-key in der Datei
/var/lib/semaphore/config.runner.key
gespeichert werden (kann mit root also vom Docker Host passieren)

image.png

image.png

image.png

wie oben in der docker-compose.yml der Pro-Variante angegeben muss der Pfad der Datei als env-var mitgegeben werden:

[...]
    runner02:
        image: semaphoreui/runner:v2.16.7
        environment:
            - SEMAPHORE_RUNNER_PRIVATE_KEY_FILE=/var/lib/semaphore/config.runner.key
[...]            

Docker-Compose .yamls

paperless-ai

https://github.com/clusterzx/paperless-ai

services:
  paperless-ai:
    image: clusterzx/paperless-ai:3.0.9
    container_name: paperless-ai
    network_mode: bridge
    restart: unless-stopped
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges=true
    environment:
      - PUID=1000
      - PGID=1000
      - PAPERLESS_AI_PORT=${PAPERLESS_AI_PORT:-3000}
      - RAG_SERVICE_URL=http://localhost:8000
      - RAG_SERVICE_ENABLED=true
    ports:
      - "3057:${PAPERLESS_AI_PORT:-3000}"
    volumes:
      - data:/app/data

volumes:
  data:
  

.env

PAPERLESS_AI_PORT=3057

Paperless-ai + netbird

um mit paperless-ai auf eine lokale KI (zB ein GMKTec Evo-X2) zugreifen zu können, nutze ich Netbird-VPN:

services:
  paperless-ai:
    image: clusterzx/paperless-ai:3.0.9
    container_name: paperless-ai
    #network_mode: host
    depends_on:
      - netbird
    restart: unless-stopped
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges=true
    environment:
      - PUID=1000
      - PGID=1000
      - PAPERLESS_AI_PORT=${PAPERLESS_AI_PORT:-3000}
      - RAG_SERVICE_URL=http://localhost:8000
      - RAG_SERVICE_ENABLED=true
      - PAPERLESS_URL=https://paperless-ai.MEINEDOMAIN.de
    ports:
      - "3057:${PAPERLESS_AI_PORT:-3000}"
    volumes:
      - data:/app/data

  netbird:
    image: netbirdio/netbird:latest
    container_name: paperless-ai-netbird
    hostname: fn-paperless-ai
    privileged: true
    #network_mode: host
    cap_add:
      - NET_ADMIN
      - SYS_ADMIN
      - SYS_RESOURCE
    volumes:
      #- /var/run/netbird:/var/run/netbird
      - nb-var-lib:/var/lib/netbird
      - nb-cfg:/etc/netbird
    environment:
      - NB_SETUP_KEY=FFE.........AA49
    restart: unless-stopped

volumes:
  data:
  nb-cfg:
  nb-var-lib:

.env

PAPERLESS_AI_PORT=3057

paperless-ai Konfiguration mit Ollama:

Provider: Ollama
Ollama API: http://evo-x2.netbird.cloud:11434
Ollama Model: gemma3:27b

image.png

Docker-Compose .yamls

immich

https://docs.immich.app/install/docker-compose/ 

docker-compose.yml

# https://docs.immich.app/install/docker-compose/

#
# WARNING: To install Immich, follow our guide: https://docs.immich.app/install/docker-compose
#
# Make sure to use the docker-compose.yml of the current release:
#
# https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
#
# The compose file on main may not be compatible with the latest release.

name: immich

services:
  immich-server:
    container_name: immich_server
    image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
    # extends:
    #   file: hwaccel.transcoding.yml
    #   service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
    volumes:
      - upload:/data
      - /etc/localtime:/etc/localtime:ro
    env_file:
      - stack.env
    ports:
      - '2283:2283'
    depends_on:
      - redis
      - database
    restart: unless-stopped
    healthcheck:
      disable: false

  immich-machine-learning:
    container_name: immich_machine_learning
    # For hardware acceleration, add one of -[armnn, cuda, rocm, openvino, rknn] to the image tag.
    # Example tag: ${IMMICH_VERSION:-release}-cuda
    image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
    # extends: # uncomment this section for hardware acceleration - see https://docs.immich.app/features/ml-hardware-acceleration
    #   file: hwaccel.ml.yml
    #   service: cpu # set to one of [armnn, cuda, rocm, openvino, openvino-wsl, rknn] for accelerated inference - use the `-wsl` version for WSL2 where applicable
    volumes:
      - model-cache:/cache
    env_file:
      - stack.env
    restart: unless-stopped
    healthcheck:
      disable: false

  redis:
    container_name: immich_redis
    image: docker.io/valkey/valkey:8@sha256:81db6d39e1bba3b3ff32bd3a1b19a6d69690f94a3954ec131277b9a26b95b3aa
    healthcheck:
      test: redis-cli ping || exit 1
    restart: unless-stopped

  database:
    container_name: immich_postgres
    image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:bcf63357191b76a916ae5eb93464d65c07511da41e3bf7a8416db519b40b1c23
    env_file:
      - stack.env
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_USER: ${DB_USERNAME}
      POSTGRES_DB: ${DB_DATABASE_NAME}
      POSTGRES_INITDB_ARGS: '--data-checksums'
      # Uncomment the DB_STORAGE_TYPE: 'HDD' var if your database isn't stored on SSDs
      # DB_STORAGE_TYPE: 'HDD'
    volumes:
      - db:/var/lib/postgresql/data
    shm_size: 128mb
    restart: unless-stopped

volumes:
  model-cache:
  db:
  upload:

.env

IMMICH_VERSION=v2
DB_PASSWORD=postgres
DB_USERNAME=postgres
DB_DATABASE_NAME=immich

 

 

Upload mit immich-go

https://github.com/simulot/immich-go 

https://github.com/simulot/immich-go/blob/main/docs/best-practices.md 

 

lokale Ordner Upload

.\immich-go upload from-folder --server=https://immich.MEINEDOMAIN.de --api-key=gHwcJ.......jE --concurrent-tasks=4 --client-timeout=60m --pause-immich-jobs=false --on-errors=continue --manage-burst=Stack --manage-raw-jpeg=StackCoverJPG --manage-heic-jpeg=StackCoverJPG --exclude-extensions="*.psd" --session-tag --folder-as-album=FOLDER --folder-as-tags --tag="Import 2026-Jan" 'D:\Bilder\anderes\Ford Fiesta\'

Google Photos Upload

zuerst Takeout herunterladen: https://takeout.google.com/ 

dann

.\immich-go upload from-google-photos --server=https://immich.MEINEDOMAIN.de --api-key=gHwc........jE --concurrent-tasks=4 --client-timeout=60m --pause-immich-jobs=true --on-errors=continue --manage-burst=Stack --manage-raw-jpeg=StackCoverJPG --manage-heic-jpeg=StackCoverJPG --session-tag E:\googlephotosbackup\takeout-20251202T213051Z-3-*.zip

 

Docker-Compose .yamls

Windows in Docker

services:
  windows:
    image: dockurr/windows
    container_name: windows
    environment:
      VERSION: "11"
    devices:
      - /dev/kvm
      - /dev/net/tun
    cap_add:
      - NET_ADMIN
    ports:
      - 8006:8006
      - 3389:3389/tcp
      - 3389:3389/udp
    volumes:
      - storage:/storage
    restart: unless-stopped
    stop_grace_period: 2m

volumes:
  storage:

Docker-Compose .yamls

Zerobyte (Restic Backup UI)

image.png

https://github.com/nicotsx/zerobyte 

services:
  zerobyte:
    image: ghcr.io/nicotsx/zerobyte:v0.22
    container_name: zerobyte
    restart: unless-stopped

# ---------------------------------
# OBACHT wenn man Volumes (zu speichernde Daten) oder Repositories (Ziele der Backups) mit SMB oder SFTP mounten möchte, 
# dann folgende Optionen:
    cap_add:
      - SYS_ADMIN
      - SYS_PTRACE
    security_opt:
      - seccomp:unconfined
      - apparmor:unconfined
# falls SFTP oder SMB nicht notwendig, dann nur:
    cap_add:
      - SYS_ADMIN
# ---------------------------------

    ports:
      - "4096:4096"
    devices:
      - /dev/fuse:/dev/fuse
    environment:
      - TZ=Europe/Berlin # Set your timezone here
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - app:/var/lib/zerobyte
      - /var/lib/docker:/backup-var-lib
      - /root:/backup-root-home
      - /mnt/fn-volume-01:/backup-fn-volume-01
      - /mnt/fn-volume-02:/backup-fn-volume-02

volumes:
  app:

Hetzner S3 Object Storage als Repo

Backend: S3

Endpoint:

fsn1.your-objectstorage.com

Bucket: <Bucket-Bezeichnung-aus-Hetzner>/<gewuenschter-Unterordner> zb BUCKETNAME/Server-01

Acces-Key & Secret-Key aus Hetzner entnehmen

image.png

! /var/lib/docker/overlay2 aufräumen

mit

sudo apt install ncdu

ncdu /

herausfinden, ob /var/lib/docker/overlay bzw. overlay2 für den vollen Speicher verantwortlich ist.
Mehr Info:
https://wiki.folkerts.it/books/code-snippets-notes/page/speicherplatz-in-treesize-form-mit-ncdu

https://stackoverflow.com/questions/46672001/is-it-safe-to-clean-docker-overlay2

https://forums.docker.com/t/some-way-to-clean-up-identify-contents-of-var-lib-docker-overlay/30604

docker system prune -a -f

oder 

docker builder prune

 

! Log-Datei Limit

Um eine aufblähende Dockerinstanz zu verhindern, kann ein Limitieren der Container-Logdatei helfen.

docker-compose.yml

logging:
      options:
        max-size: "100m"

zb in homer:

version: "2"
services:
  homer:
    image: b4bz/homer
    logging:
      options:
        max-size: "100m"
    container_name: homer
    volumes:
      - /var/lib/docker/volumes/homer/:/www/assets
    ports:
      - 8090:8080
    #environment:
    #  - UID=1000
    #  - GID=1000
    restart: unless-stopped

ODER

Direkt im Docker Daemon einstellen, damit es für alle Container automatisch gilt:

https://stackoverflow.com/a/42510314

https://stackoverflow.com/a/75928154

sudo nano /etc/docker/daemon.json (Datei anlegen, wenn nonexistent)

{
  "log-driver": "local",
  "log-opts": {"max-size": "50m", "max-file": "3"}
}

oder 

{
  "log-opts": {
    "max-size": "10m",
    "max-file": "5"
  }
}

 

weitere Quelle

https://forums.docker.com/t/some-way-to-clean-up-identify-contents-of-var-lib-docker-overlay/30604/52

 

! NAS-Server als Volume einbinden

version: "2"
services:
  ipparks:
    image: 192.168.178.224:5000/ipparks:latest
    container_name: ipparks
    volumes:
      - LaufwerkS:/mnt/NAS

volumes:
  LaufwerkS:
    driver_opts:
      type: cifs
      o: "username=ea_system,password=MEINPW123"
      device: "//192.168.178.199/ea-gmbh"

oder 

version: '3.8'
  
services:
  minio:
    restart: unless-stopped
    image: 'bitnami/minio:latest'
    ports:
      - '9000:9000'
      - '9001:9001'
    environment:
      - MINIO_ROOT_USER=admin
      - MINIO_ROOT_PASSWORD=Unude0
    networks:
      - praxistool-nw
    volumes:
      - hetzner_storagebox_minio:/bitnami/minio/data
      - data:/data
      - certs:/certs


volumes:
  data:
  certs:
  hetzner_storagebox_minio:
    driver: local
    driver_opts:
      type: cifs
      o: "username=u4b1,password=zpp,uid=1001,gid=1001,file_mode=0777,dir_mode=0777,vers=3.1.1,seal"
      device: "//u4b1.your-storagebox.de/u4b1/dockervolume_minio"

networks:
  praxistool-nw:
    driver: bridge

wichtig! vers=3.1.1 und seal parameter für sicherheit und encryption!

! Portainer Stack: Environment Variables

Wenn in Docker Portainer ein Stack erstellt und darin Umgebungsvariablen geladen werden sollen, muss im docker-compose die .env Datei als 'stack.env' angegeben werden:

    volumes:
      - ...
      - ... 
    env_file:
      - stack.env
    ports:
      - ...
drawing-4-1720124079.png

Dabei ist egal, ob die Variablen aus einer .env geladen, im Advanced Mode oder wie auf dem Screenshot im Simple Modus ausgefüllt wurden.

Zusatzinfo: Der Doppelpunkt gefolgt vom Minus im docker-compose.yml
"${COMPOSE_PORT_HTTP:-9000}:9000"
gibt den Standardwert an, falls die angegebene env-var nicht gefunden wurde.

! CPU+RAM limitieren

    deploy:
      resources:
        limits:
          cpus: '0.95'
          memory: 500M

zb in nextcloud:

  app:
    image: nextcloud:apache
    restart: always
    ports:
      - 8654:80
    volumes:
      - app02:/var/www/html:z
    environment:
      - POSTGRES_HOST=db
      - REDIS_HOST=redis
    env_file:
      - stack.env
    depends_on:
      - db
      - redis
    deploy:
      resources:
        limits:
          cpus: '0.95'
          memory: 500M


Docker Images updaten

sudo docker-compose pull && sudo docker-compose up

quelle: 

https://stackoverflow.com/a/39127792

ODER

einfach watchtower als Container erstellen:

https://schroederdennis.de/allgemein/watchtower-automatische-docker-container-updates/

#Regelmäßig alle 6 Stunden und nur mit Label Enable!
docker run -it -d \
    --name WatchTower \
    -v /var/run/docker.sock:/var/run/docker.sock \
    containrrr/watchtower \
    --label-enable \
    --interval 21600

#Zum einmal ausführen

docker run --rm \
    --name WatchTower \
    -v /var/run/docker.sock:/var/run/docker.sock \
    -e WATCHTOWER_NOTIFICATIONS=gotify \
    -e WATCHTOWER_NOTIFICATION_GOTIFY_URL="http://192.168.10.7:5001" \
    -e WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN="ASlcSC1ofE.dmaG" \
    containrrr/watchtower \
    --run-once \
    --cleanup \
    --include-restarting \
    --rolling-restart \
    --include-stopped

#Regelmäßig alle 6 Stunden
docker run -it -d \
    --name WatchTower \
    -v /var/run/docker.sock:/var/run/docker.sock \
    containrrr/watchtower \
    --cleanup \
    --include-restarting \
    --rolling-restart \
    --include-stopped \
    --interval 21600
    

Am besten aber über ein Stack mit docker-compose:

https://wiki.folkerts.it/books/docker/page/watchtower

 

Wichtige Docker Befehle

in Container Shell ausführen

docker exec -it gitlab-web-1 bash
oder
docker exec -it gitlab-web-1 sh


Docker CPU / Memory Verbrauch der einzelnen Container anzeigen

 docker ps -q | xargs  docker stats --no-stream
 oder 
 docker ps -q | xargs  docker stats

Portainer Console geht nicht

Wenn man in Portainer nicht auf die Konsole eines Containers gelangt, könnte es sein, dass im NGINX Proxy Manager der Haken bei Websockets fehlt:

image.png

Haken setzen, danach sollte es klappen.

 

Quelle
Google: portainer console stuck 'Exec into container as default user using command bash'
https://github.com/portainer/portainer/issues/3940#issuecomment-1203806507

 

Docker-Volumes mit winSCP kopieren

 

https://mulcas.com/connect-to-an-ubuntu-server-as-a-root-using-winscp/

Hetzner Storage Box über Samba einbinden

am Beispiel minio

Am besten die CIFS/Samba Share über den Dockerhost mit fstab mounten und nicht in docker-compose selbst. Dafür:

nano /etc/fstab
//u12345-sub1.your-storagebox.de/u12345-sub1/dockervolume_minio /mnt/storagebox cifs iocharset=utf8,rw,credentials=/etc/backup-credentials.txt,uid=1001,gid=1001,vers=3.1.1,seal,file_mode=0660,dir_mode=0770 0 0

nano /etc/backup-credentials.txt

username=u12345-sub1
password=SUBACCOUNTSTORAGE......PASSWORD

testen mit

mount -a

und untesten mit 

umount -a

WICHTIG
vers=3.1.1,seal hinten dranhängen, um eine möglichst sichere Samba-Verbindung herzustellen

in diesem fall wurde ein sub-account in der Hetzner Storage Box angelegt, dabei wird der uesrname und freigabename geändert:

Wenn Sie Ihren Haupt-Account verwenden, lautet der Freigabename backup
Bei der Verwendung eines Sub-Accounts müssen Sie als Nutzername und Freigabename, den Nutzername des Sub-Accounts verwenden.
Linux/Unix:
//<username>.your-storagebox.de/<Freigabename>
Windows
\\<username>.your-storagebox.de\<Freigabename>

https://docs.hetzner.com/de/robot/storage-box/access/access-samba-cifs
https://docs.hetzner.com/de/robot/storage-box/additional-users 

normalerweise ist der pfad als Hauptnutzer 

user: u12345
pw: im hetzner robot portal im Hauptaccount vergeben
//u12345.your-storagebox.de/backup

und für einen Sub-Account
user: u12345-sub1
pw: im hetzner robot portal unter sub-accounts vergeben
//u12345-sub1.your-storagebox.de/u12345-sub1

ALTE VERSION (buggy, laggy, reconnects usw)

version: '3.8'
  
services:
  minio:
    restart: unless-stopped
    image: 'bitnami/minio:latest'
    ports:
      - '9000:9000'
      - '9001:9001'
    environment:
      - MINIO_ROOT_USER=admin
      - MINIO_ROOT_PASSWORD=Unude0
    networks:
      - praxistool-nw
    volumes:
      - hetzner_storagebox_minio:/bitnami/minio/data
      - data:/data
      - certs:/certs


volumes:
  data:
  certs:
  hetzner_storagebox_minio:
    driver: local
    driver_opts:
      type: cifs
      o: "username=u4b1,password=zpp,uid=1001,gid=1001,file_mode=0777,dir_mode=0777,vers=3.1.1,seal"
      device: "//u4b1.your-storagebox.de/u4b1/dockervolume_minio"

networks:
  praxistool-nw:
    driver: bridge


MySQL: Fatal glibc error: CPU does not support x86-64-v2

Fatal glibc error: CPU does not support x86-64-v2

fix:

anderes MySQL Image verwenden

  db:
    image: mysql:8-oraclelinux8
    container_name: kimai_db
    restart: unless-stopped

Quellen

https://github.com/docker-library/mysql/issues/1055#issuecomment-2099265909

https://www.reddit.com/r/docker/comments/1cnx28w/fatal_glibc_error_cpu_does_not_support_x8664v2/ 

Docker Standardverzeichnis ändern

aktuelles Dir abfragen:

docker info | grep 'Docker Root Dir'

image.png

daemon.json erstellen falls nicht vorhanden oder ändern:

nano /etc/docker/daemon.json
{
  "data-root": "/path/to/new/docker-data"
}
# docker stoppen
sudo systemctl stop docker
sudo systemctl stop docker.socket

# checken ob gestoppt
ps aux | grep -i docker | grep -v grep

# Dockerverzeichnis rüberkopieren
sudo rsync -axPS /var/lib/docker/ /path/to/new/docker-data

# docker starten
sudo systemctl start docker

# neues dir checken
docker info | grep 'Docker Root Dir' 

# container checken
docker ps

# sicherheitshalber altes Verzeichnis löschen oder umbenennen
sudo rm -r /var/lib/docker

image.png

Error response from daemon: client version 1.xx is too old

Fehlermeldung 

 client version 1.42 is too old. Minimum supported API version is 1.44, please upgrade your client to a newer version

oder

Docker event stream returned error: Error response from daemon: client version 1.25 is too old....

image.png

Loesung:

https://github.com/traefik/traefik/issues/12253#issuecomment-3515555316 

systemctl edit docker.service

# nach 
# ### Anything danach wird discarded
# einfuegen:

[Service]
Environment=DOCKER_MIN_API_VERSION=1.24

# speichern und beenden

systemctl restart docker

! IP-Kollidierung bei Netbird verhindern (192.168.x und 10.1.x)

Falls eine IP-Adresse aus einem anderen Subnetz nicht erreichbar ist, kann es daran liegen, dass eine Docker-Bridge mit diesem Subnetz vorhanden ist.

siehe https://wiki.folkerts.it/books/docker/page/netbird-vpn-it-infrastructure-mit-wireguard-und-authentik 

 ip route show | grep -E "192\."

oder

ip route get 192.168.5.55

root@fn-01:~# ip route get 192.168.5.55
192.168.5.55 dev br-ebe13ac6d23b src 192.168.0.1 uid 0 cache

docker network ls
docker network inspect ebe13ac6d23b

lösung für den einen container

docker-compose.yml

...

      - /var/lib/docker:/backup-var-lib
      - /mnt/fn-volume-01:/backup-fn-volume-01
    networks:
      - zerobyte_network # <<< wichtig

volumes:
  app:

networks:
  zerobyte_network:
    driver: bridge
    ipam:
      config:
        - subnet: 172.69.19.0/24
          gateway: 172.69.19.1

Dauerhafte lösung

nano /etc/docker/daemon.json

{
  "default-address-pools": [
    {
      "base": "172.16.0.0/12",
      "size": 20
    }
  ],
  "data-root": "/mnt/fn-volume-01/docker-data",
  "log-opts": {
    "max-size": "100m",
    "max-file": "5"
  }
}
sudo systemctl restart docker
sudo systemctl status docker