本文へスキップ

たけぞうBLOG

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

自宅メールサーバー構築記 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)

レコード内容
MX10 mail.takezou.com
Amail.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:…(ドメイン認証)
DMARCp=none から様子見
DDNSは Cloudflare API を叩くシェルスクリプト + systemd timer(5分間隔、IP変化時のみ更新)で実装

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 によるウェブメールログイン画面の保護あたりです。

この記事をシェア