VPSでMailpitの使い捨てメール受信箱を構築
MailpitをVPSに構築し、テストメールをWeb UIで確認します。捨てドメインですべて受信し、ステージング環境から実際の顧客へ誤送信する事故を防げます。
使い捨てメール受信ボックスとは
使い捨てメール受信ボックスは、すべてのアドレス宛てのメールを受け付け、どこにも配信しない小規模な SMTP (simple mail transfer protocol) サーバーです。ステージング環境のアプリケーションは実際のメールプロバイダーではなく、このサーバーへ送信します。すべてのメッセージはそこで止まります。受信した内容は Web インターフェイスで確認できます。宛先リストの誤りやテンプレートの不具合があっても、メールが外部へ送信されないため、実害はありません。
このガイドでは、Docker Compose を使用して、単一の VPS 上に 1 台構成します。Mailpit が全宛先を受け付ける受け皿になります。SMTP リスナーはアプリケーションだけが接続できる場所にバインドします。Web インターフェイスは nginx の背後に配置し、transport layer security (TLS) とパスワードで保護します。保持数の上限を設定し、メールボックスによるディスク容量の圧迫を防ぎます。Compose が初めての場合は、VPS 向け Compose の基本で、このガイドが前提とするファイル構成を説明しています。
これはテストツールであり、メールサーバーではありません。アカウント、配信機能、スパムフィルタリングはありません。実際の利用者向けのメールボックスを用意するには、Mailcow などの完全なメールサーバーが必要であり、作業量も大幅に増えます。
Mailpit、Inbucket、MailHog の比較: 実行するメールシンクの選択
この用途に使えるツールは 3 つあります。違いは、メンテナンス状況、待ち受けるポート、受信後のメッセージを処理する機能です。以下のバージョンは 2026 年 10 月に確認したものです。
MailHog (mailhog/mailhog) は SMTP 用に 1025、インターフェース用に 8025 で待ち受けます。現在も動作します。ただし、デフォルトブランチには 2022 年 8 月以降コミットがなく、issue tracker には 200 件を超える未解決 issue があります。そのため、テスト経路で更新されていない依存関係を実行することになります。新規作業で採用しないでください。
Inbucket (inbucket/inbucket) は SMTP 用に 2500、Web インターフェース用に 9000、POP3 (post office protocol version 3) 用に 1100 で待ち受けます。Version 3.2.0 は 2026 年 10 月 10 日にリリースされました。メッセージを /storage 配下にファイルとして保存し、自動的に削除します。イメージでは INBUCKET_STORAGE_RETENTIONPERIOD=72h と INBUCKET_STORAGE_MAILBOXMSGCAP=300 が設定されています。HTTP リクエストではなく POP3 クライアントライブラリでメールを収集する必要があるテストでは、これを選択してください。
Mailpit (axllent/mailpit) は MailHog と同じ 1025 と 8025 を使用するため、アプリケーションの設定を変更せずに MailHog から置き換えられます。Version 1.31.5 は 2026 年 10 月 11 日にリリースされました。1.30 系列は 8 月の 1.30.7 で終了しています。このガイドで必要な機能はバイナリに含まれています。Web インターフェースと API (application programming interface) 用のパスワードファイル、メッセージ数の上限、メッセージの保持期間の上限、受信者フィルターを利用できます。このガイドでは Mailpit を実行します。
キャッチオールの仕組みと、DNS が関与しない理由
ここでアプリケーションは配信先を検索しません。ホストとポートを指定すると TCP 接続を開き、RCPT TO:<anyone@example.test> を通知します。Mailpit はその内容にかかわらず受信者を受け入れ、メッセージを保存します。外部への転送は行いません。ドメイン名は解決されないため、.test がドメインネームシステム(DNS)上のどこにも存在しない予約名であっても、example.test は機能します。
仕組みはこれだけです。これにより、受信トレイはデフォルトで安全になります。MX(mail exchanger)レコードは使用されず、配信も試行されません。メッセージが実在する人物に届くこともありません。
ステージングアプリケーションの送信先をシンクに設定する
アプリケーションを同じ Compose プロジェクト内のコンテナとして実行する場合は SMTP ホストを mailpit に設定し、ホスト上で実行する場合は 127.0.0.1 に設定します。ポートは 1025 に設定し、TLS を無効にして、ユーザー名とパスワードは空のままにします。Mailpit は匿名メールを受け付けます。
認証情報がないと送信しないフレームワークもあります。MP_SMTP_AUTH_ACCEPT_ANY=1 を設定すると Mailpit は任意のユーザー名とパスワードを受け付け、MP_SMTP_AUTH_ALLOW_INSECURE=1 を設定すると暗号化されていない接続で PLAIN と LOGIN メカニズムを使用できます。これら2つの設定が安全なのは、リスナーにインターネットから到達できない場合だけです。以下の構成で、その状態を確保します。
MP_SMTP_ALLOWED_RECIPIENTS は初日から設定する価値があります。これは正規表現を受け取り、正規表現に一致しないすべての受信者を拒否します。テスト用ドメインを指定しておけば、実際の顧客アドレスが残ったステージングデータベースからの送信は、シンクにひそかに到達する代わりに、アプリケーションログで確認できる明確な失敗になります。
Docker Compose ファイル
最初にディレクトリと Web インターフェース用のパスワードファイルを作成します。htpasswd -B は bcrypt ハッシュを書き込みます。Mailpit は bcrypt と平文の両方を読み取れます。
mkdir -p ~/mailpit/data
cd ~/mailpit
sudo apt update && sudo apt install -y apache2-utils
htpasswd -B -c data/ui-auth qacompose.yaml を記述します。
services:
mailpit:
image: axllent/mailpit:v1.31
container_name: mailpit
restart: unless-stopped
ports:
- "127.0.0.1:8025:8025"
- "127.0.0.1:1025:1025"
volumes:
- ./data:/data
environment:
MP_DATABASE: /data/mailpit.db
MP_MAX_MESSAGES: 2000
MP_MAX_AGE: 14d
MP_UI_AUTH_FILE: /data/ui-auth
MP_SMTP_AUTH_ACCEPT_ANY: 1
MP_SMTP_AUTH_ALLOW_INSECURE: 1
MP_SMTP_ALLOWED_RECIPIENTS: '@example\.test$$'ドル記号を 2 つ重ねるのは誤記ではありません。Compose は単一の $ を展開対象の変数の開始として解釈します。そのため、コンテナにドル記号を 1 つそのまま渡すには $$ と記述します。正規表現は @example\.test$ として Mailpit に渡されます。
起動して、ヘルス状態を確認します。
docker compose up -d
docker compose psSTATUS 列は Up ... (healthy) と表示されるはずです。イメージには独自の healthcheck が含まれており、15 秒ごとに /mailpit readyz を実行します。そのため、コンテナが starting のまま、または unhealthy に変わる場合、通常はコンテナ内の 8025 番ポートでサービスが提供されていません。このチェックは MP_ 環境変数から設定を取得しますが、コマンドラインフラグは参照しません。そのため、このガイドでは設定をすべて environment: の下に記述しています。オプションをフラグで渡すと、Mailpit が正常にサービスを提供していても、チェック結果が unhealthy になることがあります。ほかを変更する前に docker compose logs mailpit を確認してください。
公開する各ポートにはアドレスを指定します。このアドレスがセキュリティ制御になります。コンテナ内では Mailpit が 0.0.0.0 で待ち受けます。コンテナには独自のネットワーク名前空間があるため、これは問題ありません。マッピングの左側で、外部から到達できる範囲が決まります。8025:8025 と記述すると、Docker はパブリックアドレスを含むホスト上のすべてのアドレスにバインドします。
ステージングアプリが同じファイル内のサービスである場合は、1025 マッピングを完全に削除し、アプリからホスト名 mailpit のポート 1025 に接続します。同じ Compose ネットワーク上のコンテナは直接相互接続できるため、SMTP ポートがホストに到達することはありません。名前解決については、Compose ネットワークでサービス名を解決する方法を参照してください。
1 件のメッセージを送信し、届いたことを確認する
python3 - <<'EOF'
import smtplib
from email.message import EmailMessage
m = EmailMessage()
m["From"] = "staging@example.test"
m["To"] = "anyone@example.test"
m["Subject"] = "Mailpit smoke test"
m.set_content("If this appears in the web interface, the sink works.")
with smtplib.SMTP("127.0.0.1", 1025) as s:
s.send_message(m)
EOF成功した場合、スクリプトは何も出力しません。API 経由でメッセージが保存されたことを確認します。
curl -s -u qa:yourpassword http://127.0.0.1:8025/api/v1/messages保存されたメッセージを一覧表示する JSON が返ります。-u フラグを外すと、同じリクエストは拒否されます。MP_UI_AUTH_FILE が API と Web インターフェースの両方を保護しているためです。受信トレイを読み取るテストでは、これらの認証情報も送信する必要があります。
Python スクリプトから ConnectionRefusedError が返る場合、127.0.0.1:1025 で待ち受けているプロセスがありません。SMTP のマッピングを削除した場合は、これが想定される結果です。その場合、確認処理は同じ Compose ネットワーク上のコンテナから実行する必要があります。
nginx 経由でパスワード付きの Web インターフェイスを公開する
現在、インターフェイスは loopback アドレスでのみ応答します。nginx が TLS を終端し、インターフェイスに到達する前にパスワードを要求します。
sudo htpasswd -B -c /etc/nginx/mailpit.htpasswd qaserver {
listen 443 ssl;
server_name mail-test.example.com;
ssl_certificate /etc/letsencrypt/live/mail-test.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mail-test.example.com/privkey.pem;
auth_basic "mailpit";
auth_basic_user_file /etc/nginx/mailpit.htpasswd;
location / {
proxy_pass http://127.0.0.1:8025;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
}構文チェック後、sudo nginx -t && sudo systemctl reload nginx で reload します。リバースプロキシが初めてなら、リバースプロキシブロックの各ディレクティブの役割を一度読んでおくとよいでしょう。
nginx のファイルと data/ui-auth では、同じ username と password を使用します。nginx はブラウザの Authorization ヘッダーを upstream に転送するため、認証情報が一致すれば、1 回の入力で両方のチェックを通過できます。認証情報が異なると、ブラウザには一方の認証情報が保持され、2 つ目のチェックで拒否されます。
Upgrade ヘッダーと Connection ヘッダーは飾りではありません。Mailpit は WebSocket 経由で、開いているページに新着メールを送信します。これらのヘッダーなしで HTTP/1.1 を実行するプロキシでは、接続を upgrade できません。その場合、ページは正しく読み込まれますが、更新されません。メールが届いて API に表示されても、reload するまで一覧は変わりません。
両方のロックを維持してください。nginx の password は公開アドレスを保護し、MP_UI_AUTH_FILE は port 8025 自体を保護します。これは重要です。staging app が生成したすべての password reset link は、retention 期間中、そのインターフェイスで読み取り可能だからです。
シンクをオープンリレーにしない
オープンリレーとは、誰からでもメッセージを受け取り、任意の宛先へ転送する SMTP サーバーです。スパマーは常にオープンリレーをスキャンしています。自分のアドレスで見つかると、迷惑メールの報告やアカウント停止につながります。
Mailpit は、初期状態ではオープンリレーではありません。デフォルトでは、メッセージを外部へ送信しないためです。外部送信機能は 2 つありますが、設定するまでどちらも無効です。MP_SMTP_RELAY_CONFIG はリレー設定ファイルを指定し、インターフェースで release 操作を有効にします。この操作では、選択したメッセージをそのサーバー経由で送信します。自動リレーには、追加で MP_SMTP_RELAY_ALL(デフォルトは false)または MP_SMTP_RELAY_MATCHING が必要です。MP_SMTP_FORWARD_CONFIG または MP_SMTP_FORWARD_* の変数で設定する転送は、受信したすべてのメッセージを指定した固定アドレスへコピーします。これらをすべて未設定にするのは、意図的な選択です。
この性質が失われる経路は 2 つあります。MP_SMTP_RELAY_ALL を有効にすると、受信したすべてのメッセージが元の宛先へ送信され、MP_SMTP_ALLOWED_RECIPIENTS は無視されます。この状態で SMTP ポートをインターネットへ公開すると、実際に動作するオープンリレーになります。MP_SMTP_RELAY_MATCHING は、パターンに一致する任意の宛先に対して同じ動作をします。リレーを有効にせずにポートを公開した場合、第三者があなたのサーバー経由でメールを送信することはできません。ただし、ストレージを使い切ったり、チームが信頼するインターフェースにコンテンツを表示させたりできます。転送を有効にしている場合、そのメッセージはコピーを受け取る受信トレイにも届きます。
Docker ホストで注意すべき点はファイアウォールです。ポートを公開すると、Docker は nat table に独自のルールを書き込みます。コンテナ宛てのトラフィックは、ufw(uncomplicated firewall)のルールが適用される前に、そこで照合されます。sudo ufw deny 1025/tcp は成功を報告しますが、何も変更しません。Docker が ufw を経由せずにポートを公開する理由では、ルールの処理順序を説明しています。
対策は、ファイアウォールルールではなく、マッピングに指定するアドレスを修正することです。実際にどのアドレスで待ち受けているかを確認します。
sudo ss -ltnp | grep -E ':(1025|8025)'正常な出力には 127.0.0.1:1025 と 127.0.0.1:8025 が表示されます。0.0.0.0:1025 と表示される行は、マッピングからアドレスが失われ、シンクがインターネットで待ち受けていることを示します。別のマシンから実行した nc -vz mail-test.example.com 1025 は、タイムアウトするか、接続を拒否されるはずです。
アプリケーションが別のサーバーで動作している場合、2 台を接続するために 1025 を公開しないでください。両方のマシンをプライベートネットワークまたは VPN トンネルに接続し、マッピングをそのインターフェースのアドレスにバインドします。
実際の受信メールが必要な場合にだけ MX レコードを公開する
MX(mail exchanger)レコードは、ドメイン宛てのメールを受け付けるホストを、他のメールサーバーに通知します。MX レコードを省略しても、それだけで受信経路が閉じるわけではありません。ドメインに MX レコードがない場合、RFC 5321 section 5.1 の規定により、送信側サーバーはそのドメインのアドレス(A または AAAA)レコードにフォールバックします。このサーバーの Web インターフェース用に指定したホスト名には、そのレコードがあります。インターネットからのメールを防ぐのは、このサーバー上で port 25 を待ち受けるものがないことです。inbox には、自分のアプリケーションが送信したメールだけが保存されます。これが test mailbox の用途です。
実際のメールを受信するには、送信側サーバーをこのサーバーへ誘導する DNS レコード(MX レコード、またはフォールバック先となるアドレスレコード)、port 25 で待ち受ける Mailpit(MP_SMTP_BIND_ADDR=0.0.0.0:25)、そしてそのポートを開放する設定が必要です。その時点で、そのドメインのすべてのアドレスを受け付ける公開 catch-all を運用することになります。以下の影響を明確に理解してください。
- レコードが公開されてから数日以内にスパムが届き始めます。harvester が DNS を読み取るためです。その後、dictionary attack によって一般的な名前が順番に試され、試行ごとにメッセージが保存されます。
- 知らない送信者からの添付ファイルがディスクに保存され、そのまま残ります。フィルタリング機能がないため、未知の送信者から届いたアーカイブが、自分の test mail の隣に置かれます。
- ドメインを知っている人は、そのドメインのアドレスを使って third party service に登録できます。確認メールはこのサーバーに配信されます。password gate に不備が生じると、そのアカウントは inbox を読む人のものになります。
- retention limit は単なる整理の問題ではなく、負荷を支える重要な制限になります。メール量を自分で制御できなくなるためです。
deliverability check のために実際の受信メールが必要なら、専用の subdomain を割り当て、MP_MAX_AGE を短く設定し、その中のすべてを公開情報として扱ってください。利用者が継続的に使う mailbox が必要なら、フィルタリングとバックアップを備えた本格的な mail server を別途運用してください。
保持期間: 上限のない catch-all がディスクを埋める仕組み
Mailpit はデフォルトで 500 件のメッセージを保持し、それを超えた分について最も古いものを定期的に削除します。MP_MAX_MESSAGES: 0 を指定するとこの上限が無効になり、MP_MAX_AGE も設定しなければ、自動的に削除されるものはありません。そのため、誰も気付かないうちに catch-all がディスクを埋めます。MP_MAX_AGE を指定すると保持期間を設定できます。メッセージ数の上限と併用することも、代わりに使うこともできます。値は時間または日数で、36h または 14d の形式で記述します。
MP_DATABASE によって、これらのデータを再起動後も保持するかどうかが決まります。これを指定しない場合、Mailpit は一時ファイルに書き込みます。このファイルはプロセス終了時に削除されるため、再起動のたびに受信トレイが空になります。指定するとメールは再起動後も保持され、ファイルは増大します。
容量を消費する主な原因は添付ファイルです。毎晩、2 MB の PDF レポートを 300 件のテストアドレスに送信すると、1 晩で 600 MB になります。メッセージ数の上限だけでは、増加に間に合いません。メディアを多く扱う PhotoPrism または Immich のような隣接サービスが小容量の VPS ディスクの大部分をすでに使用している場合もあるため、他の用途に必要な容量を含めて増加分を見積もってください。
du -h ~/mailpit/data/mailpit.db
df -h /上限が作動するのを待たず、CI の実行間でストアを空にします。
curl -s -u qa:yourpassword -X DELETE http://127.0.0.1:8025/api/v1/messagesInbucket では、INBUCKET_STORAGE_RETENTIONPERIOD(イメージ内では 72h)と INBUCKET_STORAGE_MAILBOXMSGCAP(300)で同じ問題に対処できます。どちらを使う場合も、最初のテストスイートが接続する前に上限を決めてください。
テストスイートから受信トレイを読む
GET /api/v1/messagesは保存されている内容を一覧表示し、GET /api/v1/message/{ID}は各部とヘッダーを含む1件のメッセージを返し、GET /api/v1/searchは絞り込み、DELETE /api/v1/messagesは保存内容を消去します。実行中のバージョンに対応した対話形式のドキュメントはhttp://127.0.0.1:8025/api/v1/で提供されています。
有用なテストでは、メッセージを送信し、表示されるまでポーリングし、件名と本文内のリンクを確認してから、すべてを削除します。1回のリクエストではなく、短いリトライループでポーリングしてください。メールをバックグラウンドワーカーでキューに入れるアプリケーションは、送信処理から戻った時点では Mailpit にメッセージがまだ届いていないことがあるためです。同じパターンはセルフホスト型の API テストおよびモックツールにも見られます。これは通常、本番環境に一切接続しないステージング環境を構成するもう一方の要素です。
FAQ
自ホストの使い捨てメール受信ボックスはオープンリレーですか?
リレーを無効にしている限り、オープンリレーにはなりません。デフォルトでは Mailpit はメールを外部へ送信しません。MP_SMTP_RELAY_CONFIG は選択したメッセージを手動でリリースする機能だけを有効にし、自動リレーには MP_SMTP_RELAY_ALL または MP_SMTP_RELAY_MATCHING も必要です。これらを設定しなければ、ポート 1025 に到達した第三者がサーバー経由でメールを送信することはできません。ただし、ストレージを埋めることはできます。そのため、SMTP ポートはアプリケーションだけが到達できるアドレスにバインドしてください。Compose で 1025:1025 として公開すると、すべてのホストアドレスにバインドされます。sudo ufw deny 1025/tcp を設定しても閉じられません。Docker 自身の nat ルールが先に適用されるためです。
テストドメインに MX レコードは必要ですか?
インターネットからメールを受信したい場合だけ必要です。ただし、MX レコードを設定しないだけでは受信を防げません。MX レコードがない場合、送信側サーバーはドメインの A レコードまたは AAAA レコードにフォールバックします。インターネットからのメールを実際に防ぐには、ポート 25 で何も待ち受けないようにします。受信ボックスには、自分のアプリケーションが SMTP 経由で送信したメールだけが入ります。DNS をサーバーに向けてポート 25 を開くと、公開されたキャッチオールになります。数日でスパムが届き、辞書攻撃によって試行ごとにメッセージが保存され、フィルタリングされていない第三者の添付ファイルがディスクを埋めます。
ページを再読み込みしたときだけメッセージ一覧が更新されるのはなぜですか?
Mailpit は WebSocket を使用して、開いているページへ新着メールをプッシュします。nginx の location ブロックに proxy_http_version 1.1 と Upgrade、Connection のヘッダーがないと、その接続をアップグレードできません。そのため、ページは正常に読み込まれた後、更新されなくなります。メールは届き続け、API もメールを返すため、受信ボックスは壊れているのではなく、古い状態に見えます。該当する行を追加して nginx を再読み込みし、その後ページを再読み込みしてください。
受信ボックスによるディスク容量の消費を止めるにはどうすればよいですか?
MP_MAX_MESSAGES を実際の数値に設定したまま、MP_MAX_AGE を追加します。デフォルトの上限は 500 メッセージです。0 に設定すると件数に基づく削除が無効になります。そのため、MP_MAX_AGE を設定しない限り何も削除されず、添付ファイル付きのキャッチオールが気付かないうちに肥大化します。MP_MAX_AGE には時間または日数を指定できます。たとえば 36h や 14d のように設定します。API には Web インターフェースと同じ認証情報が必要なので、CI の teardown では curl -u qa:yourpassword -X DELETE http://127.0.0.1:8025/api/v1/messages を使用してストアを消去してください。Inbucket では INBUCKET_STORAGE_RETENTIONPERIOD(72h)と INBUCKET_STORAGE_MAILBOXMSGCAP(300)で同じことができます。
Mailpit、Inbucket、MailHog のどれを実行すべきですか?
2026 年 10 月時点で、新規の用途には Mailpit を推奨します。MailHog は引き続き動作しますが、デフォルトブランチには 2022 年 8 月以降コミットがなく、依存関係が未修正のままリリースされています。Inbucket は積極的に保守されています(3.2.0、2026 年 10 月)。テストで POP3 が必要な場合は Inbucket が適しています。Mailpit の POP3 サーバーは、パスワードファイルまたは MP_POP3_AUTH 変数で認証情報を指定した場合にだけ起動するためです。Mailpit は MailHog と同じ 1025 と 8025 のポートを使用します。そのため、MailHog の置き換えに必要なのは Compose ファイル内のイメージ名の変更だけです。