docker-compose.yml
Rama main · réplica de lectura del repositorio original.
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