Własna tymczasowa skrzynka e-mail na VPS z Mailpit
Konfiguracja serwera SMTP typu catch-all przy użyciu Mailpit i Docker Compose. Instrukcja bezpiecznego przechwytywania wiadomości testowych na własnym VPS bez ryzyka wysyłki.
Czym jest tymczasowa skrzynka odbiorcza
Tymczasowa skrzynka odbiorcza to niewielki serwer SMTP (Simple Mail Transfer Protocol), który akceptuje pocztę dla dowolnego adresu, lecz nie dostarcza jej do adresatów. Aplikacja w środowisku testowym wysyła wiadomości do tego serwera zamiast do rzeczywistego dostawcy poczty, a każda wiadomość zostaje w nim zatrzymana. Treść przychodzących wiadomości odczytuje się przez interfejs WWW, dzięki czemu błędna lista odbiorców lub uszkodzony szablon nie generują kosztów ani ryzyka, ponieważ poczta nigdy nie opuszcza skrzynki.
Niniejszy przewodnik opisuje budowę takiego rozwiązania na pojedynczym VPS przy użyciu Docker Compose. Mailpit pełni rolę uniwersalnego odbiornika (catch-all sink). Jego nasłuch SMTP jest powiązany z adresem dostępnym wyłącznie dla aplikacji, interfejs WWW znajduje się za nginx z włączonym TLS (Transport Layer Security) oraz ochroną hasłem, a limit retencji zapobiega zapełnieniu dysku przez skrzynkę. Jeśli Docker Compose jest nowym narzędziem, podstawy Compose dla VPS wyjaśniają układ plików przyjęty w tym przewodniku.
Efektem końcowym jest narzędzie testowe, a nie serwer pocztowy. Nie posiada ono kont użytkowników, mechanizmów dostarczania ani filtrowania spamu. Rzeczywiste skrzynki pocztowe dla użytkowników to pełnoprawny serwer pocztowy, taki jak Mailcow, co stanowi znacznie bardziej złożone zadanie.
Mailpit kontra Inbucket kontra MailHog: który serwer typu sink wybrać
Zadanie to realizują trzy narzędzia. Różnią się one statusem utrzymania, portami, na których nasłuchują, oraz możliwościami obsługi wiadomości po ich odebraniu. Poniższe wersje sprawdzono w sierpniu 2026 roku.
MailHog (mailhog/mailhog) nasłuchuje na porcie 1025 dla SMTP i udostępnia interfejs na porcie 8025. Narzędzie nadal działa. W domyślnej gałęzi nie odnotowano żadnego commita od sierpnia 2022 roku, a w systemie śledzenia błędów znajduje się ponad 250 otwartych zgłoszeń, co oznacza pracę na niezałatanych zależnościach w środowisku testowym. Nie należy rozpoczynać z nim nowych projektów.
Inbucket (inbucket/inbucket) nasłuchuje na porcie 2500 dla SMTP, 9000 dla interfejsu webowego oraz 1100 dla POP3 (post office protocol version 3). Wersja 3.1.1 została wydana w grudniu 2025 roku. Przechowuje wiadomości jako pliki w lokalizacji /storage i samodzielnie je usuwa: obraz ustawia parametry INBUCKET_STORAGE_RETENTIONPERIOD=72h oraz INBUCKET_STORAGE_MAILBOXMSGCAP=300. Należy wybrać to rozwiązanie, gdy test wymaga pobrania poczty za pomocą biblioteki klienta POP3 zamiast wywołania HTTP.
Mailpit (axllent/mailpit) korzysta z tych samych portów co MailHog, czyli 1025 i 8025, dzięki czemu zastępuje MailHog bez konieczności modyfikacji konfiguracji aplikacji. Wersja 1.30.7 została wydana 8 sierpnia 2026 roku. Zawiera ona wewnątrz pliku binarnego funkcje wymagane przez ten przewodnik: plik haseł dla interfejsu webowego i API (application programming interface), limit liczby wiadomości, limit wieku wiadomości oraz filtr odbiorców. Dalsza część tego przewodnika wykorzystuje Mailpit.
Jak działa mechanizm catch-all i dlaczego DNS nie bierze w tym udziału
Aplikacja nie sprawdza, dokąd dostarczyć wiadomość. Przekazuje hosta oraz port, otwiera połączenie TCP i ogłasza RCPT TO:<anyone@example.test>. Mailpit akceptuje każdego odbiorcę, niezależnie od treści, zapisuje wiadomość i nie przesyła jej dalej. Domena nie jest rozwiązywana, dlatego example.test działa, mimo że .test jest nazwą zarezerwowaną, która nie istnieje w systemie nazw domenowych (DNS).
To cały mechanizm i właśnie dlatego skrzynka odbiorcza jest domyślnie bezpieczna. Nie jest wymagany rekord MX (mail exchanger), nie podejmuje się prób dostarczenia i żadna wiadomość nie może trafić do rzeczywistego adresata.
Skierowanie aplikacji testowej na serwer przechwytujący
Ustaw host SMTP aplikacji na mailpit, gdy aplikacja działa jako kontener w tym samym projekcie Compose, lub na 127.0.0.1, gdy działa bezpośrednio na hoście. Ustaw port na 1025, wyłącz TLS oraz pozostaw nazwę użytkownika i hasło puste. Mailpit akceptuje pocztę anonimową.
Niektóre frameworki odmawiają wysyłki bez poświadczeń. MP_SMTP_AUTH_ACCEPT_ANY=1 sprawia, że Mailpit akceptuje dowolną nazwę użytkownika i hasło, a MP_SMTP_AUTH_ALLOW_INSECURE=1 zezwala na mechanizmy PLAIN oraz LOGIN przy nieszyfrowanym połączeniu. Te dwa ustawienia są tutaj bezpieczne tylko dlatego, że serwer nasłuchujący jest niedostępny z Internetu, co wymusza poniższa konfiguracja wdrożeniowa.
MP_SMTP_ALLOWED_RECIPIENTS warto ustawić od samego początku. Przyjmuje ono wyrażenie regularne i odrzuca każdego odbiorcę, który do niego nie pasuje. Skieruj je na swoją domenę testową, dzięki czemu baza danych środowiska testowego, która wciąż zawiera rzeczywiste adresy klientów, wywoła widoczny błąd w logach aplikacji zamiast wiadomości, która po cichu trafi do serwera przechwytującego.
Plik Docker Compose
Najpierw utwórz katalog oraz plik z hasłem dla interfejsu WWW. htpasswd -B zapisuje skrót bcrypt, a Mailpit odczytuje zarówno bcrypt, jak i tekst jawny.
mkdir -p ~/mailpit/data
cd ~/mailpit
sudo apt update && sudo apt install -y apache2-utils
htpasswd -B -c data/ui-auth qaZapisz compose.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$$'Podwójny znak dolara nie jest błędem. Compose interpretuje pojedynczy $ jako początek zmiennej do rozwinięcia, więc $$ jest sposobem na przekazanie dosłownego znaku dolara do kontenera. Wyrażenie regularne dociera do Mailpit jako @example\.test$.
Uruchom usługę i sprawdź stan zdrowia:
docker compose up -d
docker compose psKolumna STATUS powinna wskazywać Up ... (healthy). Obraz zawiera własny mechanizm healthcheck, który uruchamia /mailpit readyz co 15 sekund, więc kontener pozostający w stanie starting lub zmieniający go na unhealthy nie obsługuje ruchu na porcie 8025 wewnątrz kontenera. Przed wprowadzeniem dalszych zmian zapoznaj się z docker compose logs mailpit.
Oba opublikowane porty zawierają adres, który stanowi kontrolę bezpieczeństwa. Wewnątrz kontenera Mailpit nasłuchuje na 0.0.0.0, co jest poprawne, ponieważ kontener posiada własną przestrzeń nazw sieciowych. Lewa strona mapowania decyduje o tym, kto uzyskuje dostęp z zewnątrz. Zapis 8025:8025 powoduje, że Docker wiąże usługę z każdym adresem na hoście, w tym z adresem publicznym.
Jeśli aplikacja testowa jest usługą w tym samym pliku, usuń całkowicie mapowanie 1025 i skieruj aplikację na nazwę hosta mailpit na porcie 1025. Kontenery w tej samej sieci Compose komunikują się bezpośrednio, dzięki czemu port SMTP w ogóle nie jest wystawiany na hoście. W jaki sposób sieci Compose rozwiązują nazwy usług zawiera szczegółowe informacje na ten temat.
Wysłanie pojedynczej wiadomości i weryfikacja jej dostarczenia
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)
EOFSkrypt nie wyświetla żadnych komunikatów w przypadku powodzenia. Należy potwierdzić zapisanie wiadomości za pomocą API:
curl -s -u qa:yourpassword http://127.0.0.1:8025/api/v1/messagesPolecenie to zwraca obiekt JSON z listą zapisanych wiadomości. Usunięcie flagi -u spowoduje odrzucenie żądania, ponieważ MP_UI_AUTH_FILE chroni jednocześnie API oraz interfejs WWW. Każdy test odczytujący skrzynkę odbiorczą musi przesyłać te dane uwierzytelniające.
Błąd ConnectionRefusedError w skrypcie Python oznacza, że na porcie 127.0.0.1:1025 nie nasłuchuje żadna usługa. Jest to oczekiwany rezultat w przypadku usunięcia mapowania SMTP; w takiej sytuacji test musi zostać uruchomiony z poziomu kontenera działającego w tej samej sieci Compose.
Udostępnianie interfejsu WWW przez nginx z hasłem
Interfejs obecnie odpowiada wyłącznie na adresie loopback. nginx kończy połączenie TLS i wymaga podania hasła, zanim jakikolwiek ruch dotrze do usługi.
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;
}
}Przeładuj konfigurację po sprawdzeniu składni za pomocą sudo nginx -t && sudo systemctl reload nginx. Warto jednorazowo zapoznać się z dokumentacją działania poszczególnych dyrektyw w bloku reverse proxy, jeśli jest to pierwsza konfiguracja proxy.
Użyj tej samej nazwy użytkownika i hasła w pliku nginx oraz w data/ui-auth. nginx przekazuje nagłówek Authorization przeglądarki do backendu, dzięki czemu zgodne poświadczenia spełniają oba wymogi po jednorazowym monicie. Różne poświadczenia spowodują, że przeglądarka prześle zestaw danych odrzucony przez drugą kontrolę.
Nagłówki Upgrade oraz Connection nie są zbędne. Mailpit przesyła nową pocztę do otwartej strony za pośrednictwem WebSocket, a proxy działające w trybie HTTP/1.1 bez tych nagłówków nie może dokonać aktualizacji połączenia. W takim przypadku strona ładuje się poprawnie, ale nie aktualizuje: poczta dociera, API ją wykazuje, lecz lista pozostaje statyczna do momentu odświeżenia strony.
Zachowaj obie blokady. Hasło w nginx chroni adres publiczny, a MP_UI_AUTH_FILE chroni sam port 8025. Jest to istotne, ponieważ każdy link do resetowania hasła wygenerowany przez aplikację testową jest widoczny w tym interfejsie.
Nigdy nie dopuszczaj do sytuacji, w której sink staje się otwartym przekaźnikiem (open relay)
Otwarty przekaźnik to serwer SMTP, który przyjmuje wiadomość od dowolnego nadawcy i przekazuje ją do dowolnego odbiorcy. Spamerzy nieustannie skanują sieć w poszukiwaniu takich serwerów, a znalezienie jednego pod Twoim adresem kończy się zgłoszeniami o nadużyciach i zawieszeniem konta.
Mailpit nie jest otwartym przekaźnikiem w konfiguracji domyślnej, ponieważ nigdy nie przekazuje wiadomości dalej. Przekazywanie pozostaje wyłączone, dopóki nie wskażesz MP_SMTP_RELAY_CONFIG pliku konfiguracyjnego przekaźnika, a akcja zwolnienia wiadomości (release) w interfejsie nie wywołuje żadnego działania, dopóki tego nie zrobisz. Pozostawienie tego ustawienia nieaktywnym jest celowym wyborem.
Istnieją dwa sposoby na utratę tej właściwości. Skonfiguruj przekaźnik tak, aby przycisk zwolnienia działał, a następnie wystaw port SMTP na świat – w ten sposób zbudujesz działający otwarty przekaźnik. Wystaw port bez konfiguracji przekaźnika, a osoby trzecie nie będą mogły wysyłać poczty przez Twój serwer, ale mogą zapełnić pamięć masową i umieścić treści w interfejsie, któremu ufa Twój zespół.
Pułapką na hoście Docker jest zapora sieciowa. Publikacja portu sprawia, że Docker dopisuje własne reguły do tablicy nat, a ruch kierowany do kontenera jest dopasowywany tam, zanim reguły ufw (uncomplicated firewall) otrzymają szansę zadziałania. sudo ufw deny 1025/tcp zgłasza sukces, nie zmieniając niczego w konfiguracji zapory. Dlaczego Docker publikuje porty z pominięciem ufw szczegółowo omawia kolejność łańcuchów przetwarzania pakietów.
Rozwiązaniem jest określenie adresu w mapowaniu, a nie reguła zapory. Sprawdź, na jakim adresie usługa faktycznie nasłuchuje:
sudo ss -ltnp | grep -E ':(1025|8025)'Prawidłowy wynik wskazuje 127.0.0.1:1025 oraz 127.0.0.1:8025. Linia zawierająca 0.0.0.0:1025 oznacza, że mapowanie utraciło przypisanie do adresu i sink nasłuchuje na interfejsie publicznym. Z innej maszyny polecenie nc -vz mail-test.example.com 1025 powinno zakończyć się przekroczeniem czasu oczekiwania lub odrzuceniem połączenia.
Jeśli aplikacja znajduje się na innym serwerze, nie otwieraj portu 1025 w celu połączenia obu maszyn. Umieść obie maszyny w sieci prywatnej lub tunelu VPN i powiąż mapowanie z adresem tego interfejsu.
Publikuj rekordy MX tylko wtedy, gdy oczekujesz rzeczywistej poczty przychodzącej
Rekord MX (mail exchanger) informuje inne serwery pocztowe, który host przyjmuje wiadomości dla danej domeny. Bez rekordu MX w domenie tymczasowej żadna poczta z Internetu nie dotrze do serwera, ponieważ serwery nadawcze nie mają miejsca docelowego. Skrzynka odbiorcza zawiera tylko wiadomości wysłane przez własne aplikacje, do czego właśnie służy skrzynka testowa.
Odbieranie rzeczywistej poczty wymaga rekordu MX wskazującego na serwer, nasłuchiwania Mailpit na porcie 25 (MP_SMTP_BIND_ADDR=0.0.0.0:25) oraz otwarcia tego portu. W tym momencie uruchamiasz publiczny serwer typu catch-all dla każdego adresu w domenie. Należy mieć świadomość konsekwencji:
- Spam pojawia się w ciągu kilku dni od publikacji rekordu, ponieważ boty skanują DNS. Ataki słownikowe sprawdzają popularne nazwy użytkowników i zapisują wiadomość dla każdej próby.
- Załączniki od nieznanych nadawców trafiają na dysk i pozostają tam. Brak filtrów sprawia, że archiwum od nieznanego nadawcy znajduje się obok własnych wiadomości testowych.
- Każdy, kto pozna domenę, może zarejestrować się w zewnętrznych serwisach przy użyciu adresu w niej zawartego, a wiadomość potwierdzająca zostanie dostarczona na serwer. Jeśli zabezpieczenia hasłem zawiodą, konta te należą do osoby mającej dostęp do skrzynki odbiorczej.
- Limity retencji przestają być kwestią porządkową, a stają się kluczowym elementem obciążenia, ponieważ wolumen wiadomości przestaje być zależny od użytkownika.
Jeśli potrzebujesz rzeczywistej poczty przychodzącej do testów dostarczalności, użyj dedykowanej subdomeny, zachowaj krótki czas MP_MAX_AGE i traktuj wszystko, co się w niej znajduje, jako publiczne. Jeśli potrzebujesz skrzynek pocztowych, na których można polegać, uruchom profesjonalny serwer pocztowy z filtrowaniem i kopiami zapasowymi.
Retencja: jak nieograniczony catch-all zapełnia dysk
Mailpit domyślnie przechowuje 500 wiadomości i okresowo usuwa najstarsze po przekroczeniu tego limitu. Flaga MP_MAX_MESSAGES: 0 całkowicie wyłącza automatyczne usuwanie, a ta jedna zmiana sprawia, że catch-all zapełnia dysk, zanim ktokolwiek to zauważy. Flaga MP_MAX_AGE dodaje limit czasowy i przyjmuje wartości w godzinach lub dniach, zapisywane jako 36h lub 14d.
Flaga MP_DATABASE decyduje o tym, czy dane przetrwają restart. Bez niej Mailpit zapisuje wiadomości do pliku tymczasowego, który jest usuwany po zakończeniu procesu, więc każdy restart czyści skrzynkę odbiorczą. Z tą flagą poczta przetrwa restarty, a plik będzie stale rosnąć.
To załączniki zajmują miejsce. Nocne zadanie, które wysyła raport PDF o rozmiarze 2 MB na 300 adresów testowych, generuje 600 MB każdej nocy, a sam limit liczby wiadomości nie zareaguje wystarczająco szybko. Należy zaplanować ten przyrost w odniesieniu do pozostałych danych na wolumenie, ponieważ sąsiad intensywnie korzystający z mediów, taki jak PhotoPrism lub Immich, prawdopodobnie już zajął większość miejsca na małym dysku VPS.
du -h ~/mailpit/data/mailpit.db
df -h /Należy czyścić magazyn między uruchomieniami CI, zamiast czekać na zadziałanie limitu:
curl -s -u qa:yourpassword -X DELETE http://127.0.0.1:8025/api/v1/messagesInbucket rozwiązuje ten sam problem za pomocą INBUCKET_STORAGE_RETENTIONPERIOD (72h w obrazie) oraz INBUCKET_STORAGE_MAILBOXMSGCAP (300). Niezależnie od wybranego narzędzia, należy ustalić limity, zanim pierwsza suita testowa zacznie z niego korzystać.
Odczytywanie skrzynki odbiorczej z zestawu testów
GET /api/v1/messages wyświetla listę przechowywanych elementów, GET /api/v1/message/{ID} zwraca pojedynczą wiadomość wraz z jej częściami i nagłówkami, GET /api/v1/search filtruje zawartość, a DELETE /api/v1/messages czyści magazyn. Interaktywna dokumentacja dla używanej wersji jest dostępna pod adresem http://127.0.0.1:8025/api/v1/.
Użyteczny test wysyła wiadomość, odpytuje o jej pojawienie się, sprawdza temat oraz zawarty w niej link, a następnie usuwa wszystkie dane. Należy stosować odpytywanie w krótkiej pętli ponowień zamiast pojedynczego żądania, ponieważ aplikacja kolejkująca pocztę w procesie działającym w tle zwraca odpowiedź z wywołania wysyłki, zanim Mailpit otrzyma wiadomość. Ten sam wzorzec pojawia się w narzędziach do testowania i mockowania API typu self-hosted, które zazwyczaj stanowią drugą połowę środowiska stagingowego, niemającego styczności z produkcją.
FAQ
Czy własna tymczasowa skrzynka odbiorcza e-mail jest otwartym przekaźnikiem (open relay)?
Nie, dopóki funkcja przekaźnika pozostaje wyłączona. Mailpit przechowuje wiadomości i nigdy ich nie przekazuje, dopóki nie wskażesz MP_SMTP_RELAY_CONFIG w konfiguracji przekaźnika. Dzięki temu osoba niepowołana, która uzyska dostęp do portu 1025, nie może wysyłać poczty przez Twój serwer. Może jednak zapełnić przestrzeń dyskową, dlatego należy powiązać port SMTP z adresem dostępnym wyłącznie dla Twojej aplikacji. Publikacja jako 1025:1025 w pliku Compose wiąże usługę ze wszystkimi adresami hosta, a sudo ufw deny 1025/tcp nie zamknie dostępu, ponieważ reguły nat w Dockerze są przetwarzane w pierwszej kolejności.
Czy potrzebuję rekordu MX dla domeny testowej?
Tylko jeśli chcesz, aby poczta z Internetu docierała do serwera. Bez rekordu MX serwery nadawcze nie mają miejsca docelowego, więc skrzynka zawiera tylko to, co Twoje aplikacje przesyłają przez SMTP. Opublikowanie rekordu i otwarcie portu 25 oznacza uruchomienie publicznej skrzynki typu catch-all: w ciągu kilku dni pojawi się spam, ataki słownikowe zapisujące wiadomość przy każdej próbie oraz załączniki od nieznajomych na Twoim dysku, bez żadnego filtrowania.
Dlaczego lista wiadomości aktualizuje się tylko po odświeżeniu strony?
Mailpit przesyła nową pocztę do otwartej strony za pomocą protokołu WebSocket. Blok location w nginx, w którym brakuje proxy_http_version 1.1 oraz nagłówków Upgrade i Connection, nie może dokonać aktualizacji połączenia, więc strona ładuje się normalnie, a następnie przestaje reagować. Poczta nadal dociera, a API nadal ją zwraca, dlatego skrzynka wygląda na nieaktualną, a nie na uszkodzoną. Dodaj te linie, przeładuj nginx, a następnie odśwież stronę.
Jak zapobiec zapełnieniu dysku przez skrzynkę odbiorczą?
Utrzymuj MP_MAX_MESSAGES na realnym poziomie i dodaj MP_MAX_AGE. Domyślny limit to 500 wiadomości, a ustawienie 0 całkowicie wyłącza usuwanie, co sprawia, że skrzynka catch-all z załącznikami niepostrzeżenie rośnie. MP_MAX_AGE akceptuje godziny lub dni, na przykład 36h lub 14d. Wyczyść magazyn podczas usuwania środowiska CI za pomocą curl -X DELETE http://127.0.0.1:8025/api/v1/messages. Inbucket wykonuje to samo zadanie za pomocą INBUCKET_STORAGE_RETENTIONPERIOD (72h) oraz INBUCKET_STORAGE_MAILBOXMSGCAP (300).
Czy powinienem używać Mailpit, Inbucket czy MailHog?
Wybierz Mailpit do nowych projektów (stan na sierpień 2026). MailHog nadal działa, ale jego domyślna gałąź nie otrzymała żadnego commita od sierpnia 2022 roku, przez co zawiera niezałatane zależności. Inbucket jest aktywnie utrzymywany (wersja 3.1.1, grudzień 2025) i stanowi lepszy wybór, gdy test wymaga obsługi POP3, ponieważ serwer POP3 w Mailpit uruchamia się dopiero po podaniu pliku z hasłami. Mailpit korzysta z tych samych portów co MailHog, czyli 1025 i 8025, więc zastąpienie MailHog wymaga jedynie zmiany nazwy obrazu w pliku Compose.