HanyanOS 部署手记
1. Introduction
Email is one of the hardest services to self-host. Port 25 blocking by cloud providers, IP reputation management, SPF/DKIM/DMARC configuration, and the inherent complexity of the SMTP/IMAP stack make it a minefield. For HanyanOS, running a full mail stack on an N100 behind NAT (192.168.1.18) with a single public IPv4 via AWS Lightsail added unique constraints.
This post documents the complete mail pipeline: Stalwart Mail Server (v0.16.5) for MTA/IMAP, SnappyMail as the webmail frontend, AWS SES as the outbound relay, and FRP tunnels bridging the VPS edge to the internal N100.
2. Architecture Overview
1 | ┌─ Internet ─────────────────────────────────────┐ |
Key Design Decisions
| Decision | Rationale |
|---|---|
| Stalwart over Mailcow | Single binary, no Postfix/Dovecot complexity, RocksDB-backed config, modern Rust codebase |
| SES relay outbound | Lightsail blocks port 25 outbound — SES provides reputable SMTP relay with 62K free emails/month |
| FRP over Tailscale | Zero client configuration; mobile devices connect directly to mail.chenyun.org without VPN |
| SnappyMail over RainLoop | Lighter weight, actively maintained, simpler Docker deployment |
3. Docker Compose Deployment
The entire mail stack runs in two containers orchestrated by a single docker-compose.yml:
1 | # ~/mail-server/docker-compose.yml |
Notable details:
- Port 143 (IMAP plain) is exposed locally but blocked at UFW — only IMAPS (:993) reaches the internet
- Port 4190 (Sieve manage sieve) is LAN-only
- Ports 8080/8443 bound to
127.0.0.1exclusively — admin panel not exposed externally - SSL certificates from
/etc/ssl/chenyun/are bind-mounted read-only
4. FRP Tunnel Configuration
The VPS runs frps as the tunnel gateway; the N100 runs frpc to establish persistent encrypted connections. The mail-specific tunnel entries:
1 | # /etc/frp/frpc.ini (mail section) |
On the VPS side, frps listens on the corresponding remote ports and the Nginx stream layer routes public :25, :465, and :993 directly to the FRP ports.
5. Outbound Delivery via AWS SES
AWS Lightsail blocks outbound port 25 by default. Deploying a traditional MTA-to-MTA outbound path was impossible. The solution: route all outbound email through AWS SES via port 587 (STARTTLS).
Configuration in Stalwart’s admin panel:
1 | Outbound Relay: |
The SES sending limits (62,000 emails/month in the free tier) are more than sufficient for a personal mail server. SPF, DKIM, and DMARC records were added to the Route53 zone to maximize deliverability:
1 | TXT @ "v=spf1 include:amazonses.com ~all" |
6. Problems Encountered and Resolutions
6.1 AWS Port 25 Block (Critical)
Symptom: All outbound emails were silently dropped. No error in Stalwart logs, but recipients never received delivery.
Root Cause: Lightsail instances block outbound TCP 25 by default per AWS abuse policy.
Resolution: Opened an AWS support ticket requesting port 25 unblock, and simultaneously configured SES relay on port 587 as the primary outbound path. The SES route proved more reliable than direct MTA delivery and eliminated IP reputation concerns.
6.2 FRP Port Mapping Drift
Symptom: After multiple configuration iterations (docker-mailserver → Stalwart migration), the FRP port mappings diverged from the Nginx stream configuration on the VPS. Webmail worked, but IMAP connections timed out.
Resolution: Created a single source of truth — a fixed mapping table documented in HanyanOS/memory/infrastructure/network/README.md. Every FRP change must update the table before deployment.
6.3 Stalwart Route Name Collision
Symptom: After deleting an outbound route named “ses” and recreating it with the same name, Stalwart served stale cached configuration — the new route’s SMTP credentials were ignored.
Root Cause: Stalwart’s RocksDB-backed config cache retains route entries by name. Deleting and recreating with the same name does not invalidate the internal references.
Resolution: Created the new route as aws-ses instead of ses. After the name change, the configuration applied correctly.
6.4 Scroll Wheel Port Mutation
Symptom: During configuration, the submission port spontaneously changed from 587 to 586 in the admin UI.
Root Cause: Stalwart’s HTML5 admin interface uses number input fields. A mouse scroll hover over the port field while the browser window was unfocused caused an unintended increment.
Resolution: Always verify port values before saving. This is a UI ergonomics issue the Stalwart team should address.
6.5 Migration from docker-mailserver
Symptom: The previous mail stack (docker-mailserver + RainLoop) left stale Docker volumes, conflicting port bindings, and a defunct Docker network (email_default).
Resolution: Performed a complete teardown:
1 | docker compose -f ~/mail/docker-compose.yml down -v |
Then deployed the new stack from scratch. All old mail data was discarded (no migration needed — personal mail server with no critical historical data).
7. Security Posture
| Layer | Measure |
|---|---|
| Transport | TLS 1.3 on all public ports (25/465/587/993) |
| Authentication | SCRAM-SHA-256 for IMAP; LOGIN/PLAIN disabled |
| Firewall | UFW on N100: only 25/465/587/993 allowed from VPS subnet |
| Rate Limiting | Stalwart built-in: 50 connections/minute per IP |
| Anti-spam | Stalwart’s RBL + SPF/DKIM/DMARC verification |
| Admin isolation | Stalwart admin panel on 127.0.0.1:8443 only; no public route |
| Certificate | Let’s Encrypt via acme.sh; auto-renewed every 60 days |
8. Operational Notes
- Status monitoring: Mail health is tracked as part of the HanyanOS system status dashboard, with automatic restart via Docker’s
restart: unless-stoppedpolicy - Backup: The
./datadirectory (RocksDB store) is included in the nightly HanyanOS backup rotation - Logging: Stalwart logs to Docker’s journald; log rotation is handled by the host’s logrotate for
/var/lib/docker/containers/*/*.log - Performance: On N100, Stalwart consumes ~120MB RAM at idle, rising to ~250MB under load. CPU usage is negligible for personal email volumes (<100 messages/day)
9. Conclusion
Self-hosting email on an N100 behind NAT is entirely feasible with the right relay strategy. The combination of Stalwart (lightweight, Rust-based MTA/IMAP), AWS SES (reputable outbound relay), and FRP tunnels (NAT traversal) provides a reliable, low-maintenance mail infrastructure. The key lesson: never fight against cloud provider port blocking — use a relay service and redirect your engineering effort to monitoring and security hardening instead.
Next in series: WordPress + MySQL — Dockerized Blog Deployment on N100.