Objectif du guide
Ce guide décrit l’installation complète d’un serveur email professionnel indépendant, basé sur un VPS Hetzner et Mailcow. Il couvre l’ensemble du parcours : création du VPS, déblocage SMTP, DNS, PTR, installation Docker, configuration Mailcow, tests Gmail, sécurité anti-blacklist, monitoring et sauvegardes.
Principe fondamental : un serveur mail ne se gagne pas seulement avec Postfix. Il se gagne avec un alignement propre entre IP, PTR, hostname, DNS, SPF, DKIM, DMARC, TLS, réputation IP, restrictions d’envoi et monitoring.
Stack retenue
- Provider : Hetzner Cloud
- OS : Ubuntu 24.04 LTS
- Mail stack : Mailcow Dockerized
- Webmail : SOGo
- Anti-spam : Rspamd
- SMTP/IMAP : Postfix + Dovecot
Résultat attendu
- Réception Gmail vers
guillaume@ideo-lab.com - Envoi Mailcow vers Gmail
- SPF/DKIM/DMARC validés
- PTR IPv4 propre
- Rate limit SMTP actif
- Fail2Ban / netfilter renforcé
Points critiques
- Port 25 sortant ouvert par Hetzner
- MX exclusif vers Mailcow
- Suppression des anciens MX Zoho
- Suppression des anciens SPF contradictoires
- Désactivation IPv6 mail tant que le PTR IPv6 n’est pas parfait
Pré-requis
| Élément | Valeur utilisée | Rôle |
|---|---|---|
| Domaine | ideo-lab.com | Domaine mail principal |
| FQDN mail | mail.ideo-lab.com | Nom officiel du serveur mail |
| IPv4 serveur | 159.69.80.91 | IP publique Hetzner |
| Provider DNS | GoDaddy / zone DNS actuelle | Gestion A, MX, TXT |
| Provider VPS | Hetzner Cloud | Serveur mail |
Stopper immédiatement si :
- le port 25 sortant n’est pas ouvert ;
- le PTR IPv4 n’est pas configurable ;
- le domaine ne peut pas être modifié côté DNS ;
- un autre provider mail garde les MX actifs.
Ports nécessaires
| Port | Usage | Statut |
|---|---|---|
| 25 | SMTP entrant/sortant serveur à serveur | Obligatoire |
| 80 | HTTP ACME / Let’s Encrypt | Obligatoire |
| 443 | HTTPS Mailcow / SOGo | Obligatoire |
| 465 | SMTPS client | Recommandé |
| 587 | SMTP submission client STARTTLS | Recommandé |
| 993 | IMAPS client | Recommandé |
Création et préparation Hetzner
Créer le VPS
- Projet : Default ou projet dédié Mail
- Location : Nuremberg
- Image : Ubuntu 24.04
- Type : CPX / CX selon budget
- Nom serveur conseillé :
mail-ideo
Demander ouverture du port 25
Hello,
Could you please unblock outbound SMTP port 25 for my server?
Server IP: 159.69.80.91
This server is used for a legitimate private mail server.
SPF, DKIM, DMARC, PTR and abuse controls will be configured.
Thank you.Configurer le PTR IPv4
Dans Hetzner Console :
Servers
-> server
-> Networking
-> Public Network
-> IPv4
-> Edit Reverse DNSPTR attendu →159.69.80.91 -> mail.ideo-lab.com
Tester le port 25
telnet gmail-smtp-in.l.google.com 25Attendu →220 gmail-smtp-in.l.google.com
DNS complets
Records obligatoires
| Type | Nom | Valeur | TTL |
|---|---|---|---|
| A | mail | 159.69.80.91 | 600 |
| MX | @ | mail.ideo-lab.com priorité 10 | 600 |
| TXT | @ | v=spf1 mx ip4:159.69.80.91 ~all | 600 |
| TXT | _dmarc | v=DMARC1; p=none; | 600 |
| TXT | dkim._domainkey | v=DKIM1;k=rsa;... | 600 |
À supprimer lors de la migration :
- anciens MX Zoho :
mx.zoho.eu,mx2.zoho.eu,mx3.zoho.eu - anciens SPF contradictoires avec Zoho ou Mailersend
- ancien DKIM Zoho si plus utilisé :
zmail._domainkey
Vérifications DNS
nslookup -type=mx ideo-lab.com
dig mail.ideo-lab.com
dig -x 159.69.80.91
dig TXT ideo-lab.com
dig TXT _dmarc.ideo-lab.com
dig TXT dkim._domainkey.ideo-lab.comDMARC : pendant la migration, garder
p=none. Après stabilisation, passer progressivement à p=quarantine, puis éventuellement p=reject.Installation Linux / Docker
Hostname système
hostnamectl set-hostname mail.ideo-lab.com
hostnameAttendu →mail.ideo-lab.com
Packages système
apt update
apt install -y git curl jq dnsutils telnet nano mailutils docker.io docker-composeInstaller Docker Compose v2 si nécessaire
mkdir -p /usr/local/lib/docker/cli-plugins
curl -SL https://github.com/docker/compose/releases/download/v2.29.7/docker-compose-linux-x86_64 -o /usr/local/lib/docker/cli-plugins/docker-compose
chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
docker compose versionAttendu →Docker Compose version v2.x
Installation Mailcow
Cloner Mailcow
cd /opt
git clone https://github.com/mailcow/mailcow-dockerized
cd /opt/mailcow-dockerizedGénérer la configuration
./generate_config.sh| Question | Réponse |
|---|---|
| FQDN | mail.ideo-lab.com |
| Timezone | Europe/Madrid |
| Branch | 1 / stable |
| IPv6 Docker | Y si demandé, puis désactiver dans mailcow.conf ensuite si PTR IPv6 absent |
Désactiver IPv6 mail si PTR IPv6 absent
nano /opt/mailcow-dockerized/mailcow.confENABLE_IPV6=nDémarrer Mailcow
docker compose pull
docker compose up -d
docker psAccès interface
| Interface | URL | Login |
|---|---|---|
| Admin Mailcow | https://mail.ideo-lab.com/admin | admin |
| Webmail SOGo | https://mail.ideo-lab.com/SOGo | guillaume@ideo-lab.com |
Tests de validation
Test réception Gmail vers Mailcow
Envoyer depuis Gmail vers :
guillaume@ideo-lab.comObserver les logs Postfix
cd /opt/mailcow-dockerized
docker compose logs -f postfix-mailcowSuccès attendu →status=sent (250 2.0.0)
Test envoi Mailcow vers Gmail
- Configurer Thunderbird si SOGo pose problème côté navigateur ancien.
- IMAP :
mail.ideo-lab.com, port993, SSL/TLS. - SMTP :
mail.ideo-lab.com, port587, STARTTLS. - Utilisateur :
guillaume@ideo-lab.com.
Lire les retours Gmail
| Erreur Gmail | Cause | Correction |
|---|---|---|
550-5.7.26 DMARC | SPF/DKIM/DMARC incohérents | Corriger SPF, DKIM, DMARC p=none |
550-5.7.25 PTR | IPv6 sans reverse DNS | Désactiver IPv6 ou configurer PTR IPv6 |
Unauthenticated email | DKIM absent ou SPF non propagé | Ajouter dkim._domainkey Mailcow |
Sécurité anti-blacklist
Vérifier open relay
cd /opt/mailcow-dockerized
docker compose exec postfix-mailcow postconf smtpd_recipient_restrictionsContrôle critique →reject_unauth_destination
Fail2Ban / Netfilter Mailcow
| Paramètre | Valeur recommandée |
|---|---|
| Durée du bannissement | 86400 |
| Durée max. du bannissement | 604800 |
| Nb max. de tentatives | 3 |
| Fenêtre de nouvel essai | 900 |
| IPv4 subnet ban | /32 |
| IPv6 subnet ban | /128 |
Rate limit mailbox
Dans la boîte guillaume@ideo-lab.com, onglet Limite d’envoi :
Valeur →30 msgs / minute
2FA admin Mailcow
Activer OTP pour l’utilisateur admin :
Système
-> Accès
-> Authentification à deux facteurs
-> OTPConseil : utiliser Aegis Authenticator plutôt qu’une application OTP inconnue pleine de publicités. Sauvegarder la clé secrète OTP ou les codes de secours dans un emplacement sécurisé.
Monitoring blacklist et alertes serveur
Script blacklist
mkdir -p /opt/mail-monitoring
nano /opt/mail-monitoring/check_blacklist.sh#!/bin/bash
IP="159.69.80.91"
EMAIL="guillaume@ideo-lab.com"
REVERSED_IP=$(echo $IP | awk -F. '{print $4"."$3"."$2"."$1}')
BLACKLISTS=(
"zen.spamhaus.org"
"bl.spamcop.net"
"b.barracudacentral.org"
"dnsbl-1.uceprotect.net"
)
ALERT=""
for BL in "${BLACKLISTS[@]}"
do
RESULT=$(dig +short ${REVERSED_IP}.${BL})
if [ ! -z "$RESULT" ]; then
ALERT="${ALERT}\nBLACKLIST DETECTED : $BL ($RESULT)"
fi
done
if [ ! -z "$ALERT" ]; then
echo -e "$ALERT" | mail -s "MAIL SERVER BLACKLIST ALERT" $EMAIL
fichmod +x /opt/mail-monitoring/check_blacklist.sh
/opt/mail-monitoring/check_blacklist.shScript alertes serveur
nano /opt/mail-monitoring/server_alerts.sh#!/bin/bash
EMAIL="guillaume@ideo-lab.com"
ALERT=""
DISK=$(df / | tail -1 | awk '{print $5}' | sed 's/%//')
if [ "$DISK" -gt 85 ]; then
ALERT="${ALERT}\n[DISK] Usage critique : ${DISK}%"
fi
RAM=$(free | grep Mem | awk '{printf("%.0f"), $3/$2 * 100.0}')
if [ "$RAM" -gt 90 ]; then
ALERT="${ALERT}\n[RAM] Usage critique : ${RAM}%"
fi
CONTAINERS=(
"mailcowdockerized-postfix-mailcow-1"
"mailcowdockerized-dovecot-mailcow-1"
"mailcowdockerized-nginx-mailcow-1"
)
for C in "${CONTAINERS[@]}"
do
STATUS=$(docker inspect -f '' $C 2>/dev/null)
if [ "$STATUS" != "true" ]; then
ALERT="${ALERT}\n[CONTAINER DOWN] $C"
fi
done
QUEUE=$(docker exec mailcowdockerized-postfix-mailcow-1 postqueue -p | tail -1 | awk '{print $5}')
if [[ "$QUEUE" =~ ^[0-9]+$ ]]; then
if [ "$QUEUE" -gt 50 ]; then
ALERT="${ALERT}\n[SMTP QUEUE] Queue importante : $QUEUE messages"
fi
fi
if [ ! -z "$ALERT" ]; then
echo -e "$ALERT" | mail -s "MAIL SERVER ALERT" $EMAIL
fiCron monitoring
chmod +x /opt/mail-monitoring/server_alerts.sh
crontab -e0 6 * * * /opt/mail-monitoring/check_blacklist.sh
*/15 * * * * /opt/mail-monitoring/server_alerts.shBackup Mailcow
Volumes Docker Mailcow
docker volume ls | grep mailcowVolumes essentiels :
mailcowdockerized_vmail-vol-1: emailsmailcowdockerized_mysql-vol-1: configuration, domaines, comptesmailcowdockerized_redis-vol-1: cache / sessionsmailcowdockerized_crypt-vol-1: données cryptographiques
Script backup
mkdir -p /backup/mailcow
nano /backup/mailcow/backup_mailcow.sh#!/bin/bash
DATE=$(date +%Y-%m-%d_%H-%M-%S)
BACKUP_DIR="/backup/mailcow/$DATE"
mkdir -p "$BACKUP_DIR"
cd /opt/mailcow-dockerized || exit 1
docker compose stop postfix-mailcow dovecot-mailcow
docker run --rm -v mailcowdockerized_vmail-vol-1:/volume -v "$BACKUP_DIR":/backup alpine tar czf /backup/vmail.tar.gz -C /volume .
docker run --rm -v mailcowdockerized_mysql-vol-1:/volume -v "$BACKUP_DIR":/backup alpine tar czf /backup/mysql.tar.gz -C /volume .
docker run --rm -v mailcowdockerized_redis-vol-1:/volume -v "$BACKUP_DIR":/backup alpine tar czf /backup/redis.tar.gz -C /volume .
docker run --rm -v mailcowdockerized_crypt-vol-1:/volume -v "$BACKUP_DIR":/backup alpine tar czf /backup/crypt.tar.gz -C /volume .
cp mailcow.conf "$BACKUP_DIR/"
cp docker-compose.yml "$BACKUP_DIR/"
docker compose start postfix-mailcow dovecot-mailcow
find /backup/mailcow/* -maxdepth 0 -type d -mtime +7 -exec rm -rf {} \;
echo "Backup terminé : $BACKUP_DIR"chmod +x /backup/mailcow/backup_mailcow.sh
/backup/mailcow/backup_mailcow.sh
ls -lh /backup/mailcow/YYYY-MM-DD_HH-MM-SSAttendu →vmail.tar.gz, mysql.tar.gz, redis.tar.gz, crypt.tar.gz
Limite : un backup local sur le même serveur ne protège pas contre la perte du VPS. Une copie externe doit être ajoutée ensuite : Hetzner Storage Box, S3, rsync vers autre VPS, ou backup chiffré distant.
Exploitation quotidienne
Commandes utiles
| Action | Commande |
|---|---|
| Voir containers | docker ps |
| Voir logs Postfix | docker compose logs -f postfix-mailcow |
| Voir logs Dovecot | docker compose logs -f dovecot-mailcow |
| Redémarrer Mailcow | docker compose down && docker compose up -d |
| Mettre à jour images | docker compose pull && docker compose up -d |
| Voir queue SMTP | docker exec mailcowdockerized-postfix-mailcow-1 postqueue -p |
Warm-up IP
Règle : ne pas envoyer de volume massif au début. Pendant 2 à 3 semaines, rester sur des emails humains, faibles volumes, conversations réelles, réponses naturelles.
| Période | Volume conseillé | Objectif |
|---|---|---|
| Semaine 1 | 5 à 20 emails/jour | Créer une réputation initiale propre |
| Semaine 2 | 20 à 50 emails/jour | Stabiliser Gmail/Outlook |
| Semaine 3+ | Progressif | Ne jamais faire de bulk marketing sans stratégie dédiée |
Dépannage
Gmail refuse avec DMARC
550-5.7.26 Unauthenticated email from domain is not accepted due to DMARC policy- Vérifier SPF :
v=spf1 mx ip4:159.69.80.91 ~all - Vérifier DKIM :
dkim._domainkey - Mettre DMARC temporairement en
p=none - Supprimer les anciens SPF Zoho/Mailersend
Gmail refuse avec PTR IPv6
550-5.7.25 IP address sending this message does not have a PTR record setup- Identifier l’IP mentionnée par Gmail.
- Si c’est IPv6, désactiver
ENABLE_IPV6=ndans Mailcow. - Redémarrer Mailcow.
Mail reçu dans les logs mais pas visible
- Recharger SOGo avec CTRL+F5.
- Vérifier Pourriel / Archive.
- Vérifier logs Dovecot.
SOGo affiche Unauthorized
cd /opt/mailcow-dockerized
docker compose restart sogo-mailcow nginx-mailcowDocker Compose ancien
docker-compose --version
docker compose versionMailcow moderne nécessite Docker Compose v2. Installer le plugin CLI si nécessaire.
Erreur backup : Cannot stat data/vmail
La version courante de Mailcow stocke les emails dans un volume Docker, pas dans data/vmail.
docker volume ls | grep mailcow