Production Checklist¶
Go through this before declaring your Mailyte deployment production-ready — every item matters.
DNS¶
- A record for
mail.yourdomain.compoints to your server IP - MX record for
yourdomain.compoints tomail.yourdomain.com - SPF record set:
v=spf1 mx ~all(or stricter) - DKIM record published with correct public key
- DMARC record set:
v=DMARC1; p=quarantine; ... - PTR (reverse DNS) matches your mail hostname
- DNS propagation verified from external location
Bash
# Verify all DNS records
dig +short mail.yourdomain.com A
dig +short yourdomain.com MX
dig +short yourdomain.com TXT
dig +short mail._domainkey.yourdomain.com TXT
dig +short _dmarc.yourdomain.com TXT
dig -x YOUR_SERVER_IP +short
TLS / SSL¶
-
cert_managerissued real certificates (not the self-signed dev pair, not Traefik's default cert):ls -l storage/ssl_certs/ - Certificate covers
mail.yourdomain.com -
ACME_STAGING=false(the production override forces it) andCERT_SERVER_IPSis set - Postfix and Dovecot serve the issued certificate
- API served over HTTPS via Traefik (
https://api.yourdomain.com) - TLS 1.2+ only (TLS 1.0 and 1.1 disabled)
Bash
# Test TLS on SMTP
openssl s_client -connect mail.yourdomain.com:587 -starttls smtp
# Test TLS on IMAP
openssl s_client -connect mail.yourdomain.com:993
# Check certificate expiry
echo | openssl s_client -connect mail.yourdomain.com:993 2>/dev/null \
| openssl x509 -noout -dates
Firewall¶
- Ports 25, 587, 465 open (SMTP)
- Ports 143, 993 (and 110/995/4190 if used) open (IMAP/POP3/Sieve)
- Ports 80 and 443 open (Traefik: ACME + HTTPS)
- Stack started with
docker-compose.prod.yml— that is what binds every internal service (Grafana 3000, Prometheus 9090, Rspamd 11334, workers 8081-8104, Kafka 9092, Qdrant 6333, ...) to127.0.0.1 - Database port (3306) not published at all (the base file never publishes it)
- Redis port (6379) not published at all
Bash
sudo ufw status verbose
# The real check: what is actually listening on non-loopback addresses?
ss -tlnp | grep -v '127.0.0.1\|\[::1\]'
Warning: Docker publishes ports past ufw with its own iptables rules. The loopback bind addresses in
docker-compose.prod.ymlare the protection; a ufw deny on a Docker-published port does nothing.
Security¶
- All default passwords changed
-
.envfile permissions set to600 - Docker socket not exposed to untrusted containers
- fail2ban installed and configured for SMTP/IMAP
- SSH hardened (key-only auth, no root login)
- Automatic security updates enabled
- No unnecessary ports open
Bash
# Check .env permissions
ls -la .env
# Should show: -rw------- (600)
# Set if needed
chmod 600 .env
Services¶
- All containers running and healthy
Bash
docker compose -f docker-compose.yml -f docker-compose.prod.yml ps -a
# Every long-running service should show "Up (healthy)";
# secrets-check and migrate should show "Exited (0)"
-
secrets-checkandmigrateexited 0 (they are one-shot jobs) - Postfix accepting connections on port 25
- Dovecot accepting connections on port 993
- Rspamd responding on
127.0.0.1:11334(loopback-bound in production) - MySQL healthy, Redis responding to PING
- Both
apireplicas healthy - Traefik routing
api.yourdomain.com,docs.,grafana., the autoconfig/autodiscover hosts, webmail, and console
Bash
# Quick check (on the server)
curl -s http://127.0.0.1:11334/ > /dev/null && echo rspamd ok
docker compose -f docker-compose.yml -f docker-compose.prod.yml ps
# From anywhere
curl -s https://api.yourdomain.com/health | python3 -m json.tool
Email Delivery¶
- Test email sent and received successfully
- Test email doesn't land in spam
- SPF passes (check email headers)
- DKIM passes (check email headers)
- DMARC passes (check email headers)
- Inbound email delivery works
- Bounce handling works
Bash
# Check your mail setup with an external tool
# Visit: https://www.mail-tester.com
# Send an email to the address they give you
# Aim for a score of 9/10 or higher
Backups¶
- systemd backup timers installed and active:
systemctl list-timers 'mailyte-backup-*' -
secrets/dr.envconfigured (S3 destination + age encryption recipient) - Secrets escrowed off-server:
./scripts/escrow-secrets.sh(coversencryption_kek, mail_crypt keys,.env) - A full backup completed in the last 24h and uploaded offsite
- Backup restoration tested (at least once):
./scripts/restore.sh --latest --dry-run
Bash
# Take a full backup right now
./scripts/backup.sh --full
# List what exists, then rehearse a restore
./scripts/restore.sh --list
./scripts/restore.sh --latest --dry-run
Monitoring¶
- Prometheus scraping all targets
- Grafana accessible (
https://grafana.yourdomain.com) and dashboards loading - Alert rules configured
- Alert notifications tested (Slack, email, webhook)
-
monitoringservice healthy (it restarts failed containers via the docker-proxy)
Bash
# Check Prometheus targets
curl -s http://localhost:9090/api/v1/targets \
| python3 -c "
import json, sys
data = json.load(sys.stdin)
for t in data['data']['activeTargets']:
print(f\" [{t['health']}] {t['labels']['job']}\")
"
# Test alerting
curl -X POST http://localhost:9093/api/v2/alerts \
-H "Content-Type: application/json" \
-d '[{"labels":{"alertname":"TestAlert","severity":"info"},"annotations":{"summary":"Checklist test"}}]'
Performance¶
- API response time < 500ms (p95)
- Mail queue size < 100 under normal load
- Disk usage < 70%
- Memory usage < 80%
- CPU usage < 70% under normal load
Bash
# Quick performance check
curl -w "API response: %{time_total}s\n" -o /dev/null -s https://api.yourdomain.com/health
docker compose -f docker-compose.yml -f docker-compose.prod.yml exec -T postfix postqueue -p | tail -1
df -h / | tail -1
free -h | head -2
Documentation¶
- Admin credentials stored securely (password manager)
- Recovery procedures documented and accessible
- On-call rotation set up (if applicable)
- Runbook for common incidents written
Final Sign-Off¶
Once everything passes:
Bash
# Take a full backup before declaring production
./scripts/backup.sh --full
# Run the full health check one more time
curl -s https://api.yourdomain.com/health | python3 -m json.tool
Tip: Bookmark this page. Run through it again after every major update or infrastructure change.