Ctrl K
ComunidadGuías…

docker-compose.yml

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
secrets:

  LDAP_ADMIN_PASSWORD:
    file: ./config/hermes/opt/hermes/keys/ldap_admin_password_file

  LDAP_USER_PASSWORD:
    file: ./config/hermes/opt/hermes/keys/ldap_user_password_file

  AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET:
    file: ./config/hermes/opt/hermes/keys/authelia_identity_validation_reset_password_jwt_secret_file

  AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET:
    file: ./config/hermes/opt/hermes/keys/authelia_identity_providers_oidc_hmac_secret_file

  AUTHELIA_IDENTITY_PROVIDERS_OIDC_JWKS:
    file: ./config/hermes/opt/hermes/keys/authelia_identity_providers_oidc_jwks_file

  AUTHELIA_IDENTITY_PROVIDERS_OIDC_CLIENTS_SECRET_DIGEST:
    file: ./config/hermes/opt/hermes/keys/authelia_identity_providers_oidc_clients_client_secret_digest_file

  AUTHELIA_IDENTITY_PROVIDERS_OIDC_CLIENTS_SECRET_PLAIN:
    file: ./config/hermes/opt/hermes/keys/authelia_identity_providers_oidc_clients_client_secret_plain_file

  AUTHELIA_DUO_API_INTEGRATION_KEY:
    file: ./config/hermes/opt/hermes/keys/authelia_duo_api_integration_key_file

  AUTHELIA_DUO_API_SECRET_KEY:
    file: ./config/hermes/opt/hermes/keys/authelia_duo_api_secret_key_file

  AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD:
    #file: ./config/hermes/opt/hermes/keys/ldap_admin_password_file
    file: ./config/hermes/opt/hermes/keys/ldap_user_password_file

  AUTHELIA_SESSION_SECRET:
    file: ./config/hermes/opt/hermes/keys/authelia_session_secret_file

  AUTHELIA_SESSION_REDIS_PASSWORD:
    file: ./config/hermes/opt/hermes/keys/authelia_session_redis_password_file

  AUTHELIA_STORAGE_ENCRYPTION_KEY:
    file: ./config/hermes/opt/hermes/keys/authelia_storage_encryption_key_file

  # Authelia DB credentials -- live in creds/ alongside the other DB user
  # secrets (hermes, opendmarc, syslog, ciphermail, nextcloud_mysql). The
  # slim config tier backup excludes creds/ so cross-host restore leaves
  # these intact = mariadb authelia user + secret stay aligned.
  AUTHELIA_STORAGE_USERNAME:
    file: ./config/hermes/opt/hermes/creds/authelia_username
  AUTHELIA_STORAGE_PASSWORD:
    file: ./config/hermes/opt/hermes/creds/authelia_password

  NEXTCLOUD_MYSQL_USER:
    file: ./config/hermes/opt/hermes/creds/nextcloud_mysql_username

  NEXTCLOUD_MYSQL_PASSWORD:
    file: ./config/hermes/opt/hermes/creds/nextcloud_mysql_password

  NEXTCLOUD_REDIS_PASSWORD:
    file: ./config/hermes/opt/hermes/creds/nextcloud_redis_password

  NEXTCLOUD_ADMIN_USER:
    file: ./config/hermes/opt/hermes/creds/nextcloud_admin_username

  NEXTCLOUD_ADMIN_PASSWORD:
    file: ./config/hermes/opt/hermes/creds/nextcloud_admin_password

  CFADMIN_PASSWORD:
    file: ./config/hermes/opt/hermes/creds/cfadmin_password

  MYSQL_ROOT_PASSWORD:
    file: ./config/hermes/opt/hermes/creds/mysql_root_password

networks:
  hermes_net_ext:
    ipam:
      driver: default
      config:
        - subnet: ${IPV4SUBNET}.0/24
 

services:
  hermes_unbound:
    image: ${IMAGE_REGISTRY:-ghcr.io/deeztek}/hermes-unbound:${HERMES_DOCKER_IMG_VERSION}
    container_name: hermes_unbound
    logging:
      options:
        max-size: 10m
        max-file: "5"
    volumes:
      - ./config/unbound/unbound.conf:/etc/unbound/unbound.conf:ro
      - ./config/unbound/conf.d:/etc/unbound/conf.d
    restart: unless-stopped
    networks:
      hermes_net_ext:
              ipv4_address: ${IPV4SUBNET}.117
    environment:
      - TZ=${TIMEZONE}

  hermes_db_server:
    image: linuxserver/mariadb
    container_name: hermes_db_server
    logging:
      options:
        max-size: 10m
        max-file: "5"
    dns:
      - ${IPV4SUBNET}.117
    secrets:
      - MYSQL_ROOT_PASSWORD
    environment:
      - PUID=$PUID
      - PGID=$GUID
      # linuxserver/mariadb supports `FILE__<VAR>=<path>` to read the env
      # var value from a Docker secret file. Keeps the root password out
      # of `docker inspect` and out of .env. The secret file content lives
      # at config/hermes/opt/hermes/creds/mysql_root_password (chmod 600,
      # generated by install_hermes_docker.sh).
      # MariaDB sets root@localhost with this password on first init
      # (empty datadir). The install script later creates root@'%' so the
      # database accepts connections from any internal container -- uniform
      # host pattern across all users. Port 3306 is NEVER published to the
      # host, so '%' is restricted to the hermes_net_ext Docker network.
      - FILE__MYSQL_ROOT_PASSWORD=/run/secrets/MYSQL_ROOT_PASSWORD
      - TZ=$TIMEZONE
      #- MYSQL_DATABASE=USER_DB_NAME #optional
      #- MYSQL_USER=MYSQL_USER #optional
      #- MYSQL_PASSWORD=DATABASE_PASSWORD #optional
      #- REMOTE_SQL=http://URL1/your.sql,https://URL2/your.sql #optional
    healthcheck:
      # Authenticated. An unauthenticated `mysqladmin ping` reports success
      # even when the bind is REFUSED (the server answered, so ping is
      # satisfied), so the old probe went green on a database that could
      # not authenticate anyone, while writing an access-denied warning
      # every 10 seconds. That filled the error log with 17GB on a
      # long-running install and buried everything else in it. #339
      #
      # MYSQL_PWD rather than -p so the password never reaches the
      # process list. The secret is already mounted here for
      # FILE__MYSQL_ROOT_PASSWORD, so nothing new is introduced.
      test: ["CMD-SHELL", "MYSQL_PWD=\"$$(cat /run/secrets/MYSQL_ROOT_PASSWORD)\" mysqladmin ping -u root --silent"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    volumes:
      - db_data:/config
      # Hermes-owned MariaDB settings. /etc/my.cnf does `!includedir
      # /etc/my.cnf.d`, and the image starts mariadbd with
      # --defaults-extra-file=/config/custom.cnf, so this is read first
      # and custom.cnf layers on top. Only set what custom.cnf omits. #339
      - ./config/database/etc/my.cnf.d/hermes.cnf:/etc/my.cnf.d/hermes.cnf:ro
      - ./config/hermes/opt/hermes:/opt/hermes
    # No host ports exposed — only accessed by containers on Docker network
    restart: always
    networks:
      hermes_net_ext:
              ipv4_address: ${IPV4SUBNET}.104
    extra_hosts:
    - "host.docker.internal:host-gateway"
    - "${HERMES_HOSTNAME}:${HOST_IP}"


  hermes_ofelia:
    image: 'mcuadros/ofelia:latest'
    container_name: hermes_ofelia
    logging:
      options:
        max-size: 10m
        max-file: "5"
    dns:
      - ${IPV4SUBNET}.117
    environment:
      - "TZ=$TIMEZONE"
    volumes:
      # the ofelia config file
      - ./config/ofelia:/etc/ofelia
      # your docker reg credentials
      #- /home/centos/.docker/config.json:/root/.docker/config.json
      # the docker socket so ofelia can docker run ...
      - /var/run/docker.sock:/var/run/docker.sock:ro
    restart: unless-stopped
    # Ofelia must sit on hermes_net_ext, not the default bridge: its [global]
    # smtp-host in config/ofelia/config.ini resolves hermes_postfix_dkim by name
    # and :10026 only accepts mynetworks (172.16.32.0/24). Without this the
    # failure notifications can never be delivered (#303).
    networks:
      hermes_net_ext:
        ipv4_address: ${IPV4SUBNET}.118
    depends_on:
      - hermes_commandbox

  hermes_nginx:
    image: ${IMAGE_REGISTRY:-ghcr.io/deeztek}/hermes-nginx:${HERMES_DOCKER_IMG_VERSION}
    container_name: hermes_nginx
    logging:
      options:
        max-size: 10m
        max-file: "5"
    dns:
      - ${IPV4SUBNET}.117
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=$TIMEZONE
    volumes:
      - ./config/nginx/etc/nginx/nginx.conf:/etc/nginx/nginx.conf
      #- ./config/nginx/etc/nginx/sites-available/hermes.conf:/etc/nginx/sites-enabled/hermes.conf
      #- ./config/nginx/etc/nginx/sites-available/hermes-ssl.conf:/etc/nginx/sites-enabled/hermes-ssl.conf
      - ./config/nginx/etc/nginx/sites-available:/etc/nginx/sites-available
      - ./config/nginx/etc/nginx/sites-enabled:/etc/nginx/sites-enabled
      - ./config/nginx/etc/nginx/snippets:/etc/nginx/snippets
      - ./config/hermes/var/www/html:/var/www/html
      - ./config/hermes/opt/hermes/ssl:/opt/hermes/ssl
      - ./config/certbot/conf:/etc/letsencrypt
      #- /etc/letsencrypt/live/${HERMES_HOSTNAME}/fullchain.pem:/etc/letsencrypt/live/${HERMES_HOSTNAME}/fullchain.pem
      #- /etc/letsencrypt/live/${HERMES_HOSTNAME}/privkey.pem:/etc/letsencrypt/live/${HERMES_HOSTNAME}/privkey.pem
      #- ./config/certbot/conf/:/etc/nginx/ssl/:ro
      - nginx_logs:/var/log
    networks:
      hermes_net_ext:
              ipv4_address: ${IPV4SUBNET}.106
    extra_hosts:
    - "host.docker.internal:host-gateway"
    - "${HERMES_HOSTNAME}:${HOST_IP}"
    ports:
      - "80:80"
      - "443:443"
    depends_on:
      - hermes_authelia
      - hermes_ciphermail
      - hermes_commandbox
    restart: always
 
  hermes_authelia:
    image: authelia/authelia:${AUTHELIAVERSION}
    container_name: hermes_authelia
    dns:
      - ${IPV4SUBNET}.117
    secrets:
      - AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET
      - AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET
      - AUTHELIA_IDENTITY_PROVIDERS_OIDC_JWKS
      - AUTHELIA_IDENTITY_PROVIDERS_OIDC_CLIENTS_SECRET_DIGEST
      - AUTHELIA_DUO_API_INTEGRATION_KEY
      - AUTHELIA_DUO_API_SECRET_KEY
      - AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD
      - AUTHELIA_SESSION_SECRET
      - AUTHELIA_SESSION_REDIS_PASSWORD
      - AUTHELIA_STORAGE_ENCRYPTION_KEY
      - AUTHELIA_STORAGE_USERNAME
      - AUTHELIA_STORAGE_PASSWORD
    logging:
      options:
        max-size: 10m
        max-file: "5"
    volumes:
      - ./config/authelia:/config
      - ./config/hermes/var/www/html/dist/img:/var/www/html/dist/img
      - ./config/hermes/opt/hermes/keys:/keys
      - authelia_logs:/logs
      - authelia_db:/db
    networks:
      hermes_net_ext:
              ipv4_address: ${IPV4SUBNET}.108
    extra_hosts:
    - "host.docker.internal:host-gateway"
    - "${HERMES_HOSTNAME}:${HOST_IP}"

    expose:
      - 9091
    restart: always
    environment:
      - TZ=${TIMEZONE}
      - X_AUTHELIA_CONFIG_FILTERS=template #Enable Authelia template file filter https://www.authelia.com/configuration/methods/files/#file-filters
      - AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE=/run/secrets/AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET
      - AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE=/run/secrets/AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET
      - AUTHELIA_DUO_API_INTEGRATION_KEY_FILE=/run/secrets/AUTHELIA_DUO_API_INTEGRATION_KEY
      - AUTHELIA_DUO_API_SECRET_KEY_FILE=/run/secrets/AUTHELIA_DUO_API_SECRET_KEY
      - AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE=/run/secrets/AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD
      - AUTHELIA_SESSION_SECRET_FILE=/run/secrets/AUTHELIA_SESSION_SECRET
      - AUTHELIA_SESSION_REDIS_PASSWORD_FILE=/run/secrets/AUTHELIA_SESSION_REDIS_PASSWORD
      - AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE=/run/secrets/AUTHELIA_STORAGE_ENCRYPTION_KEY
      - LDAP_USER_USERNAME=${LDAP_USER_USERNAME}
      - LDAP_BASE_DN=${LDAP_BASE_DN}
    depends_on:
      hermes_postfix_dkim:
        condition: service_started
      hermes_ldap:
        condition: service_healthy


  hermes_authelia_redis:
    image: redis
    container_name: hermes_authelia_redis
    dns:
      - ${IPV4SUBNET}.117
    command: sh -c 'redis-server --requirepass "$$(cat /run/secrets/AUTHELIA_SESSION_REDIS_PASSWORD)"'
    secrets:
      - AUTHELIA_SESSION_REDIS_PASSWORD
    logging:
      options:
        max-size: 10m
    volumes:
      - authelia_redis:/data
    networks:
      hermes_net_ext:
              ipv4_address: ${IPV4SUBNET}.109
    extra_hosts:
    - "host.docker.internal:host-gateway"
    - "${HERMES_HOSTNAME}:${HOST_IP}"
    expose:
      - 6379
    restart: always
    environment:
      - TZ=${TIMEZONE}


  hermes_commandbox:
    image: ${IMAGE_REGISTRY:-ghcr.io/deeztek}/hermes-commandbox:${HERMES_DOCKER_IMG_VERSION}
    container_name: hermes_commandbox
    cap_add:
      - SYS_ADMIN
    secrets:
      - CFADMIN_PASSWORD
    logging:
      options:
        max-size: 10m
    dns:
      - ${IPV4SUBNET}.117
    environment:
      TZ: ${TIMEZONE}
      CFADMIN_PASSWORD_FILE: /run/secrets/CFADMIN_PASSWORD
      #HERMES_HOSTNAME: ${HERMES_HOSTNAME}
      #HOST_IP: ${HOST_IP}
      CONSOLE_HOST: ${CONSOLE_HOST}
      #url_rewrites: "true"
    volumes:
      - ./.env:/opt/hermes/.env:ro #.env mounted read-only for install-time values
      - /var/run/docker.sock:/var/run/docker.sock #Access Docker Unix socket
      - ./config/hermes/var/www/html:/var/www/html #Application Directory
      - commandbox_serverhome:/serverhome #Server Home Directory
      - amavis_data:/mnt/data/amavis #Amavis Data
      - dovecot_mail:/mnt/data/vmail:ro #Dovecot Vmail Data (read-only for monitoring)
      - dovecot_sieve:/mnt/data/sieve #Sieve scripts (shared with hermes_dovecot)
      - nextcloud:/mnt/data/nextcloud #Nextcloud Data
      - ./config/ciphermail/usr/share/djigzo:/usr/share/djigzo #Ciphermail
      - ./config/hermes/opt/hermes:/opt/hermes
      - ./config/commandbox/serverhome/WEB-INF/lucee-server/context/password.txt:/serverhome/WEB-INF/lucee-server/context/password.txt
      - ./config/certbot/conf:/etc/letsencrypt
      - ./config/ofelia:/etc/ofelia
      # Every log volume in the stack, so one scheduled task can rotate
      # all of them. /servicelogs rather than /opt/hermes/logs: the latter
      # is inside a bind mount of the repo working tree, and mail_archive.sh
      # already uses it for mailarchive.log.
      - authelia_logs:/servicelogs/authelia
      - dovecot_logs:/servicelogs/dovecot
      - postfix_dkim_logs:/servicelogs/postfix_dkim
      - mail_filter_logs:/servicelogs/mail_filter
      - dmarc_logs:/servicelogs/dmarc
      - openarc_logs:/servicelogs/openarc
      - ldap_logs:/servicelogs/ldap
      - nginx_logs:/servicelogs/nginx
      - ./config/authelia:/etc/authelia
      - ./config/unbound/conf.d:/etc/unbound/conf.d # Unbound
      - ./config/nginx/etc/nginx:/etc/nginx
      - ./config/dovecot-2.4/conf:/etc/dovecot
      - ./config/postfix-dkim/etc/postfix:/etc/postfix
      - ./config/postfix-dkim/etc/opendkim.conf:/etc/opendkim.conf
      - ./config/postfix-dkim/etc/postfix-policyd-spf-python:/etc/postfix-policyd-spf-python
      - ./config/opendmarc/etc/opendmarc:/etc/opendmarc
      - ./config/openarc/etc/openarc:/etc/openarc #OpenARC config (#229 - regenerated by inc/arc_generate_config_file.cfm)
      - ./config/mail_filter/etc/amavis:/etc/amavis #Amavis config (mynetworks, white/black lists)
      - ./config/body_milter/etc/hermes/body_milter:/etc/hermes/body_milter #Body milter config (#214 disclaimers, future signatures/banners/linkguard) - CFML writes maps + per-scope files; the milter container mtime-watches and reloads
      - ./config/linkguard/etc/linkguard:/etc/linkguard #Link Guard (#186) container config - CFML linkguard_write_and_reload.cfm writes config.json/api_key/keys; the hermes_linkguard container mtime-watches and reloads
      - ./config/mail_filter/etc/spamassassin:/etc/spamassassin #SpamAssassin config (local.cf)
      - ./config/mail_filter/etc/clamav:/etc/clamav #ClamAV config (clamd.conf)
      - ./config/mail_filter/etc/fangfrisch/fangfrisch.conf:/etc/fangfrisch/fangfrisch.conf #Fangfrisch config (malware feeds)
      - mail_filter_data_clamav:/var/lib/clamav #ClamAV signature data (local.ign2)
      - ./config/ldap:/ldap
      - ./config/postfix-dkim/etc/rsyslog.d:/mnt/rsyslog/postfix
      - ./config/opendmarc/etc/rsyslog.d:/mnt/rsyslog/opendmarc
      - ./config/mail_filter/etc/rsyslog.d:/mnt/rsyslog/mail_filter
      - ./config/ldap/etc/rsyslog.d:/mnt/rsyslog/ldap
      - /proc:/host/proc:ro #Host /proc for system resource monitoring
      - /:/host/root:ro #Host root filesystem for disk usage monitoring
    networks:
      hermes_net_ext:
              ipv4_address: ${IPV4SUBNET}.113
    extra_hosts:
    - "host.docker.internal:host-gateway"
    - "${HERMES_HOSTNAME}:${HOST_IP}"
    # No host ports exposed — Nginx proxies to 8888 internally
    restart: always
    privileged: true
    expose:
      - 8888

  hermes_postfix_dkim:
    image: ${IMAGE_REGISTRY:-ghcr.io/deeztek}/hermes-postfix-dkim:${HERMES_DOCKER_IMG_VERSION}
    container_name: hermes_postfix_dkim
    logging:
      options:
        max-size: 10m
    dns:
      - ${IPV4SUBNET}.117
    ports:
      - 25:25
      - 465:465
      - 587:587
  
    restart: unless-stopped
    depends_on:
      hermes_db_server:
        condition: service_healthy

    networks:
      hermes_net_ext:
              ipv4_address: ${IPV4SUBNET}.110
    sysctls:
      - net.ipv6.conf.all.disable_ipv6=1
    extra_hosts:
      - "host.docker.internal:host-gateway"
      - "${HERMES_HOSTNAME}:${HOST_IP}"
    volumes:
      - ./config/postfix-dkim/etc/postfix:/etc/postfix
      - ./config/postfix-dkim/etc/postfix-policyd-spf-python:/etc/postfix-policyd-spf-python
      - ./config/postfix-dkim/etc/rsyslog.d/mysql.conf:/etc/rsyslog.d/mysql.conf
      - ./config/certbot/conf:/etc/letsencrypt
      - ./config/postfix-dkim/etc/opendkim.conf:/etc/opendkim.conf
      - ./config/postfix-dkim/etc/opendkim-sign.conf:/etc/opendkim-sign.conf
      - ./config/hermes/opt/hermes:/opt/hermes
      - postfix_dkim_logs:/var/log
      - postfix_dkim_queue:/var/spool/postfix
    environment:
      - TZ=${TIMEZONE}

  hermes_dmarc:
    image: ${IMAGE_REGISTRY:-ghcr.io/deeztek}/hermes-dmarc:${HERMES_DOCKER_IMG_VERSION}
    container_name: hermes_dmarc
    logging:
      options:
        max-size: 10m
    dns:
      - ${IPV4SUBNET}.117
    # No host ports exposed — internal OpenDMARC
    restart: unless-stopped
    depends_on:
      - hermes_postfix_dkim

    networks:
      hermes_net_ext:
              ipv4_address: ${IPV4SUBNET}.111
    sysctls:
      - net.ipv6.conf.all.disable_ipv6=1
    extra_hosts:
      - "host.docker.internal:host-gateway"
      - "${HERMES_HOSTNAME}:${HOST_IP}"
    volumes:
      - ./config/opendmarc/etc/opendmarc:/etc/opendmarc
      - ./config/opendmarc/etc/rsyslog.d/mysql.conf:/etc/rsyslog.d/mysql.conf
      - ./config/hermes/opt/hermes:/opt/hermes:ro
      - dmarc_logs:/var/log
    environment:
      - TZ=${TIMEZONE}

  hermes_openarc:
    image: ${IMAGE_REGISTRY:-ghcr.io/deeztek}/hermes-openarc:${HERMES_DOCKER_IMG_VERSION}
    container_name: hermes_openarc
    logging:
      options:
        max-size: 10m
    dns:
      - ${IPV4SUBNET}.117
    # No host ports exposed — internal OpenARC milter only reached via inet:hermes_openarc:8893
    restart: unless-stopped
    depends_on:
      - hermes_postfix_dkim

    networks:
      hermes_net_ext:
              ipv4_address: ${IPV4SUBNET}.114
    sysctls:
      - net.ipv6.conf.all.disable_ipv6=1
    extra_hosts:
      - "host.docker.internal:host-gateway"
      - "${HERMES_HOSTNAME}:${HOST_IP}"
    volumes:
      - ./config/openarc/etc/openarc:/etc/openarc
      - ./config/openarc/etc/rsyslog.d/openarc.conf:/etc/rsyslog.d/openarc.conf
      - ./config/hermes/opt/hermes:/opt/hermes
      - openarc_logs:/var/log
    environment:
      - TZ=${TIMEZONE}



  hermes_mail_filter:
    image: ${IMAGE_REGISTRY:-ghcr.io/deeztek}/hermes-mail-filter:${HERMES_DOCKER_IMG_VERSION}
    # --- Optional: rebuild locally with DCC ---------------------------------
    # DCC is deliberately NOT in the published image. Its licence is free only
    # to organisations that do not sell filtering devices or services except to
    # their own users, and it forbids redistributing binaries. Most self-hosted
    # operators qualify; Deeztek sells Hermes Pro and does not, so we cannot
    # ship the binaries for you.
    #
    # To enable, uncomment the build: block below and run:
    #   docker compose build hermes_mail_filter
    #   docker compose up -d hermes_mail_filter
    # then set Content Checks > Antispam Settings > DCC = Enabled.
    #
    # NOTE: system_update_docker.sh pulls images, which replaces your local
    # build and removes DCC. Re-run the two commands above after every upgrade.
    # See docs/admin/04-content-checks/antispam-settings.md
    #
    # build:
    #   context: ./Docker/mail_filter
    #   dockerfile: dockerfiles/mail_filter/Dockerfile
    #   args:
    #     WITH_DCC: "true"
    container_name: hermes_mail_filter
    logging:
      options:
        max-size: 10m
    dns:
      - ${IPV4SUBNET}.117
    # No host ports exposed — Amavis/SpamAssassin, Postfix connects internally
    restart: unless-stopped
    depends_on:
      hermes_db_server:
        condition: service_healthy

    networks:
      hermes_net_ext:
              ipv4_address: ${IPV4SUBNET}.105
    sysctls:
      - net.ipv6.conf.all.disable_ipv6=1
    extra_hosts:
      - "host.docker.internal:host-gateway"
      - "${HERMES_HOSTNAME}:${HOST_IP}"
    volumes:
      - ./config/mail_filter/etc/amavis/black.lst:/etc/amavis/black.lst
      - ./config/mail_filter/etc/amavis/white.lst:/etc/amavis/white.lst
      - ./config/mail_filter/etc/amavis/mynetworks:/etc/amavis/mynetworks
      - ./config/mail_filter/etc/amavis/conf.d/50-user:/etc/amavis/conf.d/50-user
      - ./config/mail_filter/etc/amavis/conf.d/15-content_filter_mode:/etc/amavis/conf.d/15-content_filter_mode
      - ./config/postfix-dkim/etc/postfix/relay_domains:/etc/postfix/relay_domains
      - ./config/mail_filter/etc/spamassassin/local.cf:/etc/spamassassin/local.cf
      - ./config/mail_filter/etc/spamassassin/init.pre:/etc/spamassassin/init.pre   # #234 - SPF plugin disabled (postfix-policyd-spf-python is the authoritative verifier)
      - ./config/mail_filter/etc/spamassassin/v312.pre:/etc/spamassassin/v312.pre   # #234 - DKIM plugin disabled (OpenDKIM at :25 is the authoritative verifier)
      - ./config/mail_filter/etc/spamassassin/v400.pre:/etc/spamassassin/v400.pre   # SpamAssassin DMARC plugin disabled (needs the SPF/DKIM plugins #234 disables; OpenDMARC is the authoritative verifier)
      - ./config/mail_filter/sa-bayes:/sa-bayes
      - ./config/mail_filter/etc/clamav/clamd.conf:/etc/clamav/clamd.conf
      - ./config/mail_filter/etc/fangfrisch/fangfrisch.conf:/etc/fangfrisch/fangfrisch.conf
      - ./config/mail_filter/etc/rsyslog.d/mysql.conf:/etc/rsyslog.d/mysql.conf
      - ./config/mail_filter/usr/local/bin/setup-clamav-sigs:/usr/local/bin/setup-clamav-sigs
      - ./config/common/etc/mailname:/etc/mailname
      - ./config/hermes/opt/hermes/sa-bayes:/opt/hermes/sa-bayes
      - mail_filter_logs:/var/log
      - mail_filter_data_amavis:/var/lib/amavis
      - mail_filter_data_clamav:/var/lib/clamav
      - mail_filter_data_fangfrisch:/var/lib/fangfrisch
      - amavis_data:/mnt/data/amavis

    environment:
      - TZ=${TIMEZONE}

  hermes_body_milter:
    image: ${IMAGE_REGISTRY:-ghcr.io/deeztek}/hermes-body-milter:${HERMES_DOCKER_IMG_VERSION}
    container_name: hermes_body_milter
    logging:
      options:
        max-size: 10m
    dns:
      - ${IPV4SUBNET}.117
    restart: unless-stopped
    networks:
      hermes_net_ext:
        ipv4_address: ${IPV4SUBNET}.119
    extra_hosts:
      - "host.docker.internal:host-gateway"
      - "${HERMES_HOSTNAME}:${HOST_IP}"
    volumes:
      # Modifier configuration root. CFML regenerator writes here; the
      # milter mtime-watches each map and reloads on change. Link Guard's
      # HMAC keys (linkguard_hmac_key[_previous]) and the linkguard/ config
      # subtree are written here too — same shared mount, so no extra bind.
      - ./config/body_milter/etc/hermes/body_milter:/etc/hermes/body_milter
      # Read-only access to postfix's relay_domains (canonical "is this
      # a local domain?" lookup, used to determine outbound vs inbound).
      - ./config/postfix-dkim/etc/postfix/relay_domains:/etc/postfix/relay_domains:ro
      # Link Guard (#186) stateful v2 token store. The milter is the SOLE WRITER
      # of url_map.db here (short id -> original URL); the hermes_linkguard
      # container reads the SAME file read-only to resolve clicks. Shared via the
      # same named volume; rollback-journal (not WAL) so cross-container RO needs
      # no shared-memory file. Keeps the token tiny -> no over-length skip.
      - linkguard_data:/var/lib/linkguard
    environment:
      - TZ=${TIMEZONE}
      - LG_URLMAP_DB=/var/lib/linkguard/url_map.db

  hermes_linkguard:
    # Link Guard (#186) time-of-click safe-links endpoint. Self-contained
    # Python service; verifies HMAC-signed link tokens minted by the body
    # milter and resolves a click-time verdict (heuristics + local feeds
    # [+ GSB/VT]). Can also run STANDALONE on a separate Docker host to keep
    # click-time load off the mail box — see Docker/linkguard for the
    # standalone compose. Ports: 8894 public (/lg/, nginx-proxied), 8895
    # mgmt (/api/*, console-only, NOT nginx-proxied).
    image: ${IMAGE_REGISTRY:-ghcr.io/deeztek}/hermes-linkguard:${HERMES_DOCKER_IMG_VERSION}
    container_name: hermes_linkguard
    logging:
      options:
        max-size: 10m
    dns:
      - ${IPV4SUBNET}.117
    restart: unless-stopped
    networks:
      hermes_net_ext:
        ipv4_address: ${IPV4SUBNET}.121
    volumes:
      # Runtime config (config.json, api_key, keys/current.key,
      # keys/previous.key) — CFML writes here via linkguard_write_and_reload.cfm;
      # the service mtime-watches and reloads. In-stack management is via this
      # mount; the /api/* port is for REMOTE instances.
      - ./config/linkguard/etc/linkguard:/etc/linkguard
      # Operational SQLite store (blocklist feeds, verdict cache, click log).
      - linkguard_data:/var/lib/linkguard
    environment:
      - TZ=${TIMEZONE}
      - LG_PUBLIC_PORT=8894
      - LG_MGMT_PORT=8895

  hermes_ciphermail:
    image: ${IMAGE_REGISTRY:-ghcr.io/deeztek}/hermes-ciphermail:${HERMES_DOCKER_IMG_VERSION}
    container_name: hermes_ciphermail
    privileged: true
    logging:
      options:
        max-size: 10m
    dns:
      - ${IPV4SUBNET}.117
    # No host ports exposed — Nginx proxies to 8443, Postfix connects to 25/10025 internally

    restart: unless-stopped
    depends_on:
      - hermes_postfix_dkim

    networks:
      hermes_net_ext:
              ipv4_address: ${IPV4SUBNET}.112
    sysctls:
      - net.ipv6.conf.all.disable_ipv6=1
    extra_hosts:
      - "host.docker.internal:host-gateway"
      - "${HERMES_HOSTNAME}:${HOST_IP}"
    volumes:
      #- ./config/ciphermail/etc/default/tomcat10:/etc/default/tomcat10
      - ./config/ciphermail/usr/share/djigzo/conf/james/SAR-INF/smtp_transport_config.xml:/usr/share/djigzo/conf/james/SAR-INF/smtp_transport_config.xml
      #- ./config/ciphermail/usr/share/djigzo/conf/james/SAR-INF/smtp_server_config.xml:/usr/share/djigzo/conf/james/SAR-INF/smtp_server_config.xml
      - ./config/ciphermail/usr/share/djigzo/conf/james/SAR-INF/config.xml:/usr/share/djigzo/conf/james/SAR-INF/config.xml
      - ./config/ciphermail/usr/share/djigzo/conf/database/hibernate.mysql.connection.xml:/usr/share/djigzo/conf/database/hibernate.mysql.connection.xml
      - ./config/ciphermail/usr/share/djigzo/conf/database/hibernate.cfg.xml:/usr/share/djigzo/conf/database/hibernate.cfg.xml
      #- ./config/ciphermail/usr/share/djigzo/wrapper/wrapper-additional-parameters.conf:/usr/share/djigzo/wrapper/wrapper-additional-parameters.conf
      #- ./config/ciphermail/opt/tomcat/conf/server.xml:/opt/tomcat/conf/server.xml
      #- ./config/ciphermail/opt/tomcat/conf/Catalina/localhost/ciphermail.xml:/opt/tomcat/conf/Catalina/localhost/ciphermail.xml
      #- ./config/ciphermail/opt/tomcat/conf/Catalina/localhost/web.xml:/opt/tomcat/conf/Catalina/localhost/web.xml
      - ./config/hermes/opt/hermes:/opt/hermes
    environment:
      - TZ=${TIMEZONE}


  hermes_fail2ban:
    image: lscr.io/linuxserver/fail2ban:latest
    container_name: hermes_fail2ban
    build:
      context: .
      dockerfile: ./Docker/fail2ban/dockerfiles/fail2ban/Dockerfile
    dns:
      - ${IPV4SUBNET}.117
    cap_add:
      - NET_ADMIN
      - NET_RAW
    # Host networking required for iptables to work on host's network stack
    network_mode: host
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=${TIMEZONE}
      - HERMES_COMMANDBOX_IP=${IPV4SUBNET}.113
      - VERBOSITY=-vv #optional
    volumes:
      - ./config/fail2ban/config:/config
      - ./config/fail2ban/scripts:/scripts
      - ./config/fail2ban/scripts/detect-iptables-backend.sh:/custom-cont-init.d/10-detect-iptables-backend:ro
      - dovecot_logs:/remotelogs/dovecot:ro
      - authelia_logs:/remotelogs/authelia:ro
      - postfix_dkim_logs:/remotelogs/postfix_dkim:ro
      - ./config/hermes/opt/hermes:/opt/hermes:ro
    restart: always
    # Note: networks and extra_hosts removed - not compatible with network_mode: host

  hermes_nextcloud_redis:
    image: redis
    container_name: hermes_nextcloud_redis
    dns:
      - ${IPV4SUBNET}.117
    logging:
      options:
        max-size: 10m
    volumes:
      - nextcloud_redis:/var/lib/redis
    command: sh -c 'redis-server --requirepass "$$(cat /run/secrets/NEXTCLOUD_REDIS_PASSWORD)"'
    secrets:
      - NEXTCLOUD_REDIS_PASSWORD
    restart: always
    networks:
      hermes_net_ext:
              ipv4_address: ${IPV4SUBNET}.115
    expose:
      - 6379


  hermes_nextcloud:
    image: nextcloud:${NCVERSION}
    container_name: hermes_nextcloud
    # Optional local build: adds troubleshooting utilities (dnsutils, telnet,
    # iputils-ping) on top of the upstream nextcloud:${NCVERSION} image. Not
    # required for normal operation. Uncomment to enable, then re-run
    # `docker compose up -d --build hermes_nextcloud`.
    #build:
      #context: .
      #dockerfile: ./Docker/nextcloud/dockerfiles/nextcloud/Dockerfile
      #args:
        #NCVERSION: ${NCVERSION}
    dns:
      - ${IPV4SUBNET}.117
    secrets:
      - NEXTCLOUD_MYSQL_USER
      - NEXTCLOUD_MYSQL_PASSWORD
      - NEXTCLOUD_REDIS_PASSWORD
      - NEXTCLOUD_ADMIN_USER
      - NEXTCLOUD_ADMIN_PASSWORD
    logging:
      options:
        max-size: 10m
    # No host ports exposed — Nginx proxies to port 80 internally at /nc/
    volumes:
      - nextcloud:/var/www/html
      - ./config/hermes/var/www/html/dist/img:/img:ro #Hermes logo for Nextcloud theming (#399)
      - ./config/hermes/opt/hermes:/opt/hermes #Shared volume for app password crypto, creds, and temp files
    environment:
      - TZ=${TIMEZONE}
      - MYSQL_HOST=hermes_db_server:3306
      - MYSQL_USER_FILE=/run/secrets/NEXTCLOUD_MYSQL_USER
      - MYSQL_PASSWORD_FILE=/run/secrets/NEXTCLOUD_MYSQL_PASSWORD
      - MYSQL_DATABASE=nextcloud
      - REDIS_HOST=hermes_nextcloud_redis
      - REDIS_HOST_PASSWORD_FILE=/run/secrets/NEXTCLOUD_REDIS_PASSWORD
      - NEXTCLOUD_ADMIN_USER_FILE=/run/secrets/NEXTCLOUD_ADMIN_USER
      - NEXTCLOUD_ADMIN_PASSWORD_FILE=/run/secrets/NEXTCLOUD_ADMIN_PASSWORD
      - NEXTCLOUD_TRUSTED_DOMAINS=${NEXTCLOUD_TRUSTED_DOMAINS}
      - OVERWRITEWEBROOT=/nc
      - OVERWRITEPROTOCOL=https
      # OVERWRITECLIURL is the base URL Nextcloud uses for cron, occ
      # commands, and any code path that builds URLs without an HTTP
      # request context. It must NOT include the webroot suffix (/nc) -
      # the URL generator appends OVERWRITEWEBROOT separately when
      # constructing absolute URLs. Including /nc here causes doubling
      # (e.g. /nc/nc/index.php/apps/...) in the OIDC auto-redirect flow.
      # Read at runtime by reverse-proxy.config.php.
      - OVERWRITECLIURL=https://${HERMES_HOSTNAME}
      #- APACHE_DISABLE_REWRITE_IP=1
      - TRUSTED_PROXIES=${TRUSTED_PROXIES}
      - SMTP_HOST=hermes_postfix_dkim
      - SMTP_PORT=10026
      - SMTP_SECURE=
      - MAIL_FROM_ADDRESS=postmaster
      - MAIL_DOMAIN=hermes.local
    depends_on:
      hermes_nextcloud_redis:
        condition: service_started
      hermes_db_server:
        condition: service_healthy
    restart: unless-stopped
    networks:
      hermes_net_ext:
              ipv4_address: ${IPV4SUBNET}.101
    extra_hosts:
    - "host.docker.internal:host-gateway"
    - "${HERMES_HOSTNAME}:${HOST_IP}"


  hermes_dovecot:
    image: ${IMAGE_REGISTRY:-ghcr.io/deeztek}/hermes-dovecot:${HERMES_DOCKER_IMG_VERSION}
    container_name: hermes_dovecot
    dns:
      - ${IPV4SUBNET}.117
    logging:
      options:
        max-size: 10m
    ports:
      - 993:993
      - 995:995
      - 143:143
      - 110:110
      # Port 24 (LMTP) not exposed — Postfix connects internally
      - 4190:4190
      # Port 9587 (SASL auth) not exposed — Postfix connects internally
    depends_on:
      hermes_db_server:
        condition: service_healthy
      hermes_ldap:
        condition: service_healthy
    restart: unless-stopped
    networks:
      hermes_net_ext:
              ipv4_address: ${IPV4SUBNET}.100

    extra_hosts:
    - "host.docker.internal:host-gateway"
    - "${HERMES_HOSTNAME}:${HOST_IP}"
    volumes:
      - ./config/dovecot-2.4/conf:/etc/dovecot
      - ./config/dovecot-2.4/scripts:/scripts
      - ./config/certbot/conf:/etc/letsencrypt
      - ./config/hermes/opt/hermes/ssl:/opt/hermes/ssl
      - /opt/hermes/keys/ecprivkey.pem:/keys/ecprivkey.pem
      - /opt/hermes/keys/ecpubkey.pem:/keys/ecpubkey.pem
      - dovecot_mail:/srv/mail
      - dovecot_sieve:/srv/sieve
      - dovecot_logs:/logs
      #- /etc/letsencrypt/live/${HERMES_HOSTNAME}/fullchain.pem:/certs/fullchain.pem
      #- /etc/letsencrypt/live/${HERMES_HOSTNAME}/privkey.pem:/certs/privkey.pem
    environment:
      - TZ=${TIMEZONE}


  hermes_ldap:
    image: ${IMAGE_REGISTRY:-ghcr.io/deeztek}/hermes-openldap:${HERMES_DOCKER_IMG_VERSION}
    container_name: hermes_ldap
    logging:
      options:
        max-size: 10m
        max-file: "5"
    dns:
      - ${IPV4SUBNET}.117
    #entrypoint: ["sleep", "infinity"]
    secrets:
      - LDAP_ADMIN_PASSWORD
      - LDAP_USER_PASSWORD
    environment:
      LDAP_DOMAIN: ${LDAP_DOMAIN}
      LDAP_ADMIN_USERNAME: ${LDAP_ADMIN_USERNAME}
      LDAP_USER_USERNAME: ${LDAP_USER_USERNAME}
      LDAP_GROUPS: ${LDAP_GROUPS}
      LDAP_LOG_LEVEL: ${LDAP_LOG_LEVEL}
      # Without TZ this container runs UTC while the rest run local time,
      # which misaligns slapd events against mail events in the Syslog DB.
      # Needs tzdata in the image or glibc falls back to UTC labelled
      # "America" -- see the openldap Dockerfile (#305).
      TZ: ${TIMEZONE}
    healthcheck:
      test: ["CMD", "/usr/local/bin/ldapsearch", "-x", "-H", "ldap://localhost:389", "-b", "", "-s", "base", "(objectclass=*)"]
      # 60s, not 10s: every check is an anonymous bind + search that slapd
      # logs as 7 lines, ~60,000 a day into the Syslog database at 10s (#367).
      interval: 60s
      timeout: 5s
      retries: 5
      start_period: 15s
      # Checks every 5s until healthy (within start_period), so the services
      # that wait for LDAP to be healthy do not wait a full interval at start.
      # Needs Docker Engine 25.0+.
      start_interval: 5s
    volumes:
      - ldap_data:/var/lib/ldap
      - ldap_logs:/var/log
      - ./config/ldap/hermes:/custom
      - ./config/hermes/opt/hermes/certs:/opt/hermes/certs:ro
      - ./config/hermes/opt/hermes/tmp:/opt/hermes/tmp
      - ./config/ldap/etc/rsyslog.d/slapd.conf:/etc/rsyslog.d/slapd.conf
      - ./config/ldap/etc/rsyslog.d/mysql.conf:/etc/rsyslog.d/mysql.conf
      - ./config/ldap/etc/ldap/slapd.d:/etc/ldap/slapd.d
    ports:
      - "389:389"
      - "636:636"
    networks:
      hermes_net_ext:
              ipv4_address: ${IPV4SUBNET}.120
    restart: always


volumes:

  amavis_data:
    # Amavis quarantine archive. Lives on its own tier (ARCHIVE_MOUNT, #260)
    # so it can use cheap bulk storage independent of the latency-sensitive
    # DBs on DATA_MOUNT. Default /mnt/archive; install_hermes_docker.sh
    # prompts for the path and writes it into .env.
    driver: local
    driver_opts:
      type: none
      o: bind
      device: ${ARCHIVE_MOUNT}/amavis

  db_data:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: ${DATA_MOUNT}/dbase

  linkguard_data:
    # Link Guard operational SQLite (blocklist feeds, verdict cache, click
    # log). On DATA_MOUNT; install_hermes_docker.sh provision_mount_dirs
    # creates ${DATA_MOUNT}/linkguard (owned by the container uid).
    driver: local
    driver_opts:
      type: none
      o: bind
      device: ${DATA_MOUNT}/linkguard

  nextcloud_redis:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: ${FILES_MOUNT}/redis

  nextcloud:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: ${FILES_MOUNT}/app

  dovecot_mail:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: ${VMAIL_MOUNT}/dovecot

  dovecot_sieve:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: ${DATA_MOUNT}/dovecot/sieve

  authelia_redis:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: ${DATA_MOUNT}/authelia/redis


  dovecot_logs:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: ${DATA_MOUNT}/dovecot/logs

  authelia_logs:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: ${DATA_MOUNT}/authelia/logs

  authelia_db:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: ${DATA_MOUNT}/authelia/db

  nginx_logs:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: ${DATA_MOUNT}/nginx/logs


  commandbox_serverhome:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: ${DATA_MOUNT}/commandbox/serverhome


  ldap_data:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: ${DATA_MOUNT}/ldap/data

  ldap_logs:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: ${DATA_MOUNT}/ldap/logs


  postfix_dkim_logs:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: ${DATA_MOUNT}/postfix_dkim/logs

  postfix_dkim_queue:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: ${DATA_MOUNT}/postfix_dkim/queue

  dmarc_logs:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: ${DATA_MOUNT}/dmarc/logs

  openarc_logs:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: ${DATA_MOUNT}/openarc/logs

  mail_filter_logs:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: ${DATA_MOUNT}/mail_filter/logs

  mail_filter_data_amavis:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: ${DATA_MOUNT}/mail_filter/data/amavis

  mail_filter_data_clamav:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: ${DATA_MOUNT}/mail_filter/data/clamav
 
  mail_filter_data_fangfrisch:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: ${DATA_MOUNT}/mail_filter/data/fangfrisch