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年8月に確認したものです。
MailHog (mailhog/mailhog) は SMTP で1025番ポートをリッスンし、8025番ポートでインターフェースを提供します。現在も動作します。ただし、デフォルトブランチへのコミットは2022年8月以降なく、issue tracker には250件を超える未解決の issue があります。そのため、テスト経路で更新されていない依存関係を実行することになります。新しい作業で使わないでください。
Inbucket (inbucket/inbucket) は SMTP で2500番ポート、Web インターフェースで9000番ポート、POP3(post office protocol version 3)で1100番ポートをリッスンします。Version 3.1.1 は2025年12月にリリースされました。メッセージを /storage 配下にファイルとして保存し、自動的に削除します。イメージでは INBUCKET_STORAGE_RETENTIONPERIOD=72h と INBUCKET_STORAGE_MAILBOXMSGCAP=300 を設定します。HTTP 呼び出しではなく POP3 クライアントライブラリでメールを収集するテストに適しています。
Mailpit (axllent/mailpit) は MailHog と同じ1025番ポートと8025番ポートを使用するため、アプリケーション設定を変更せずに MailHog と置き換えられます。Version 1.30.7 は2026年8月8日にリリースされました。このガイドで必要な機能をバイナリに内蔵しています。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.30
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) と表示されるはずです。このイメージには、15 秒ごとに /mailpit readyz を実行する独自の healthcheck が含まれています。そのため、コンテナが starting のままになるか、unhealthy に変わる場合、コンテナ内の 8025 でサービスを提供できていません。ほかの変更を行う前に docker compose logs mailpit を確認します。
公開する両方のポートにはアドレスを指定します。このアドレスがセキュリティ制御になります。コンテナ内で Mailpit は 0.0.0.0 を待ち受けます。コンテナには独自のネットワーク名前空間があるため、これは問題ありません。マッピングの左側で、外部から接続できる範囲を決めます。8025:8025 と記述すると、Docker はパブリックアドレスを含むホスト上のすべてのアドレスにバインドします。
staging アプリが同じファイル内のサービスである場合は、1025 マッピングを完全に削除し、アプリからポート 1025 のホスト名 mailpit に接続します。共有 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 header を upstream に転送するため、認証情報が一致すれば、1 回の入力で両方のチェックを通過できます。異なる認証情報を設定すると、ブラウザーは一方の認証情報を保持したまま、もう一方のチェックで拒否されます。
Upgrade と Connection の header は装飾ではありません。Mailpit は WebSocket を使用して、開いているページへ新着メールを送信します。これらの header を付けずに HTTP/1.1 で動作するプロキシでは、接続を upgrade できません。その場合、ページは正しく読み込まれますが、更新されません。メールが届いても API には表示されますが、reload するまで一覧は変わりません。
両方のロックを維持してください。nginx のパスワードは公開アドレスを保護し、MP_UI_AUTH_FILE は port 8025 自体を保護します。staging アプリがこれまでに生成したすべての password reset link は、このインターフェースで読めるためです。
Sink をオープンリレーにしない
オープンリレーとは、誰からでもメッセージを受け取り、任意の宛先へ転送する SMTP サーバーです。スパマーは常にオープンリレーを探しています。自分のアドレスでオープンリレーが見つかると、迷惑メール報告やアカウント停止につながります。
Mailpit は、標準状態ではオープンリレーではありません。メッセージを転送しないためです。MP_SMTP_RELAY_CONFIG にリレー設定ファイルを指定するまで、リレーは無効です。指定しない限り、インターフェースの release 操作も何も実行しません。未設定のままにすることは、意図した選択です。
この性質が失われる経路は2つあります。リレーを設定して release ボタンを機能させたうえで SMTP ポートをインターネットに公開すると、動作するオープンリレーになります。リレーを設定せずにポートだけを公開した場合、第三者はそのサーバー経由でメールを送信できません。ただし、ストレージを使い果たしたり、チームが信頼するインターフェースにコンテンツを書き込んだりできます。
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 と表示された行は、マッピングからアドレス指定が失われ、sink がインターネットで待ち受けていることを示します。別のマシンからは、nc -vz mail-test.example.com 1025 がタイムアウトするか、接続拒否になることを確認します。
アプリケーションが別のサーバーで動作している場合でも、2台を接続するために 1025 を開放しないでください。両方のマシンをプライベートネットワークまたは VPN トンネルに接続し、そのインターフェースのアドレスにマッピングをバインドします。
実際の受信メールが必要な場合にのみ MX レコードを公開する
MX(mail exchanger)レコードは、ドメイン宛てのメールを受け付けるホストを他のメールサーバーに通知します。使い捨てのドメインに MX レコードがなければ、インターネットからメールが届くことはありません。送信側のサーバーに配送先がないためです。受信トレイに保存されるのは、自分のアプリケーションが送信したメールだけです。テスト用メールボックスはそのために使います。
実際のメールを受信するには、ホストを指す MX レコード、port 25 で待ち受ける Mailpit(MP_SMTP_BIND_ADDR=0.0.0.0:25)、そしてその port の開放が必要です。この時点で、そのドメインのすべてのアドレスを受け付ける公開 catch-all を運用することになります。その後に起きることを明確に理解してください。
- DNS を参照する収集ボットがいるため、レコードが公開されてから数日以内にスパムが届き始めます。その後、辞書攻撃によって一般的な名前が順に試され、試行ごとにメッセージが保存されます。
- 見知らぬ送信者からの添付ファイルがディスクに保存され、そのまま残ります。フィルタリングされないため、未知の送信者からのアーカイブが自分のテストメールと同じ場所に保存されます。
- ドメインを知っている人は、そのドメインのアドレスで第三者サービスに登録できます。確認メールはサーバーに配信されます。パスワードによる保護に一度でも穴が生じると、それらのアカウントは受信トレイを読む人のものになります。
- 保存期間の制限は単なる整理ではなく、負荷を支える重要な設定になります。受信量を自分で制御できなくなるためです。
到達性を確認するために実際の受信メールが必要な場合は、専用のサブドメインを割り当て、MP_MAX_AGE を短く設定し、その中のすべてを公開情報として扱ってください。利用者が依存するメールボックスが必要な場合は、フィルタリングとバックアップを備えた実際のメールサーバーを別途運用してください。
保持期間: 無制限の catch-all がディスクを満杯にする仕組み
Mailpit はデフォルトで 500 件のメッセージを保持し、それを超えた分のうち古いものを定期的に削除します。MP_MAX_MESSAGES: 0 は自動削除を完全に無効にします。この変更だけで、誰も気付かないうちに 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 にリレー設定を指定するまで転送しません。そのため、1025 番ポートに到達した第三者が、サーバー経由でメールを送信することはできません。ただし、ストレージを使い果たされる可能性はあります。SMTP ポートは、アプリケーションだけが到達できるアドレスに bind してください。Compose で 1025:1025 と公開すると、すべてのホストアドレスに bind されます。Docker 独自の nat ルールが先に適用されるため、sudo ufw deny 1025/tcp ではポートを閉じられません。
テストドメインに MX レコードは必要ですか?
インターネットからメールを受信したい場合だけ必要です。MX レコードがないと、送信側サーバーは配信先を判断できません。そのため、受信ボックスには自分のアプリケーションが SMTP 経由で送信したメールだけが保存されます。レコードを公開して port 25 を開くと、公開された catch-all を運用することになります。数日以内に spam が届き、試行ごとにメッセージを保存する辞書攻撃を受け、フィルタリングされていない第三者の添付ファイルがディスクに保存されます。
ページを reload したときだけメッセージ一覧が更新されるのはなぜですか?
Mailpit は WebSocket を使用して、開いているページへ新着メールを push します。nginx の location block に proxy_http_version 1.1 と Upgrade、Connection の headers がないと、その接続を upgrade できません。そのため、ページは正常に読み込まれた後、更新されなくなります。メール自体は届き、API もメールを返します。これが、受信ボックスが壊れているのではなく、古い状態に見える理由です。該当する行を追加して nginx を reload し、その後ページを reload してください。
受信ボックスによるディスク容量の圧迫を防ぐにはどうすればよいですか?
MP_MAX_MESSAGES には実際の数値を設定し、MP_MAX_AGE を追加してください。デフォルトの上限は 500 メッセージです。0 に設定すると削除が完全に無効になり、添付ファイル付きの catch-all が気付かないうちに容量を消費する原因になります。MP_MAX_AGE には時間または日数を指定できます。たとえば、36h や 14d です。CI の teardown では curl -X DELETE http://127.0.0.1:8025/api/v1/messages を使用して store を消去してください。Inbucket では INBUCKET_STORAGE_RETENTIONPERIOD (72h) と INBUCKET_STORAGE_MAILBOXMSGCAP (300) で同じ制御ができます。
Mailpit、Inbucket、MailHog のどれを実行すべきですか?
2026 年 8 月時点では、新規の用途には Mailpit を使用してください。MailHog は現在も動作しますが、default branch には 2022 年 8 月以降 commit がないため、依存関係が未修正のままリリースされています。Inbucket は現在も積極的に保守されています(3.1.1、2025 年 12 月)。Mailpit では password file を指定しないと POP3 server が起動しないため、テストで POP3 が必要な場合は Inbucket の方が適しています。Mailpit は MailHog と同じ port、1025 と 8025 を使用します。そのため、MailHog の置き換えに必要なのは Compose file 内の image name の変更だけです。