Proxmox LXC + docker-mailserverで自宅メールサーバー構築 〜Roundcubeウェブメール化とCloudflare Tunnel公開まで〜

自宅回線(フレッツ・op25B)+独自ドメイン takezou.com で、本格的なメールサーバーを稼働させることにしました。Proxmox VE の LXC コンテナに docker-mailserver を載せ、Roundcube で Gmail 風のウェブメール化し、さらに Cloudflare Tunnel でインターネット公開まで一気通貫で行ったので、設計とハマりどころを記録として残します。
目次
全体構成
- 仮想化: Proxmox VE クラスタ上の LXC (Debian 13 / 2 vCPU / 3GB RAM / 30GB / unprivileged / nesting+keyctl)
- MTA/IMAP: docker-mailserver (Postfix + Dovecot + Rspamd ではなく amavis + SpamAssassin 構成)
- 送信: Brevo (ex-Sendinblue) の SMTP リレー経由(op25B 回線のため直接配送不可)
- DNS: Cloudflare(MX / SPF / DKIM / DMARC) + DDNS スクリプト(5分間隔の systemd timer)
- TLS: Let’s Encrypt 証明書を DNS-01 チャレンジで取得
- 受信: ルーターで 25/143/465/587/993/4190 を LXC へ転送 + fail2ban
- ウェブメール: Roundcube(LE証明書を焼いたカスタムイメージ、HTTPS)
- 外部公開: Cloudflare Tunnel(
webmail.example.com)
ポイントは「メール系ポートはルーター転送、ウェブメールはトンネル」という住み分けです。SMTP/IMAP は Cloudflare がproxyできないため通常のポート転送が必須ですが、ウェブメール側はトンネルに乗せることでポート開放ゼロで HTTPS 公開できます。
1. LXC コンテナの作成
docker を動かすので nesting=1 と keyctl=1 が必須です(unprivileged の場合)。設定ファイルはこうなっています。
arch: amd64
cores: 2
memory: 3072
features: nesting=1,keyctl=1
net0: name=eth0,bridge=vmbr0,ip=192.168.2.25/24,gw=192.168.2.1,type=veth
onboot: 1
ostype: debian
rootfs: local-lvm:vm-101-disk-0,size=30G
swap: 1024
tags: mail
timezone: Asia/Tokyo
unprivileged: 1
2. docker-mailserver の導入
MTA 周りはゼロから Postfix/Dovecot を固めるのではなく、評判の良い docker-mailserver (DMS) にしました。compose はシンプルです。
services:
mailserver:
image: ghcr.io/docker-mailserver/docker-mailserver:latest
container_name: mailserver
hostname: mail.takezou.com
restart: always
stop_grace_period: 1m
ports:
- "25:25" # SMTP
- "143:143" # IMAP
- "465:465" # SMTPS
- "587:587" # Submission (STARTTLS)
- "993:993" # IMAPS
- "4190:4190" # ManageSieve
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/letsencrypt:/etc/letsencrypt:ro
env_file: mailserver.env
cap_add:
- NET_ADMIN # fail2ban用
mailserver.env の主要項目は以下。最初 ENABLE_SPAMASSASSIN=0 のまま運用してしまいましたが、後述の GTUBE テスト後に 1 へ変更しています。
ENABLE_SPAMASSASSIN=1
ENABLE_SIEVE=1
ENABLE_FAIL2BAN=1
ENABLE_CLAMAV=0 # RAM +1GB のため様子見
SSL_TYPE=letsencrypt
# Brevoリレー(RELAY_* envは後述のバグがあるので注意)
ハマりどころ: RELAY_* だけだと relayhost が空になる
DMS latest には RELAY_HOST / RELAY_PORT 等を env で設定しても postconf relayhost が空のままになる挙動がありました。確実なのは永続 overlay で直接書く方法です。
# docker-data/dms/config/postfix-main.cf
relayhost = [smtp-relay.brevo.com]:587
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = static::
smtp_sasl_security_options = noanonymous
smtp_tls_security_level = encrypt
3. DNS 設定(Cloudflare)
| レコード | 内容 |
|---|---|
| MX | 10 mail.takezou.com |
| A | mail.takezou.com → 自宅WAN IP(DDNSで自動更新) |
| TXT (SPF) | v=spf1 mx include:spf.sendinblue.com include:_spf.google.com ~all |
| TXT (DKIM) | mail._domainkey に2048bit公開鍵(DMSで生成) |
| TXT (Brevo) | brevo-code:…(ドメイン認証) |
| DMARC | p=none から様子見 |
4. TLS証明書は DNS-01 で
自宅サーバーは 80 番ポートの開放を避けたいので DNS-01 チャレンジ一択です。certbot + python3-certbot-dns-cloudflare を LXC ホスト側(コンテナ内)に入れ、Cloudflare API トークン(Zone:DNS:Editのみ)で発行しました。
certbot certonly --dns-cloudflare \
--dns-cloudflare-credentials /root/.secrets/cloudflare.ini \
-d mail.takezou.com
更新時は /etc/letsencrypt/renewal-hooks/deploy/restart-mail.sh でメールコンテナを再起動し、Dovecot/Postfix に新しい証明書を読み込ませます。証明書は compose で /etc/letsencrypt:ro マウントして共有。
5. 送信経路: Brevo リレー
フレッツの op25B 回線ではアウトバウンドの TCP/25 が塞がれているため、自ドメインから直接配送できません。Brevo の SMTP リレー(587)を relayhost に設定して解決。DKIM 署名(s=mail, d=takezou.com)は DMS 内の OpenDKIM が付与し、実際に status=sent まで確認できました。
注意点として、Brevo 側の「アカウント本体のメール確認」「送信者の確認」「SMTPキーのIP制限解除」の3点が済んでいないと、DATA コマンド後に無応答でタイムアウトするという分かりにくい症状に悩まされます。
6. 受信: ポート転送と実送信テスト
ルーター(GL.iNet)で 25/143/465/587/993/4190 を LXC(192.168.2.25)へ転送し、Gmail から実メールを送って DKIM verification successful / DMARC pass / INBOX 配信まで確認しました。SPF が softfail になるのは、ポート転送の NAT で Gmail 送信サーバーの IP が消失するためで、自構成では想定内です。
7. Roundcube でウェブメール化
Roundcube の公式 Docker イメージは HTTP しか listen しないため、SSL vhost を焼いたカスタムイメージを作りました。
FROM roundcube/roundcubemail:latest
RUN a2enmod ssl headers && \
echo "ServerName mail.takezou.com" > /etc/apache2/conf-available/servername.conf \
&& a2enconf servername
COPY webmail-ssl.conf /etc/apache2/sites-enabled/000-default.conf
# webmail-ssl.conf(抜粋)
<VirtualHost *:443>
DocumentRoot /var/www/html/public_html
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/mail.takezou.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/mail.takezou.com/privkey.pem
</VirtualHost>
DB は SQLite(単人数なら十分)、IMAP/SMTP は Docker ネットワーク内で ssl://mailserver:993 / tls://mailserver:587 を指定し、smtp_user=%u でIMAPと同じパスワードを使って認証 → Brevo リレーへ、という流れです。
ハマりどころ3連発
- カスタム config.inc.php には
$config['plugins'] = [];が必須。エントリポイント生成の config.docker.inc.php がこれを前提としているため、定義なしでマウントすると initdb が fatal で起動しません。 - DocumentRoot は
/var/www/html/public_html。本家 vhost をそのまま真似ると 404 になります。 - HTTP→HTTPS リダイレクトのポートずれ。
%{HTTP_HOST}にはリクエストのポート(8080)が混ざるため、RewriteCond でホスト部だけ取り出して 8443 を付け直す必要があります。
8. Cloudflare Tunnel でポート開放ゼロ公開
ウェブメールは webmail.example.com として公開しています(実URLはセキュリティ上の理由により本記事では伏せています)。仕組みは単純で、CT 内に cloudflared コンテナを起動し、Cloudflare エッジへのアウトバウンド接続を張るだけ。ルーターの設定変更は不要です。
# config.yml
tunnel: <TUNNEL-ID>
credentials-file: /etc/cloudflared/tunnel.json
ingress:
- hostname: webmail.example.com
service: https://webmail:443
originRequest:
noTLSVerify: true # オリジン証明書はmail.takezou.com名のため
- service: http_status:404
ハマりどころ
- 公式 cloudflared イメージは非root(UID 65532)で動く。
tunnel.jsonは 600 のままchown 65532しないと permission denied で再起動ループします。 cloudflared tunnel run <名前>は cert.pem を参照するため、資格情報だけをコンテナに渡している構成では失敗します。config.yml に Tunnel UUID を書いてrunは引数なしで。- mail.takezou.com をトンネル化してはいけない。このホスト名の A レコードは MX・SMTP・IMAP クライアントの接続先そのものです。トンネル用 CNAME に置き換えるとメール全体が停止します。ウェブメールは webmail サブドメインで分離するのが正解。
9. スパムフィルター: 有効化と GTUBE テスト
DMS はデフォルトで SpamAssassin が無効(ENABLE_SPAMASSASSIN=0)でした。有効化後、SpamAssassin の標準テストパターン GTUBE を port 25 経由で送り、Hits: 1003.71 で判定 → タグ付け+検疫+Junk フォルダ振り分けまで確認しています。なぜならローカルの sendmail パスは content_filter 対象外(信頼済み)なので、フィルタのテストは必ず外部 SMTP 経由で行う必要があるからです。
10. 運用とバックアップ
- 週次 vzdump(snapshot モード + zstd)を Proxmox のバックアップジョブで取得(keep-last=2)
- 手動バックアップ時は sha256 記録 + zstd ストリームテスト + tar 展開テストを実施(過去にパイプ直受けで破損した教訓から)
- fail2ban による認証総当たりの遮断は最初から有効
- メモリ使用感: CT 全体で約 500MB / 3GB(SpamAssassin 有効後)
まとめ
docker-mailserver + Roundcube + Cloudflare Tunnel の組み合わせで、「ポート開放を最小限にしつつ、独自ドメインの送受信と Gmail 風ウェブメールを実現する」構成が 3GB RAM の LXC 1 個に収まりました。次の課題は ClamAV(ウイルススキャン、RAM +1GB)の要否と、Cloudflare Access によるウェブメールログイン画面の保護あたりです。

