Before we start, it's important to note that SSL certificates are typically issued for domain names, not IP addresses or localhost.
To serve https://localhost/ in a way that modern browsers accept (no red warning), you need two things:
- A certificate whose Subject Alternative Name (SAN) includes
localhost(and often127.0.0.1and::1). - A way for your OS/browser to trust the issuing CA (or the certificate itself).
The most reliable developer workflow is to create a local, trusted CA and issue a cert for localhost.
Option A (recommended): mkcert (trusted HTTPS with minimal friction)
mkcert creates a local certificate authority (CA), installs it into your system trust store, then issues certs for your local domains.
1) Install mkcert
-
macOS (Homebrew):
brew install mkcert -
Windows (Chocolatey):
choco install mkcert -
Windows (Scoop):
scoop bucket add extras && scoop install mkcert -
Linux: install from your distro packages or the project releases; also ensure
nsstooling is installed for Firefox trust integration (varies by distro).
2) Create and trust a local CA
mkcert -install
3) Generate a cert for localhost
Run this where you want the files:
mkcert localhost 127.0.0.1 ::1
This produces files like:
-
localhost+2.pem(certificate) -
localhost+2-key.pem(private key)
4) Configure your local server to use the cert
Node.js (Express or plain HTTPS)
import https from "https";
import fs from "fs";
import app from "./app.js"; // your Express app
https.createServer(
{
key: fs.readFileSync("./localhost+2-key.pem"),
cert: fs.readFileSync("./localhost+2.pem"),
},
app
).listen(443, () => console.log("HTTPS on https://localhost"));
If you don’t want root/admin privileges for port 443, use 8443 and browse https://localhost:8443/.
Python (Flask)
from flask import Flask
app = Flask(__name__)
@app.get("/")
def hello():
return "hello"
if __name__ == "__main__":
app.run(ssl_context=("localhost+2.pem", "localhost+2-key.pem"), port=8443)
Uvicorn / FastAPI
uvicorn main:app --host 127.0.0.1 --port 8443 \
--ssl-keyfile localhost+2-key.pem --ssl-certfile localhost+2.pem
Nginx (reverse proxy)
server {
listen 443 ssl;
server_name localhost;
ssl_certificate /path/to/localhost+2.pem;
ssl_certificate_key /path/to/localhost+2-key.pem;
location / {
proxy_pass http://127.0.0.1:3000;
}
}
Option B: Caddy (very convenient local HTTPS)
Caddy can generate and manage a local CA automatically and serve HTTPS locally with minimal configuration.
Example Caddyfile:
localhost {
reverse_proxy 127.0.0.1:3000
}
Run Caddy and it will handle certs for you (you may need to trust Caddy’s local CA depending on platform prompts).
Option C: OpenSSL self-signed cert (works, but browsers will warn)
This is quick, but you’ll typically see a browser warning unless you manually trust the cert/CA.
Modern browsers require SAN, so use an OpenSSL config:
- Create
localhost.cnf:
[req]
default_bits = 2048
prompt = no
default_md = sha256
x509_extensions = v3_req
distinguished_name = dn
[dn]
CN = localhost
[v3_req]
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
IP.1 = 127.0.0.1
IP.2 = ::1
- Generate cert + key:
openssl req -x509 -nodes -days 825 -newkey rsa:2048 \
-keyout localhost.key -out localhost.crt -config localhost.cnf
Then point your server to localhost.crt / localhost.key.
If you want no warnings, you must add trust manually (varies by OS/browser) or create a local CA and trust it (which is effectively what mkcert automates).
Posted on 02 January 2026
-
“Certificate not valid for this host”: your cert is missing SAN entries for
localhostor127.0.0.1. -
Chrome/Edge caching/HSTS: if you previously forced HTTPS or hit a bad cert, you may need to clear HSTS for
localhost(or use a different hostname likemyapp.local). -
Firefox trust differs: on some systems Firefox uses its own store;
mkcert -installusually handles this if the right NSS tooling is present. - Port 443 permissions: on macOS/Linux, binding to 443 may require admin/root; use 8443 or a reverse proxy that listens on 443.
- Containers: if your server runs in Docker, mount the cert/key into the container and reference those paths; trust is still on the host/browser side.
Practical recommendation
If your goal is simply “make https://localhost/ work cleanly in a browser,” use mkcert and run your dev server on 8443 (or use a reverse proxy to 443). It’s the fastest path to a trusted cert with correct SANs.
If you tell me what stack you’re using (Node/Express, Vite, Next.js, Django, Nginx, IIS, Docker, etc.), I can give the exact config snippet for that specific server.
Top comments (0)