Jedna komenda daje localhostowi publiczny URL z HTTPS — bez DNS, bez certyfikatu, bez reguł firewalla. Oto jak efektywnie używać ngrok w pracy z Laravel’em: od testowania webhooków Stripe lokalnie, po prezentowanie funkcji klientowi bez deployu.
📋 Spis treści
- 🔌 Jak działa ngrok
- ⚙️ Instalacja i pierwsze uruchomienie
- 🔐 Statyczne domeny i stabilne URL-e
- 💳 Testowanie webhooków płatności lokalnie
- 🛠️ ngrok z Laravel Herd i Valet
- 🔍 Inspektor ngrok — podgląd surowych payloadów webhooków
- 🧪 Automatyczne testowanie webhooków bez ngrok
- 🚀 Inne przypadki użycia, które warto znać
- ✅ Podsumowanie
🔌 Jak działa ngrok
ngrok tworzy bezpieczny tunel między publicznym URL-em a portem działającym na lokalnej maszynie. Gdy żądanie trafia na https://abc123.ngrok-free.app, ngrok przekazuje je zaszyfrowanym tunelem do http://localhost:8000 (lub innego skonfigurowanego portu).
Dostawca płatności (Stripe)
│
▼
https://twoja-subdomena.ngrok-free.app ← publiczny endpoint HTTPS
│ (zaszyfrowany tunel)
▼
http://localhost:8000 ← lokalny serwer Laravel
│
▼
WebhookController@handle
Bez konfiguracji DNS, bez certyfikatu SSL, bez reguł firewalla. Tunel jest nawiązywany wychodzącym połączeniem z Twojej maszyny — działa za NAT-em, korporacyjnym firewallem i VPN-em.
Czym ngrok nie jest: narzędziem do deploymentu. Tunel zamyka się po zamknięciu terminala. To wyłącznie narzędzie deweloperskie i debugujące.
⚙️ Instalacja i pierwsze uruchomienie
macOS (Homebrew):
brew install ngrok/ngrok/ngrok
Bezpośrednie pobranie:
# Pobierz z https://ngrok.com/download — rozpakuj i przenieś do PATH
sudo mv ngrok /usr/local/bin
Uwierzytelnienie (wymagane darmowe konto dla stabilnych URL-i):
ngrok config add-authtoken TWÓJ_TOKEN
Wystawienie lokalnego serwera Laravel:
# Uruchom Laravel
php artisan serve # domyślnie port :8000
# W drugim terminalu wyeksponuj go na zewnątrz
ngrok http 8000
Wynik:
Session Status online
Account [email protected] (Plan: Free)
Forwarding https://abc123.ngrok-free.app -> http://localhost:8000
Connections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00
URL https://abc123.ngrok-free.app jest teraz publicznie dostępny. Możesz go wkleić gdziekolwiek — do panelu Stripe, Postmana, przeglądarki kolegi, urządzenia mobilnego w innej sieci.
Z niestandardowym portem (Laravel Sail / Docker):
ngrok http 80 # jeśli Sail mapuje na port 80
🔐 Statyczne domeny i stabilne URL-e
Darmowy plan przydziela losową subdomenę przy każdym restarcie tunelu. To problem przy konfiguracji webhooków — przy każdym restarcie ngrok trzeba by aktualizować URL w panelu Stripe.
Rozwiązanie: statyczna domena (bezpłatna)
Darmowy plan ngrok zawiera jedną statyczną domenę. Zarezerwuj ją na dashboard.ngrok.com/domains.
ngrok http --domain=twoja-nazwa.ngrok-free.app 8000
Teraz URL się nie zmienia. Skonfiguruj go w Stripe raz i restartuj ngrok ile razy chcesz.
Plik konfiguracyjny dla powtarzalnych sesji:
# ~/.config/ngrok/ngrok.yml
version: "3"
authtoken: TWÓJ_TOKEN
tunnels:
laravel:
proto: http
addr: 8000
domain: twoja-nazwa.ngrok-free.app
ngrok start laravel
💳 Testowanie webhooków płatności lokalnie
Tu ngrok przynosi największą wartość. Dostawcy płatności wysyłają zdarzenia webhook (płatność potwierdzona, zwrot, anulowanie subskrypcji) na URL zarejestrowany w ich panelu. Ten URL musi być publicznie dostępny. Podczas lokalnego developmentu localhost odpada.
Stripe
1. Uruchom ngrok ze statyczną domeną:
ngrok http --domain=twoja-nazwa.ngrok-free.app 8000
2. Zarejestruj endpoint webhooka w panelu Stripe:
Developers → Webhooks → Add endpoint
URL: https://twoja-nazwa.ngrok-free.app/webhook/stripe
Events: payment_intent.succeeded, payment_intent.payment_failed, charge.refunded
3. Trasa webhooka w Laravel (poza middleware api, bez CSRF):
// routes/web.php
use App\Http\Controllers\Webhook\StripeWebhookController;
Route::post('/webhook/stripe', StripeWebhookController::class)
->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class]);
4. Kontroler — zawsze weryfikuj sygnaturę:
// app/Http/Controllers/Webhook/StripeWebhookController.php
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Webhook;
use App\Http\Controllers\Controller;
use App\Jobs\HandleStripePaymentSucceeded;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Stripe\Exception\SignatureVerificationException;
use Stripe\Webhook;
class StripeWebhookController extends Controller
{
public function __invoke(Request $request): Response
{
$payload = $request->getContent();
$sigHeader = $request->header('Stripe-Signature');
try {
$event = Webhook::constructEvent(
$payload,
$sigHeader,
config('services.stripe.webhook_secret'),
);
} catch (SignatureVerificationException) {
return response('Invalid signature', 400);
}
match ($event->type) {
'payment_intent.succeeded' => HandleStripePaymentSucceeded::dispatch($event->data->object),
'payment_intent.payment_failed' => logger()->warning('Payment failed', ['id' => $event->data->object->id]),
default => null,
};
return response('OK', 200);
}
}
5. Ustaw sekret webhooka w .env:
# Z panelu Stripe → Webhooks → Twój endpoint → Signing secret
STRIPE_WEBHOOK_SECRET=whsec_...
6. Wywołaj testowe zdarzenie przez Stripe CLI:
brew install stripe/stripe-cli/stripe
stripe login
stripe trigger payment_intent.succeeded
Stripe wysyła prawdziwy podpisany webhook na URL ngrok, który przekazuje go do Laravel. Możesz debugować cały przepływ przez dd() lub Telescope — bez żadnego mockowania.
Przelewy24 / PayU / Tpay
Polscy dostawcy płatności działają identycznie, ale zamiast nagłówkowej sygnatury Stripe używają whitelisty IP lub podpisów HMAC. ngrok działa tak samo.
Przykład Przelewy24:
Zarejestruj URL ngrok jako adres powiadomień zwrotnych w panelu P24:
https://twoja-nazwa.ngrok-free.app/webhook/p24
// app/Http/Controllers/Webhook/P24WebhookController.php
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Webhook;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;
class P24WebhookController extends Controller
{
public function __invoke(Request $request): Response
{
// P24 wysyła weryfikację CRC — sprawdź ją przed przetworzeniem
$expectedSign = md5(
$request->input('p24_session_id') . '|' .
$request->input('p24_order_id') . '|' .
$request->input('p24_amount') . '|' .
$request->input('p24_currency') . '|' .
config('services.p24.crc_key')
);
if ($request->input('p24_sign') !== $expectedSign) {
Log::warning('P24 nieprawidłowa sygnatura', $request->all());
return response('ERROR', 400);
}
// Przetwórz zweryfikowane powiadomienie
Log::info('P24 płatność potwierdzona', [
'order_id' => $request->input('p24_order_id'),
'session_id' => $request->input('p24_session_id'),
'amount' => $request->input('p24_amount'),
]);
return response('OK', 200);
}
}
Ważne: Zawsze weryfikuj sygnaturę lub CRC każdego przychodzącego webhooka. ngrok wystawia lokalny serwer na publiczny internet — niezweryfikowany handler webhooka to otwarty endpoint.
🛠️ ngrok z Laravel Herd i Valet
Jeśli używasz Laravel Herd lub Valet, Twoja aplikacja jest już dostępna pod domeną .test (np. myapp.test). ngrok może tunelować bezpośrednio do tego wirtualnego hosta.
Herd / Valet:
# Herd używa portu 80 z wirtualnym hostingiem — przekaż nagłówek host
ngrok http --host-header=myapp.test 80
Bez --host-header żądanie dociera do nginx/caddy bez poprawnego nagłówka Host i zwraca 404.
Albo użyj wbudowanej funkcji share w Herd:
Laravel Herd Pro zawiera wbudowaną funkcję udostępniania stron, która używa ngrok pod spodem. Jeśli masz Herd Pro, kliknij prawym przyciskiem myszy na stronę w aplikacji Herd w pasku menu i wybierz "Share" — konfiguracja ngrok jest obsługiwana automatycznie.
Dla Herd Basic używaj ngrok bezpośrednio z flagą --host-header.
🔍 Inspektor ngrok — podgląd surowych payloadów webhooków
ngrok uruchamia lokalny inspektor webowy na http://localhost:4040. To jedna z jego najbardziej przydatnych funkcji przy debugowaniu webhooków.
Otwórz przeglądarkę podczas działania ngrok:
http://localhost:4040
Otrzymasz podgląd w czasie rzeczywistym każdego żądania, które przeszło przez tunel:
- Pełne nagłówki i treść żądania (surowy payload JSON od Stripe, P24 itp.)
- Status odpowiedzi i treść zwrócona przez Laravel
- Czas obsługi żądania
- Przycisk Replay — wysyła dokładnie to samo żądanie ponownie bez wywoływania go od nowa po stronie dostawcy płatności
Funkcja replay jest szczególnie przydatna przy iteracji nad logiką handlera webhooka. Wywołaj webhook raz ze Stripe, a potem odtwarzaj go z inspektora ile razy potrzebujesz, poprawiając kod. Nie trzeba wielokrotnie tworzyć prawdziwych transakcji testowych.
Dostęp do inspektora przez API:
# Wylistuj ostatnie żądania programowo
curl http://localhost:4040/api/requests/http | jq '.requests[0].response.status'
🧪 Automatyczne testowanie webhooków bez ngrok
ngrok to właściwe narzędzie do interaktywnego debugowania. W testach automatycznych używaj wbudowanego testowania HTTP w Laravel.
// tests/Feature/Webhook/StripeWebhookTest.php
<?php
declare(strict_types=1);
use App\Models\Order;
use Illuminate\Support\Facades\Queue;
use App\Jobs\HandleStripePaymentSucceeded;
it('przetwarza prawidłowy webhook payment_intent.succeeded od Stripe', function () {
Queue::fake();
$order = Order::factory()->create(['status' => 'pending']);
$payload = json_encode([
'type' => 'payment_intent.succeeded',
'data' => [
'object' => [
'id' => 'pi_test_123',
'metadata' => ['order_id' => $order->id],
'amount' => 9900,
'currency' => 'pln',
],
],
]);
// Buduj prawidłową sygnaturę Stripe dla testu
$secret = config('services.stripe.webhook_secret');
$timestamp = time();
$signature = hash_hmac('sha256', "{$timestamp}.{$payload}", $secret);
$header = "t={$timestamp},v1={$signature}";
$this->postJson('/webhook/stripe', json_decode($payload, true), [
'Stripe-Signature' => $header,
])->assertOk();
Queue::assertDispatched(HandleStripePaymentSucceeded::class);
});
it('odrzuca webhook z nieprawidłową sygnaturą', function () {
$this->postJson('/webhook/stripe', ['type' => 'payment_intent.succeeded'], [
'Stripe-Signature' => 't=000,v1=invalidsig',
])->assertStatus(400);
});
Ten test działa bez ngrok, bez dostępu do sieci, bez prawdziwego konta Stripe. Używaj ngrok za pierwszym razem, gdy integrujesz nowy webhook — gdy już rozumiesz strukturę payloadu, napisz test i nie musisz już tunelu do regresji.
🚀 Inne przypadki użycia, które warto znać
Aplikacje mobilne: React Native lub Flutter muszą trafić do prawdziwego API. http://localhost:8000 nie działa na fizycznym urządzeniu ani symulatorze w innej sieci. Wskaż aplikację na URL ngrok.
Callbacki OAuth: Dostawcy OAuth (GitHub, Google) wymagają zarejestrowanego URI przekierowania. Podczas developmentu zarejestruj domenę ngrok jako adres callbacka i zaktualizuj go raz przy konfiguracji statycznej domeny.
Demonstracja funkcji klientowi bez deployu: Uruchom ngrok, wyślij link, zaprezentuj funkcję. URL działa dla każdego.
Testowanie funkcji wymagających HTTPS lokalnie: Niektóre API przeglądarki (geolokalizacja, kamera, Service Workers) wymagają HTTPS. ngrok zapewnia HTTPS od razu, nawet gdy lokalny serwer działa na HTTP.
Inspekcja dowolnego ruchu HTTP: ngrok działa z każdym lokalnym serwerem — nie tylko PHP. Używaj go z python -m http.server, Node.js lub innym lokalnym serwisem.
✅ Podsumowanie
- ngrok tworzy zaszyfrowany publiczny tunel do lokalnego serwera jedną komendą — bez DNS, SSL ani reguł firewalla
- Używaj statycznej domeny (bezpłatnej), żeby URL-e webhooków nie zmieniały się między restartami tunelu
- Do webhooków płatności (Stripe, P24, PayU, Tpay) zarejestruj URL ngrok w panelu dostawcy raz i testuj cały przepływ end-to-end lokalnie — łącznie z prawdziwymi sygnaturami
- Zawsze weryfikuj sygnatury webhooków — ngrok to publiczny endpoint
- Inspektor ngrok pod
http://localhost:4040pozwala odtwarzać webhooki bez ponownego wywoływania ich od strony dostawcy płatności - Do CI i testów regresji mockuj webhook podpisanym payloadem w teście feature — ngrok zostaw do interaktywnej pracy integracyjnej
- Flaga
--host-headerjest wymagana przy tunelowaniu do wirtualnych hostów Herd lub Valet
Obserwuj mnie na LinkedIn po więcej wskazówek o Laravel! Straciłeś kiedyś godziny debugując webhook, który działał tylko na produkcji? ngrok by to zaoszczędził. Daj znać w komentarzach!