Home/ Blog/ Tutorial

HTTPS for localhost in 2026 — six ways compared.

Browsers keep tightening the screws on HTTP. Service workers, cookies with SameSite=None, WebAuthn, clipboard write, camera access — all require a secure context, which in practice means HTTPS. Here are the six real options, ranked by how much friction each adds.

Why you need HTTPS locally

The list of web features that refuse to work over plain HTTP keeps growing:

  • Service Workers — required for PWAs, offline flows, push notifications.
  • SameSite=None cookies — you need Secure too, which requires HTTPS.
  • WebAuthn / passkeys — secure context only.
  • navigator.clipboard.writeText — silently fails over HTTP.
  • Camera, microphone, geolocation — prompts refuse without HTTPS.
  • Third-party OAuth callbacks — Google, GitHub, etc. reject HTTP redirect URLs.
  • CORS preflight edge cases — mixed-content rules trip on HTTP origins.

localhost itself is a special case — browsers treat it as a secure context even over HTTP, for historical convenience. But the moment you use a custom domain (which you need for cookies scoped to .company.com or for testing the actual prod URL pattern), the special case evaporates.

Six ways, ranked

1 — A tunneling service (easiest)

Run a tunnel. You get a real HTTPS URL signed by a public CA, no cert work on your end. This is the answer when you need HTTPS because someone else needs to reach you — webhooks, clients, real phones.

tunnel21 http 3000
# → https://your-subdomain.21tunnel.app (Let's Encrypt, real cert)

ngrok http 3000
# → https://xxxx.ngrok-free.app

cloudflared tunnel --url http://localhost:3000
# → https://weird-animal.trycloudflare.com

Pros: zero cert work, real trusted CA, HTTPS from first second. Works for mobile-device testing (real iPhones get the proper padlock).

Cons: only solves the public case. If you're testing locally on your own browser without needing external reach, this is overkill. URL changes on free tiers unless you reserve a subdomain.

2 — mkcert (best for browser testing)

mkcert creates a local Certificate Authority, installs it in your OS trust store, and issues certs for anything you want. Your browser trusts them instantly; no warnings, no tunneling.

brew install mkcert            # macOS
mkcert -install                 # one-time: install local root

mkcert localhost 127.0.0.1 ::1 app.local
# → writes localhost+3.pem + localhost+3-key.pem

Point your dev server at the cert files:

# Node HTTPS server
import https from "https";
import fs from "fs";
https.createServer({
  key:  fs.readFileSync("localhost+3-key.pem"),
  cert: fs.readFileSync("localhost+3.pem"),
}, app).listen(3000);

Pros: no browser warnings, works offline, no external dependency. The gold standard for local dev HTTPS.

Cons: only your machines trust the cert. Teammates must install their own CA. Phones won't trust it without installing the mkcert root on the device, which is fiddly on iOS.

3 — Caddy, local mode

Caddy is a reverse proxy that auto-provisions certs. Point it at localhost:3000, give it a domain, and it handles TLS termination.

# Caddyfile
localhost {
  reverse_proxy localhost:3000
}

caddy run
# Now https://localhost/ proxies to your dev server with a valid cert.

For custom domains (dev.mycompany.local), Caddy runs its own local CA, same pattern as mkcert — but as a daemon, with auto-renewal, which is more ergonomic for long-lived dev setups.

Pros: one tool does TLS + reverse proxy + virtual hosts. Config is tiny.

Cons: you're running a separate process. Overkill for “I just want HTTPS on port 3000 for five minutes.”

4 — Let's Encrypt via DNS-01 challenge

Want a real publicly-valid cert for a domain that points at 127.0.0.1? Use Let's Encrypt's DNS-01 challenge. You prove domain ownership by adding a TXT record; you never expose any public port.

# With certbot + cloudflare plugin
certbot certonly --dns-cloudflare \
  --dns-cloudflare-credentials ~/cloudflare.ini \
  -d dev.yourdomain.com

# Then pin dev.yourdomain.com → 127.0.0.1 in /etc/hosts

Pros: real cert, trusted by every browser and every OS, without anyone being able to reach your box from outside. Works for teammates (if they also put the domain in their /etc/hosts).

Cons: you need a domain you own and DNS API access. Cert expires every 90 days, renewal is your problem.

5 — Framework dev certs

Most modern frameworks ship a --https flag that generates a self-signed cert for you. Convenient but the browser warning gets old.

# Vite
vite --https

# Next.js (experimental flag)
next dev --experimental-https

# Rails (with some coaxing)
rails server -b "ssl://localhost:3000?cert=./cert.pem&key=./key.pem"

# .NET / dotnet
dotnet dev-certs https --trust

.NET is the exception — dotnet dev-certs installs a trusted cert in your OS store, essentially doing what mkcert does automatically. The others generate untrusted self-signed certs you'll click past.

Pros: one flag. No extra tools. Works for quick local testing.

Cons: browser shows a warning every time on most frameworks. Third-party OAuth callbacks usually refuse self-signed destinations.

6 — Old-school self-signed

The manual version of option 5.

openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem \
  -sha256 -days 365 -nodes -subj "/CN=localhost"

Pros: works everywhere, no tools to install.

Cons: every browser complains, every client complains, every teammate has to click past the same warning. In 2026 there's essentially no reason to do this instead of mkcert.

How to pick one

Decision tree:

  • Do external systems need to reach you? (webhooks, mobile devices, OAuth callbacks, teammates over the internet) → tunneling service.
  • Is this just your machine, your browser?mkcert. It's the cleanest dev-only experience.
  • Your whole team needs to hit the same dev domain, on your office LAN?Let's Encrypt DNS-01 for a publicly-valid cert + a shared /etc/hosts entry. Everyone gets browser-trusted HTTPS, your box stays internal-only.
  • You already have reverse-proxy needs (multiple services on different subdomains) → Caddy. TLS comes along for free.
  • You're sanity-checking one feature and will click past the warning → framework's --https flag.

Three gotchas that waste hours

Every one of these has eaten an afternoon of mine.

  1. HSTS caching. If you ever visited https://app.mycompany.com in production, your browser may have cached an HSTS policy that forbids HTTP to that domain. When you try to test locally against http://app.mycompany.com, the browser upgrades it to HTTPS silently and the connection fails. Fix: clear HSTS for the domain in chrome://net-internals/#hsts.
  2. Certificate hostnames. A cert for localhost does not cover 127.0.0.1, and a cert for example.com does not cover www.example.com. Always list every name you'll access in the SAN. With mkcert, this means mkcert localhost 127.0.0.1 ::1 app.local — all four.
  3. Mixed content. An HTTPS page that tries to load http:// resources is silently broken in the browser dev tools. A common flavor: your dev server serves HTTPS, but the API client hardcodes http://localhost:3001 somewhere. The fetch fails with a CORS-ish error that actually has nothing to do with CORS.

Short version: if a real browser on another device needs to reach you, use a tunneling service. If it's just your own Chrome talking to your own laptop, use mkcert. Everything else is a special case.

For tunneling: 21tunnel serves HTTPS by default on every tunnel (Let's Encrypt on our side), free on Hobby tier, custom domain on signup. Compare to other tunneling tools in our roundup.