Modern web APIs (service workers, geolocation, clipboard, camera) require HTTPS — even during development. This guide covers how to get trusted HTTPS on localhost without browser warnings.
Why you can’t use Let’s Encrypt for localhost
Let’s Encrypt validates domain ownership by checking a file on your server or a DNS record. localhost isn’t a real domain — it resolves to 127.0.0.1 on your machine only. No one can verify you “own” localhost, so no public CA will issue a certificate for it.
Your options for localhost HTTPS:
- mkcert — creates locally-trusted certificates (recommended)
- Self-signed certificate — works but shows browser warnings
- Tunneling service — ngrok, Cloudflare Tunnel (real domain, real cert)
Option 1: mkcert (recommended)
mkcert creates a local Certificate Authority on your machine, adds it to your system trust store, and issues certificates signed by that CA. Browsers trust these certificates with no warnings.
Install mkcert
# macOS
brew install mkcert
# Linux (requires libnss3-tools for Firefox)
sudo apt install libnss3-tools
brew install mkcert # or download from GitHub releases
# Windows
choco install mkcert
# or: scoop install mkcert
Set up the local CA
mkcert -install
# Output: Created a new local CA
# The local CA is now installed in the system trust store
This adds a root certificate to your system and browser trust stores. Do this once — it works for all future certificates.
Generate certificates
# For localhost
mkcert localhost 127.0.0.1 ::1
# For a custom local domain
mkcert myapp.test "*.myapp.test" localhost 127.0.0.1
# Output:
# Created a new certificate valid for the following names:
# - "localhost"
# - "127.0.0.1"
# - "::1"
# The certificate is at "./localhost+2.pem" and the key at "./localhost+2-key.pem"
Use with your dev server
Node.js/Express:
const https = require('https');
const fs = require('fs');
const app = require('./app');
https.createServer({
key: fs.readFileSync('./localhost+2-key.pem'),
cert: fs.readFileSync('./localhost+2.pem'),
}, app).listen(3000);
Nginx (local):
server {
listen 443 ssl;
server_name localhost;
ssl_certificate /path/to/localhost+2.pem;
ssl_certificate_key /path/to/localhost+2-key.pem;
# ...
}
Vite / webpack dev server:
// vite.config.js
import fs from 'fs';
export default {
server: {
https: {
key: fs.readFileSync('./localhost+2-key.pem'),
cert: fs.readFileSync('./localhost+2.pem'),
},
},
};
Security warning
Never share the mkcert root CA key (rootCA-key.pem in mkcert -CAROOT). Anyone who has it can create trusted certificates for any domain on your machine — essentially a local man-in-the-middle capability.
mkcert is for development only. For production, use Let’s Encrypt.
Option 2: Self-signed certificate
If you can’t install mkcert, generate a self-signed certificate with OpenSSL:
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 \
-keyout localhost-key.pem -out localhost-cert.pem \
-days 365 -nodes -subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
Downside: Browsers show a security warning because self-signed certificates aren’t trusted by any CA. You’ll need to click through the warning each time (or add an exception).
Option 3: Tunneling (real domain + real certificate)
Use a tunneling service to expose your local server with a real domain and certificate:
# ngrok
ngrok http 3000
# Gives you: https://abc123.ngrok.io
# Cloudflare Tunnel
cloudflared tunnel --url http://localhost:3000
# Gives you: https://random-name.trycloudflare.com
When this makes sense: testing webhooks, sharing with a teammate, testing OAuth callbacks that require HTTPS.
mkcert vs self-signed vs tunneling
| mkcert | Self-signed | Tunneling | |
|---|---|---|---|
| Browser warnings | None | Yes (every session) | None |
| Setup effort | Low (one install) | Low (one command) | Low |
| Works offline | ✅ | ✅ | ❌ |
| Performance | Local speed | Local speed | Network latency |
| Custom domains | ✅ (*.test, etc.) | ✅ | Provider-assigned |
| Best for | Daily development | Quick one-off | Webhooks, sharing |
Frequently asked questions
Can I use the same mkcert certificate across my team?
Don’t share the root CA key. Each developer should run mkcert -install on their own machine to create their own local CA. You can share the generated certificates (the .pem files), but each person needs the CA installed to trust them.
Does mkcert work with Docker?
Yes. Mount the certificate files into the container and reference them in the server config. The container itself doesn’t need mkcert installed — just the .pem files. Your host machine (where the browser runs) needs the mkcert CA installed.
Which local domain should I use?
Use .test (e.g., myapp.test) — it’s reserved by IETF for testing and will never conflict with a real domain. Avoid .dev (owned by Google) and .local (used by mDNS). Add an entry to /etc/hosts to point your .test domain to 127.0.0.1.
How do I switch from localhost HTTPS to production HTTPS?
They’re completely separate. Your localhost certificate (mkcert/self-signed) stays on your dev machine. For production, get a real certificate from Let’s Encrypt via GetHTTPS. Your code should read cert paths from environment variables so the switch is just changing a config.