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__=` 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