All articles
HTTPSmkcertlocal developmentlocalhost tunneling

Localhost HTTPS: mkcert vs Tunnel, Honestly Compared

Two tools, one problem: your dev server needs to be reachable over HTTPS. mkcert gives you a locally-trusted TLS certificate so https://localhost:3000 just works. A tunnel gives you a public HTTPS URL backed by a real certificate from a real CA. They're not competitors — they solve different problems — but people keep treating them as if they are.

The short answer

  • You need HTTPS only on your own machine, for your own browser? Use mkcert.
  • You need an external service (Stripe, GitHub, Auth0, a phone) to reach your dev server? Use a tunnel.
  • Both? Run both. They don't conflict.

That's the whole article in three bullets. The rest is just the why.

mkcert: locally-trusted certificates in 30 seconds

mkcert installs a local certificate authority into your operating system's trust store and issues certificates from it. Your browser sees the cert as trusted because the CA is trusted. No your connection is not private warnings, no --ignore-certificate-errors flags, no rolling your own self-signed certs.

# once per machine
mkcert -install

# once per project
mkcert localhost 127.0.0.1

# you now have localhost.pem and localhost-key.pem

Wire them into your dev server (Vite, Next.js, Express, Django runserver_plus, all support TLS args) and you've got https://localhost:3000 with a real certificate.

The catch: only your machine trusts that CA. Your teammate's browser will warn. Your phone won't trust it without manually installing the CA root.

Tunnel: real HTTPS, real public URL

A localhost tunnel forwards traffic from a public HTTPS URL to your local port. The certificate is issued by a real CA (Let's Encrypt or similar) and trusted by every browser on every device.

npx portpreview 3000

The tunnel handles TLS termination at the cloud gateway. Your local server can stay on plain HTTP — the public URL is HTTPS, and that's the surface every external service touches.

Side by side

NeedmkcertTunnel
HTTPS in your local browserYesYes (via the public URL)
Service worker / secure cookie testing in your browserYesYes
External provider can reach youNoYes
Phone in your hand can reach youNo (without installing CA)Yes
OAuth provider accepts the URLSometimes (Google: yes; many: no)Yes
Stripe / GitHub / Twilio can webhook to itNoYes
Setup time30 seconds per machineOne command per session
Works offlineYesNo
Survives airplane modeYesNo

Where people get this wrong

Trying to use mkcert for webhook testing

Stripe can't trust your machine's local CA. It doesn't matter how trusted the cert looks in your browser — Stripe is in a different network. You need a tunnel for any inbound traffic from the public internet.

Using a tunnel for solo browser-only HTTPS

If you just want service workers to work or a secure cookie to set in your own browser, mkcert is faster and works offline. Don't burn a tunnel session for what a certificate file solves.

Picking one tool and forcing the other use case through it

Running both is fine. Most of our setup uses mkcert for daily browser-side work and a tunnel when we're testing webhooks or OAuth callbacks. They cohabit in package.json without conflict.

What about Caddy or nginx?

Yes, you can run Caddy with automatic HTTPS in front of your dev server, and that works too — it's essentially "mkcert with extra steps and a reverse proxy". For most local dev, mkcert is simpler. For more elaborate routing with multiple local services, Caddy earns its keep.

Our actual setup

One repo we work in has mkcert in the dev script for localhost-side HTTPS, and a separate tunnel npm script that runs portpreview when somebody needs webhooks or mobile testing. The tunnel URL gets passed via env var so OAuth redirect URIs are easy to swap. It took 20 minutes to wire up and we've never thought about local TLS again.

For OAuth-specific tunnel setup, see how to test OAuth callbacks locally. For sharing previews with teammates or designers, sharing your local dev server. Join the PortPreview waitlist for the tunnel side of this setup.

Frequently asked questions

Should I use mkcert or a tunnel for local HTTPS?
Both, for different things. mkcert gives your own browser a trusted certificate on localhost and works offline. A tunnel gives you a public HTTPS URL that external services and other devices can reach. They don't conflict — most teams use both.
Can mkcert handle webhook testing from Stripe or GitHub?
No. mkcert certificates are trusted only on the machine that installed the CA. External webhook providers can't reach localhost regardless of how trusted the certificate is in your local browser. Use a tunnel for any inbound public traffic.
Does mkcert work for OAuth localhost callbacks?
Some providers (Google, certain dev tiers of Auth0) accept localhost over HTTP or HTTPS for development. Many don't. mkcert gives you HTTPS for those that do; for the strict ones, a tunnel with a public HTTPS URL is required.