Docker Mailserver. Собственный почтовый сервер для домена, миграция
linux Docker Traefik Debian почта imapsync Rspamd Postfix Dovecot Docker Mailserver
Первоначально я пользовался сервисом почты для домена от Яндекса, но однажды они сильно ограничили бесплатный тариф двумя, если не ошибаюсь, пользователями (читай ящиками). А мне хотя бы 3 надо (по факту использую 5). Ну ладно, переехал на сервис Мэйла, который VK WorkSpace. И вот на днях (10 июня 2026 года) они прислали письмо, мол, сторонние почтовые программы работать не будут, переходите в наш говноапп (назовем вещи своими словами). Они там ухи объелись? И главное непонятно, это только бесплатных пользователей касается или корпоратов тоже. В любом случае, я тогда лучше заплачу хостинг-провайдеру за VDS/VPS и разверну собственный почтовый сервер. Проблема, конечно, в доверии к такому серверу со стороны крупных почтовиков, но попробуем прорваться хотя бы в спам за счет DKIM, SPF и всего остального.
Наверное для очистки совести стоит все же упомянуть о почтовом хостинге от reg.ru. На первый взгляд это выгодное предложение, но обнаружился маленький нюанс в виде отдельной платы за IPv4-адрес, что делает предложение не столь выгодным. Хотя возможно смысл и был бы в случае использования еще и обычного хостинга, жаль что на хранилище S3 это не распространяется.
По теме есть отличная статья Настройка Postfix + Dovecot + Postfixadmin + Roundcube + DKIM на Debian, но хотелось бы все-таки решить задачу через Docker для пущей переносимости. Официального образа postfix вроде не существует, и в целом, почитав разные другие материалы, решил, что проще все же настроить какой-нибудь «комбайн» типа mailcow, Mailu или Docker Mailserver. Первый я исключил практически сразу – не понравились повышенные системные требования и куча контейнеров (хотя таков путь), а вот между вторым и третьим выбор далеко не столь очевиден. Решил все же остановиться на Docker Mailserver как на более известном, что ли, хотя уже с самого начала было понятно, что с «сертификацией» проще дела обстоят у Mailu.
Подготовка
Прежде всего предлагаю убедиться в том, что IP-адрес вашего сервера не находится в черных списках, например с помощью сервиса из вышеупомянутой статьи. Мне, например, первоначально попался не совсем «чистый» айпишник, и даже неоднократная смена не помогла, так что пришлось еще и проводить рокировку серверов.
Что касается операционной системы, то на мой взгляд, для запуска Docker лучше Debian ничего не придумали. Разве что Arch Linux, но она относительно редко встречается на хостингах, да и в концепции «плавающего» (rolling) релиза вроде как несколько повышается вероятность поломки при очередном обновлении.
Актуальной версией на момент написания статьи является Debian 13 «trixie». Буду абсолютно бессовестным и крайне небезопасным образом работать под root, тем более что контейнер по идее требует таковые права. По крайней мере в документации об этом явно ничего не сказано, разве что в рамках «неофициальной» поддержки Podman.
Итак, обновляю систему:
apt update
apt upgrade
reboot
Опять же, пока мы ничего толком не сделали, предлагаю проверить 25-й порт:
apt install telnet
telnet relay.1c.ru 25
Если все в порядке, то должно установиться соединение. Пример вывода:
Trying 185.12.152.55...
Connected to relay.1c.ru.
Escape character is '^]'.
220 relay.1c.ru ESMTP Tue, 16 Jun 2026 10:27:50 +0300
Выход: Ctrl + ], q. Если же порт закрыт, то соединение установлено не будет, соответственно надо или писать в поддержку, чтобы его открыли, или вообще пробовать другой хостинг и начинать все заново.
Вернемся к настройке ОС. Устанавливаю собственный пароль:
passwd
Для удобства ставлю Midnight Commander:
apt install mc
Настраиваю доступ по SSH по ключу. Тезисно, поскольку статья не об этом:
- генерирую пару в PuTTYgen;
- копирую-вставляю открытый ключ в ~/.ssh/authorized_keys;
- сохраняю ключи в различных форматах себе на диск (в частности «родной» формат подходит не только для самого PuTTY, но и FileZilla);
- проверяю, что работает вход по ключу, сохраняю настройки (сессию) в PuTTY;
- закрываю доступ по паролю в /etc/ssh/sshd_config:
PermitRootLogin prohibit-password;
перезапускаю ssh или вообще всю систему.
Устанавливаю Docker по инструкции:
apt update
apt install ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/debian
Suites: $(. /etc/os-release && echo "$VERSION_CODENAME")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
apt update
apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
По факту первые три команды скорее всего необязательные: база пакетов и так только что была обновлена, ca-certificates и curl с высокой долей вероятности уже установлены, а директория /etc/apt/keyrings существует. Перестраховаться, впрочем, не повредит. Но второй apt update (после добавления репозитория Докера) уже точно обязателен.
Проверяю Докер:
systemctl status docker
docker info
docker run hello-world
Я думал, что конфликт Docker с systemd-resolved характерен только для Arch Linux, но каким-то образом словил его и в Debian. Причем даже явное указание DNS в /etc/docker/daemon.json не помогло, так что бессовестным образом отключил systemd-resolved совсем и прописал сервера Яндекса вручную в /etc/resolv.conf (сначала удалил более недействительную ссылку).
В принципе уже можно добавить A-запись для mail.example.com (по идее субдомен не обязан назваться именно mail, но не будем вносить смуту). Поскольку «корпоративная» почта уже есть и должна работать как можно дольше, с MX и прочими записями в DNS пока что повременим.
Установка Docker Mailserver
Поскольку ничего другого на сервере не предполагается, предлагаю безраздельно занять каталог /srv. В свою очередь, документация предлагает скачать и адаптировать файлы с Гитхаба:
cd /srv
wget https://raw.githubusercontent.com/docker-mailserver/docker-mailserver/master/compose.yaml
wget https://raw.githubusercontent.com/docker-mailserver/docker-mailserver/master/mailserver.env
Заготовка compose.yaml представляла собой следующее:
services:
mailserver:
image: ghcr.io/docker-mailserver/docker-mailserver:latest
container_name: mailserver
# Provide the FQDN of your mail server here (Your DNS MX record should point to this value)
hostname: mail.example.com
env_file: mailserver.env
# More information about the mail-server ports:
# https://docker-mailserver.github.io/docker-mailserver/latest/config/security/understanding-the-ports/
ports:
- "25:25" # SMTP (explicit TLS => STARTTLS, Authentication is DISABLED => use port 465/587 instead)
- "143:143" # IMAP4 (explicit TLS => STARTTLS)
- "465:465" # ESMTP (implicit TLS)
- "587:587" # ESMTP (explicit TLS => STARTTLS)
- "993:993" # IMAP4 (implicit TLS)
volumes:
- ./docker-data/dms/mail-data/:/var/mail/
- ./docker-data/dms/mail-state/:/var/mail-state/
- ./docker-data/dms/mail-logs/:/var/log/mail/
- ./docker-data/dms/config/:/tmp/docker-mailserver/
- /etc/localtime:/etc/localtime:ro
restart: always
stop_grace_period: 1m
# Uncomment if using `ENABLE_FAIL2BAN=1`:
# cap_add:
# - NET_ADMIN
# NOTE: The container image configures a `HEALTHCHECK` internally.
# Docker Compose should default to using `dms-healthcheck`.
# healthcheck:
# test: "ss --listening --ipv4 --tcp | grep --silent ':smtp' || exit 1"
Минимально необходимо указать правильное hostname (в соответствии с MX записью, впрочем пока у меня есть только A-запись) и установить часовой пояс на хосте (в случае чего, подставьте ваш вместо Московского):
timedatectl set-timezone Europe/Moscow
Далее нужно долго и упорно ковырять mailserver.env… Или не особо, значения по умолчанию вроде бы достаточно хороши. Разве что я решил пользоваться Rspamd вместо OpenDKIM и т.д.:
ENABLE_OPENDKIM=0
ENABLE_OPENDMARC=0
ENABLE_POLICYD_SPF=0
ENABLE_RSPAMD=1
ENABLE_AMAVIS=0
Я привел переменные в порядке следования в файле, поэтому включение Rspamd так вот затесалось незаметно. SpamAssasin и Postgrey по идее отключены по умолчанию: ENABLE_SPAMASSASSIN=0, ENABLE_POSTGREY=0. Включать ли серые списки в самом Rspamd: RSPAMD_GREYLISTING=1 – хороший вопрос. По умолчанию выключены, я не стал менять.
Включать ли антивирус – ClamAV – тоже большой вопрос. Владимир (автор serveradmin.ru) говорит, что он сам по себе не очень актуален, а в России еще и недоступен. Поэтому скорее всего включать его не надо.
В завершение необходимо создать пользователя, причем умудриться сделать это в течение двух минут, пока контейнер не перезагрузился. Пробуем:
docker compose up -d
docker exec -it mailserver setup email add user@example.com
docker exec -it mailserver setup alias add postmaster@example.com user@example.com
Здесь mailserver – имя контейнера, указанное в compose.yaml. Создание почтового псевдонима postmaster скорее традиция, но вообще говоря по умолчанию на него приходит оповещения встроенной проверки обновления. Добавление остальных пользователей (почтовых ящиков) осуществляется аналогично через setup email add.
Обычно я еще назначаю один из адресов «по умолчанию», т.е. на случай, если почту отправят на несуществующий ящик на домене. В документации предлагают добавить строку в docker-data/dms/config/postfix-virtual.cf:
@example.com user@example.com
Вот только там не говорится, что надо сначала объявить алиасы ящиков на самих себя! Иначе почта не будет доходить до легитимных адресатов. Т.е.:
user@example.com user@example.com
postmaster@example.com user@example.com
user2@example.com user2@example.com
user3@example.com user3@example.com
@example.com user@example.com
При этом в лог начинают сыпаться предупреждения, что псевдоним не будет добавлен в базу пользователей Dovecot дважды. Увы…
Настройка TLS, а фактически Traefik
Вот тот недостаток, о котором я говорил в самом начале – сертификацией предлагается заниматься самим, причем рекомендуется certbot. Лично у меня душа к нему не лежит, поскольку нужна то ли задача cron (а лучше таймер systemd) на хосте, то ли еще как-то очень сильно мудрить для решения задачи полностью в Docker.
Долго думал и решил все-таки взять Traefik, хотя и это решение в данном случае особым изяществом не отличается. Дело вот в чем: либо мы стреляем пушкой по воробьям, запуская «какой-то» веб-сервис типа whoami, лишь бы получить сертификат, оставляя порты проброшенными непосредственно к mailserver, либо долго и упорно настраиваем именно режим обратного прокси (с которым тоже далеко не все в порядке).
Эх, была не была, сразу сделаем более правильно, то есть обратный прокси. Дополнительно изолируем сокет:
services:
mailserver:
image: ghcr.io/docker-mailserver/docker-mailserver:latest
container_name: mailserver
# Provide the FQDN of your mail server here (Your DNS MX record should point to this value)
hostname: mail.example.com
env_file: mailserver.env
networks:
- traefik-servicenet
volumes:
- ./docker-data/dms/mail-data/:/var/mail/
- ./docker-data/dms/mail-state/:/var/mail-state/
- ./docker-data/dms/mail-logs/:/var/log/mail/
- ./docker-data/dms/config/:/tmp/docker-mailserver/
- ./docker-data/traefik/acme.json:/etc/letsencrypt/acme.json:ro
- /etc/localtime:/etc/localtime:ro
restart: always
stop_grace_period: 1m
# Uncomment if using `ENABLE_FAIL2BAN=1`:
# cap_add:
# - NET_ADMIN
# NOTE: The container image configures a `HEALTHCHECK` internally.
# Docker Compose should default to using `dms-healthcheck`.
# healthcheck:
# test: "ss --listening --ipv4 --tcp | grep --silent ':smtp' || exit 1"
labels:
- traefik.enable=true
# Explicit TLS (STARTTLS) - SMTP
- traefik.tcp.routers.mail-smtp.rule=HostSNI(`*`)
- traefik.tcp.routers.mail-smtp.entrypoints=mail-smtp
- traefik.tcp.routers.mail-smtp.service=mail-smtp
- traefik.tcp.services.mail-smtp.loadbalancer.server.port=25
- traefik.tcp.services.mail-smtp.loadbalancer.proxyProtocol.version=2
# Explicit TLS (STARTTLS) - IMAP
- traefik.tcp.routers.mail-imap.rule=HostSNI(`*`)
- traefik.tcp.routers.mail-imap.entrypoints=mail-imap
- traefik.tcp.routers.mail-imap.service=mail-imap
- traefik.tcp.services.mail-imap.loadbalancer.server.port=143
- traefik.tcp.services.mail-imap.loadbalancer.proxyProtocol.version=2
# Implicit TLS - SMTP
- traefik.tcp.routers.mail-submissions.rule=HostSNI(`mail.example.com`)
- traefik.tcp.routers.mail-submissions.entrypoints=mail-submissions
- traefik.tcp.routers.mail-submissions.tls.passthrough=true
- traefik.tcp.routers.mail-submissions.service=mail-submissions
- traefik.tcp.services.mail-submissions.loadbalancer.server.port=465
- traefik.tcp.services.mail-submissions.loadbalancer.proxyProtocol.version=2
# Explicit TLS (STARTTLS) - SMTP
- traefik.tcp.routers.mail-submission.rule=HostSNI(`*`)
- traefik.tcp.routers.mail-submission.entrypoints=mail-submission
- traefik.tcp.routers.mail-submission.service=mail-submission
- traefik.tcp.services.mail-submission.loadbalancer.server.port=587
- traefik.tcp.services.mail-submission.loadbalancer.proxyProtocol.version=2
# Implicit TLS - IMAP
- traefik.tcp.routers.mail-imaps.rule=HostSNI(`mail.example.com`)
- traefik.tcp.routers.mail-imaps.entrypoints=mail-imaps
- traefik.tcp.routers.mail-imaps.tls.passthrough=true
- traefik.tcp.routers.mail-imaps.service=mail-imaps
- traefik.tcp.services.mail-imaps.loadbalancer.server.port=993
- traefik.tcp.services.mail-imaps.loadbalancer.proxyProtocol.version=2
dockerproxy:
image: wollomatic/socket-proxy:1
command:
- '-loglevel=error'
- '-allowfrom=traefik' # allow only hostname "traefik" to connect
- '-listenip=0.0.0.0'
- '-allowGET=/v1\..{1,2}/(version|containers/.*|events.*)' # this regexp allows readonly access only for requests that Traefik needs
- '-allowHEAD=/_ping' # allow to retrieve Docker API version
- '-allowbindmountfrom=/var/log,/tmp' # restrict bind mounts to specific directories
- '-shutdowngracetime=5' # wait 5 seconds before shutting down
- '-watchdoginterval=3600' # check once per hour for socket availability
- '-stoponwatchdog' # halt program on error and let compose restart it
restart: always
read_only: true
mem_limit: 64M
cap_drop:
- ALL
security_opt:
- no-new-privileges
user: 65534:<<docker-gid>> # replace <<docker-gid>> with the docker gid on your host system
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- docker-proxynet
traefik:
image: traefik:v3.7
restart: always
read_only: true
depends_on:
- mailserver
- dockerproxy
security_opt:
- no-new-privileges:true
networks:
- traefik-servicenet
- docker-proxynet
ports:
# More information about the mail-server ports:
# https://docker-mailserver.github.io/docker-mailserver/latest/config/security/understanding-the-ports/
- "25:25" # SMTP (explicit TLS => STARTTLS, Authentication is DISABLED => use port 465/587 instead)
- "143:143" # IMAP4 (explicit TLS => STARTTLS)
- "465:465" # ESMTP (implicit TLS)
- "587:587" # ESMTP (explicit TLS => STARTTLS)
- "993:993" # IMAP4 (implicit TLS)
# web
- "80:80" # HTTP
- "443:443" # HTTPS
- "443:443/udp" # HTTP/3
volumes:
- ./docker-data/traefik/traefik.yaml:/etc/traefik/traefik.yaml:ro # static configuration
- ./docker-data/traefik/usersfile:/etc/traefik/usersfile:ro # dashboard basic auth
- ./docker-data/traefik/acme.json:/etc/traefik/acme.json # certificate storage
labels:
- traefik.enable=true
- traefik.http.routers.dashboard.rule=Host(`mail.example.com`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))
- traefik.http.routers.dashboard.service=api@internal
- traefik.http.routers.dashboard.middlewares=apiauth
- traefik.http.middlewares.apiauth.basicauth.usersfile=/etc/traefik/usersfile
networks:
traefik-servicenet:
ipam:
config:
- subnet: "172.16.42.0/24"
docker-proxynet:
internal: true
Ой, ой, ой, видите ли он не захотел настроить таймер systemd на хосте, зато не поленился написать огромную простыню кода.
Что же, давайте разбираться. Первое отличие – из сервиса mailserver исчезли порты, их теперь нужно пробрасывать к Traefik'у. Вместо них – метки для того же самого Traefik. Настраиваются они однотипно: порты 25, 143 и 587 никоим образом TLS не задействуют, в то время как для 465 и 993 я решил указать хост явно. Это, в свою очередь, требует обработку TLS непосредственно на уровне почтовика, т.е. добавляется еще и tls.passthrough=true. Также в каждом случае включаем PROXY-протокол для передачи IP-адреса клиента. Сервисы нужно прописывать явно, так как их получается несколько для одного контейнера, и в такой ситуации программа не может автоматически их сопоставить. Фрагмент для IMAP по TLS:
services:
mailserver:
labels:
- traefik.tcp.routers.mail-imaps.rule=HostSNI(`mail.example.com`)
- traefik.tcp.routers.mail-imaps.entrypoints=mail-imaps
- traefik.tcp.routers.mail-imaps.tls.passthrough=true
- traefik.tcp.routers.mail-imaps.service=mail-imaps
- traefik.tcp.services.mail-imaps.loadbalancer.server.port=993
- traefik.tcp.services.mail-imaps.loadbalancer.proxyProtocol.version=2
Надеюсь, я сам ничего не перепутал и призываю вас быть внимательными: при копировании/вставке легко ошибиться (особенно submission/submissions).
Еще мы протаскиваем acme.json:
services:
mailserver:
volumes:
- ./docker-data/traefik/acme.json:/etc/letsencrypt/acme.json:ro
А также подключаем контейнер к «сервисной» сети Traefik в соответствии с концепцией изоляции сокета, при которой используются две сети.
services:
mailserver:
networks:
- traefik-servicenet
Следующий сервис – dockerproxy – взят практически без изменений из документации, разве что я заменил уровень логирования на error. Обратите внимание, что этот сервис, в отличие от предыдущего, подключен к внутренней сети docker-proxynet. Еще нужно будет подставить идентификатор группы docker на хосте, в моем случае это 993, но у вас оно может быть иным.
services:
dockerproxy:
image: wollomatic/socket-proxy:1
command:
- '-loglevel=error'
- '-allowfrom=traefik' # allow only hostname "traefik" to connect
- '-listenip=0.0.0.0'
- '-allowGET=/v1\..{1,2}/(version|containers/.*|events.*)' # this regexp allows readonly access only for requests that Traefik needs
- '-allowHEAD=/_ping' # allow to retrieve Docker API version
- '-allowbindmountfrom=/var/log,/tmp' # restrict bind mounts to specific directories
- '-shutdowngracetime=5' # wait 5 seconds before shutting down
- '-watchdoginterval=3600' # check once per hour for socket availability
- '-stoponwatchdog' # halt program on error and let compose restart it
restart: always
read_only: true
mem_limit: 64M
cap_drop:
- ALL
security_opt:
- no-new-privileges
user: 65534:<<docker-gid>> # replace <<docker-gid>> with the docker gid on your host system
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- docker-proxynet
Наконец, объявляем сам Traefik и сети:
services:
traefik:
image: traefik:v3.7
restart: always
read_only: true
depends_on:
- mailserver
- dockerproxy
security_opt:
- no-new-privileges:true
networks:
- traefik-servicenet
- docker-proxynet
ports:
# More information about the mail-server ports:
# https://docker-mailserver.github.io/docker-mailserver/latest/config/security/understanding-the-ports/
- "25:25" # SMTP (explicit TLS => STARTTLS, Authentication is DISABLED => use port 465/587 instead)
- "143:143" # IMAP4 (explicit TLS => STARTTLS)
- "465:465" # ESMTP (implicit TLS)
- "587:587" # ESMTP (explicit TLS => STARTTLS)
- "993:993" # IMAP4 (implicit TLS)
# web
- "80:80" # HTTP
- "443:443" # HTTPS
- "443:443/udp" # HTTP/3
volumes:
- ./docker-data/traefik/traefik.yaml:/etc/traefik/traefik.yaml:ro # static configuration
- ./docker-data/traefik/usersfile:/etc/traefik/usersfile:ro # dashboard basic auth
- ./docker-data/traefik/acme.json:/etc/traefik/acme.json # certificate storage
labels:
- traefik.enable=true
- traefik.http.routers.dashboard.rule=Host(`mail.example.com`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))
- traefik.http.routers.dashboard.service=api@internal
- traefik.http.routers.dashboard.middlewares=apiauth
- traefik.http.middlewares.apiauth.basicauth.usersfile=/etc/traefik/usersfile
networks:
traefik-servicenet:
ipam:
config:
- subnet: "172.16.42.0/24"
docker-proxynet:
internal: true
Актуальной версией на данный момент является 3.7, ее образ и берем. Добавил небольшое «огораживание» в виде read_only: true и security_opt.no-new-privileges:true, хотя полностью защищенный вариант как в примере делать не стал – почтовый сервер работает от root, ну и Traefik пусть тоже. Хотя вообще говоря можно было бы заморочиться с нестандартными портами и, как следствие, точками входа.
Далее следует подключение к сетям; к почтовым портам добавил веб и пробросил несколько файлов. С помощью меток настроил панель управления непосредственно на субдомене (пока, во всяком случае). Есть подозрение, что помимо контроля как такового это необходимо и для получения сертификата, иначе некому будет слушать 443 порт.
В настройках «сервисной» сети явно обозначил подсеть, она потребуется далее. В официальном примере решили задать IP непосредственно контейнеру, но у меня возник конфликт адресов.
К сожалению, до запуска еще очень далеко. Чтобы не городить еще миллион меток, я обычно выношу ряд настроек Traefik'а в файл. В данном случае это /srv/docker-data/traefik/traefik.yaml:
providers:
docker:
exposedByDefault: false
endpoint: 'tcp://dockerproxy:2375'
network: traefik-servicenet
api: {}
entryPoints:
web:
address: ":80"
http:
redirections:
entrypoint:
to: websecure
scheme: https
websecure:
address: ":443"
asDefault: true
http:
tls:
certResolver: letsencrypt
mail-smtp:
address: ":25"
mail-imap:
address: ":143"
mail-submissions:
address: ":465"
mail-submission:
address: ":587"
mail-imaps:
address: ":993"
certificatesResolvers:
letsencrypt:
acme:
email: your-mail@3rdpartyservice.com
storage: /etc/traefik/acme.json
tlsChallenge: {}
Сначала настраиваем Docker, поскольку он у нас изолирован. Далее помимо стандартных точек входа для веба объявляем почтовые и, наконец, настраиваем Let's Encrypt (подставьте свою почту, наверное лучше от какого-нибудь сервиса, а не на домене).
Теперь сформируем список пользователей панели управления, к сожалению для этого надо установить на хост утилиты от Апача:
apt install apache2-utils
htpasswd -cBC 10 /srv/docker-data/traefik/usersfile admin
Вместо admin желательно написать какое-нибудь другое имя, а пароль будет запрошен интерактивно.
Предварительно создадим файл acme.json, иначе при запуске создастся директория:
touch /srv/docker-data/traefik/acme.json
chmod 600 /srv/docker-data/traefik/acme.json
Права 600, в свою очередь, требует Traefik.
Но и это еще не все, так как требуется перевести Postfix и Dovecot в режим работы позади прокси.
Файл /srv/docker-data/dms/config/postfix-master.cf:
smtp/inet/postscreen_upstream_proxy_protocol=haproxy
submission/inet/smtpd_upstream_proxy_protocol=haproxy
submissions/inet/smtpd_upstream_proxy_protocol=haproxy
Файл /srv/docker-data/dms/config/dovecot.cf:
haproxy_trusted_networks = 172.16.42.0/24
service imap-login {
inet_listener imap {
haproxy = yes
}
inet_listener imaps {
haproxy = yes
}
}
Я не планирую пользоваться pop3 и sieve, поэтому и не стал дублировать их настройки из примера.
Наконец, в mailserver.env включаем SSL (которая давно TLS):
SSL_TYPE=letsencrypt
И вот тут важный момент. Написано, что раз мы настроили прокси-протокол, то некая внутренняя почта будет отклоняться – как раз из-за отсутствия этого самого протокола. Так что скорее всего ранее упомянутая встроенная проверка обновлений теряет смысл:
ENABLE_UPDATE_CHECK=0
В общем-то можно просто подписаться на уведомления о новых релизах на Гитхабе. Будем надеяться, других палок в колеса прокси-протокол не вставит
, уж больно не хочется для него еще и альтернативные порты заводить. Или да, таки пользоваться certbot'ом! 
Похоже наконец-то можно снова запускать:
docker compose up -d
Для наглядности – вот что у меня получилось в Traefik.
DKIM, SPF и прочие DNS
После того, как я зачем-то потратил уйму времени на настройку Трафика, могу вернуться к настройке почты как таковой. Как вы помните, у меня есть только A-запись. К счастью, для добавления PTR-записи (она же rDNS, обратная зона и т.п.), мой хостинг-провайдер предоставляет раздел в панели управления, так что с этим проблема не возникла. Собственно, переходим к DKIM – своего рода электронной подписи писем.
Генерируем ключи и настройки Rspamd:
docker exec -it mailserver setup config dkim
В конце процесса будет выведена строка для добавления в качестве TXT-записи, начинается примерно так:
v=DKIM1; k=rsa; p=...
Добавляем (редактируем) DNS-запись, которая так и называется: mail._domainkey.example.com. («хвост» может и не потребоваться, в случае чего опять же выводится подсказка).
Следующей почему-то идет _dmarc.example.com., которая описывает, что делать с письмами, не прошедшими проверки. Предлагается воспользоваться генератором, но в целом давайте вновь позаимствуем минимальный пример у Владимира:
v=DMARC1; p=none; rua=mailto:dmarc@example.com
В этом случае с письмами ничего дополнительно не происходит, а вам приходит отчет. Соответственно такой ящик должен бы существовать (или как в начале настроен перехват). Справедливости ради, средства проверки, например тот же сервис от MxToolbox, будет рекомендовать прописать какое-нибудь действие (поместить в карантин или вообще отклонить), но с этим связан определенный риск, если вдруг где-то в настройках допущена ошибка.
SPF-запись (непосредственно на домене) позволяет проверить, что письмо отправлено с допустимого сервера. Рекомендуют начать с «мягких» настроек (и ими же, наверное, и закончить):
v=spf1 mx ~all
Т.е. идет проверка по mx-записям, а в случае чего письмо отправляется в спам. При «жесткой» настройке -all «подозрительное» письмо в принципе будет отклонено.
Наконец, MX-запись должна указывать на mail.example.com. (вроде как именно с точкой в конце).
Для проверки можно воспользоваться утилитой dig из пакета bind9-dnsutils:
apt install bind9-dnsutils
dig +short example.com MX
dig +short mail.example.com A
IP-адрес на обратную зону (PTR) можно проверить командой host.
И давайте наверное все-таки включим MTA-STS, вроде как из разряда «бест практикс». В mailserver.env меняем:
ENABLE_MTA_STS=1
И делаем вниз-вверх для перезапуска.
Шифрование почты
По умолчанию письма по сути хранятся в виде текстовых файлов. Само по себе это наверное не так уж и плохо, но вот если вдруг создавать tar.gz и куда-то еще его отправлять, то это уже не очень хорошо. Понятно, что если кто-то вдруг получит доступ к серверу, то он сможет и письма прочитать, но в случае шифрования для этого потребуются некоторые усилия.
Итак, сформируем ключ и сохраним его где-то вне каталога /srv (в моем случае), чтобы ненароком их тоже tar'ом не заархивировать. Например:
mkdir /root/certs
cd /root/certs
openssl ecparam -name prime256v1 -genkey | openssl pkey -out ecprivkey.pem
openssl pkey -in ecprivkey.pem -pubout -out ecpubkey.pem
Далее предлагается создать конфигурационный файл, и тут возникает несколько проблем. Первая – с размещением, если это делать в docker-data/dms/config, то получится как бы дублирование, так как вновь созданный файл нужно пробрасывать в /etc/dovecot/conf.d/ контейнера, а директория уже соответствует /tmp/docker-mailserver. Второй момент – оказалось, что в Docker Mailserver используется старый Dovecot 2.3, документацию к которому немного спрятали, поэтому изначально я был очень удивлен разным синтаксисом (начиная с 2.4 он другой).
В общем сделал файл docker-data/dovecot/conf.d/10-custom.conf:
# Enables mail_crypt for all services (imap, pop3, etc)
mail_plugins = $mail_plugins mail_crypt
plugin {
mail_crypt_global_private_key = </certs/ecprivkey.pem
mail_crypt_global_public_key = </certs/ecpubkey.pem
mail_crypt_save_version = 2
}
И добавил в compose.yaml:
services:
mailserver:
volumes:
- ./docker-data/dovecot/conf.d/10-custom.conf:/etc/dovecot/conf.d/10-custom.conf
- /root/certs/:/certs/
Забегая вперед, заработало.
Перенос писем
Лично мне хотелось бы забрать письма из «старых» почтовых ящиков, хотя объективно там вроде нет какой-то супер важной информации. Пока работает доступ по IMAP, для этого можно воспользоваться встроенным fetchmail (думал я) или отдельной утилитой imapsync.
Жаль только, что этот раздел изначально не особо актуальный, так как после отключения доступа по IMAP даже и непонятно, как можно будет письма перенести. Из локальной копии условного Thunderbird им же?
В веб-интерфейсе почты какого-то массового экспорта я не обнаружил (а сохранять письма по одному – ну такое себе). Или вдруг если вы с платного тарифа съезжаете… Но все же опишу, как было дело, возможно хотя бы в каком-то виде информация пригодится.
fetchmail – неудача
Вновь все остановил – docker compose down – и включил в mailserver.env:
ENABLE_FETCHMAIL=1
Создал docker-data/dms/config/fetchmail.cf:
poll 'imap.mail.ru' proto imap
user 'user1@example.com' pass 'password1' is 'user1@example.com' fetchall keep
user 'user2@example.com' pass 'password2' is 'user2@example.com' fetchall keep
Синтаксис довольно чудной и наверное даже близок к естественному языку. Как я говорил в начале, перенос идет из VK WorkMail, поэтому imap.mail.ru – это их реальный сервер. Возникает вопрос, откуда взять пароли. Это довольно утомительно, поскольку надо зайти в каждый ящик и создать пароль внешнего приложения. В меню аккаунта это «Пароль и безопасность» и далее соответствующий раздел. Несмотря на то, что и там, и там, именем пользователя является адрес почты, пришлось явно их сопоставлять (ключевое слово is).
И вот с сочетанием fetchall keep, как показала практика, возникла основная проблема. Слово keep требуется для сохранения исходных писем на всякий случай (хотя вскоре я удалю весь свой WorkMail). fetchall говорит о том, что нужно забирать все письма, иначе будут подтягиваться только непрочитанные, что для задачи первоначального импорта не подходит. Но (всегда есть «но»)! Почему-то нет контроля уже загруженных писем (наверное расчет идет на удаление исходных), поэтому при каждом запуске (который происходит в соответствии с FETCHMAIL_POLL) все начинается заново. Таким образом за ночь набралось где-то по 6 дублей!
Опять же, узнал я об этом позже, пока что поднял сервер, и вот тут как раз возникла проблема с обратным прокси:
postfix/postscreen[928]: warning: haproxy read: time limit exceeded
fetchmail[875]: SMTP connect to localhost failed
В целом, раз к этому моменту есть acme.json, можно или на время отключить Traefik, или просто бессовестным образом убрать настройки обратного прокси для postfix. Я так и сделал (убрал postfix-master.cf) – импорт пошел. Кстати импортированные таким образом сообщения отмечаются как новые, что вообще-то не совсем удобно, но что поделать (не импортировать
).
Еще одна проблема – rspamd (или даже postfix непосредственно) может отклонять старые письма, отправленные с уже не существующих доменов. Даже настройка RSPAMD_HFILTER=0 не помогла.
По итогу fetchmail мне не понравился – он решает другую задачу, а все импортированные письма удалил и решил начать заново с помощью…
imapsync
Установка выглядит примерно так:
wget https://imapsync.lamiral.info/dist2/imapsync.deb
apt --fix-broken install ./imapsync.deb
К счастью, для Debian автор делает пакет, поэтому компилировать из исходников не потребовалось.
И поехали – для каждого почтового ящика командуем:
imapsync --host1 imap.mail.ru --user1 user@example.com --password1 secret1 --host2 mail.example.com --user2 user@example.com --password2 secret2
Или погодите, сначала пара замечаний. Во-первых, наверное лучше написать скрипт, чтобы пароли не сохранялись в истории команд, либо указывать файлы с паролями (--passfile1, --passfile2), либо вообще не указывать, чтобы программа их запрашивала интерактивно. Во-вторых, возможно лучше сначала запустить импорт в тестовом режиме (с флагом --dry).
В принципе я так и сделал, сначала на одном из ящиков выполнил пробный запуск:
imapsync --dry --host1 imap.mail.ru --user1 user@example.com --host2 mail.example.com --user2 user@example.com
А потом потихоньку перенес все ящики интерактивно, очищая предварительно следы жалких потуг fetchmail'а. Вообще есть флаг --delete2, удаляющий сообщения с host2, отсутствующие на host1 (внимание, опасность!), но воспользоваться я им не рискнул – уже начали поступать новые письма.
Результат – совсем другое дело, хотя и не без нюансов в виде «лишних» папок из-за различий идентификаторов (задублировались черновики, спам и т.д.). Но уж с папками-то разобраться намного проще, чем с дублирующимися и отброшенными письмами.
Настройка почтового клиента
Я вдруг осознал, что веб-интерфейса к почте-то нет (хотя никто и не обещал)! В целом лично мне он не особо нужен, так как пользуюсь Thunderbird (от чего и был так сильно возмущен решением согнать всех в ихнее приложение). Надо подумать, стоит ли заморачиваться ради 0,1% случаев или установить клиент на ноутбук? Есть подозрение, что подружить Roundcube с сервером за обратным прокси (а еще и файловой структурой) будет непросто, а пока что воспользуемся почтовым клиентом.
В общем случае настройки таковы:
# сервер входящей почты
Протокол: IMAP
Хост: mail.example.com
Порт: 993
Имя пользователя: user@example.com
Защита соединения: SSL/TLS
Метод аутентификации: обычный пароль
# сервер исходящей почты
Хост: mail.example.com
Порт: 465
Имя пользователя: user@example.com
Защита соединения: SSL/TLS
Метод аутентификации: обычный пароль
Что касается Thunderbird, то я предлагаю сразу после ввода основных данных сразу переходить к ручной настройке:
Думаю, в других программах настройка ±аналогичная.
Вроде как можно настроить автоопределение конфигурации, но как будто бы для личных целей и/или небольшого количества пользователей это нецелесообразно.
Финальная проверка
Во-первых, отправил несколько писем сам себе. Гугл и Яндекс, как ни странно, приняли письма сразу во входящие, а вот mail.ru видимо на меня обиделся и сбил на подлете. То есть прямо Undelivered Mail Returned to Sender, код 550 spam message. Можно было бы заполнить некую форму жалобы по ссылке, чего я наверное пока делать не буду, а немного подожду и удалю VK WorkSpace (возможно есть связанные с этим проверки). Если что, тогда уже буду разбираться с реальными письмами.
Во-вторых, в уже неоднократно упомянутой статье предлагается проверить письмо с помощью mail-tester.com, что я и сделал.
Как и Владимир, получил высшую оценку с замечаниями по отсутствии HTML-версии (предпочитаю текстовые) и заголовка List-Unsubscribe, который нужен только в случае рассылок.
Отключение IPv6
Напоследок – еще одна, как ни странно, проблема. Docker, да и сама почта вроде как не очень дружат с IPv6. Дело осложняется еще и обратным прокси, который как бы висит на IPv4, и как минимум Dovecot доверяет именно «четвертой» подсети. Одновременно с этим у меня есть запись AAAA со звездочкой, поэтому хочешь не хочешь, а пришлось сделать AAAA-запись и для mail., иначе адрес разыменовывался бы вообще на другой сервер.
По поводу IPv6 есть целая страница документации. Казалось бы, среди всех предложенных вариантов проще всего прописать userland-proxy: false в /etc/docker/daemon.json, но нет – в результате все сломалось и Докер вообще перестал запускаться. Так что из простых способов остается или проброс портов только по IPv4, или прослушивание этих же портов только по IPv4 Traefik'ом. Решил подправить compose.yaml:
services:
traefik:
ports:
- "0.0.0.0:25:25" # SMTP (explicit TLS => STARTTLS, Authentication is DISABLED => use port 465/587 instead)
- "0.0.0.0:143:143" # IMAP4 (explicit TLS => STARTTLS)
- "0.0.0.0:465:465" # ESMTP (implicit TLS)
- "0.0.0.0:587:587" # ESMTP (explicit TLS => STARTTLS)
- "0.0.0.0:993:993" # IMAP4 (implicit TLS)
Поживем – увидим, будет ли от этого вред или польза.
Заключение
Оно какое-то не особо радостное. И так плохо, и этак нехорошо. Возможно стоило бы все же предпочесть mailcow или Mailu, или плюнуть и настраивать все непосредственно на хосте, пожертвовав некоей потенциальной переносимостью. К сожалению, вряд ли у меня появится возможность сравнить, так что теперь уж как настроил, так настроил.
К достоинствам Docker Mailserver наверное можно отнести относительную простоту этой самой настройки, если, конечно, не выделяться и пользоваться certbot'ом. Также разработчики вроде как позаботились о безопасности, поэтому новенький сервер не становится проходным двором. Ну и наверное стоит отметить «стильный, модный, молодежный» Rspamd.
Что не является стильным и далее по тексту, так это устаревший Dovecot 2.3.x. Думаю стоило бы использовать более актуальный, хотя тогда возникнет очень болезненный вопрос миграции. Еще один недостаток (но я не вполне уверен, что это косяк именно данного комбайна), это тупизм с псевдонимами при настройке перехвата почты для неизвестных адресов на домене.
Отсутствие веб-интерфейса к почте (справедливости ради, есть отдельные веб-интерфейсы для Rspamd, sieve и возможно чего-то еще) наверное все же из разряда 50/50. Как я писал выше, в 99,9% случаев я пользуюсь Thunderbird, поэтому мне лично веб-почта не особо-то и нужна. Если же для вас это критично, то наверное лучше рассмотреть другие варианты.
Категория: Программирование, веб | Опубликовано 17.06.2026
Похожие материалы
Перенос GitLab на другой сервер в Docker
Примерно год я мучался с GitLab на сервере с двумя гигабайтами оперативки. Когда оплаченный период закончился, решил взять более мощный VDS по формуле 4/4/30. До этого сам GitLab был установлен непосредственно из репозитория, но для экспериментов с Pages и т.д. нужен Docker. А раз он и так есть, почему бы не завернуть GitLab в контейнер? Заодно на сервер можно будет установить что-нибудь еще.







