SSL Certificates for Localhost and Development

How to set up SSL certificates for localhost and local development environments. Covers mkcert, self-signed certificates, and why HTTPS matters in development.

Modern web development increasingly requires HTTPS, even on localhost. Browser APIs like Service Workers, the Geolocation API, Web Bluetooth, and the Clipboard API only work in secure contexts. If you are developing a Progressive Web App, testing OAuth flows, or working with any security-sensitive browser feature, you need HTTPS on your local development server.

But public certificate authorities cannot issue certificates for localhost or private IP addresses. This guide covers the practical approaches to getting HTTPS working in your local environment. For background on SSL certificates in general, see What Is an SSL Certificate?.

Why You Need HTTPS in Development

Secure Context Requirements

Browsers restrict many APIs to "secure contexts," which means either HTTPS or localhost over HTTP. The catch is that localhost over HTTP is only treated as secure for the page's origin. If your app loads resources from other local domains or uses custom hostnames (like myapp.local or dev.myapp.com), those are not treated as secure without HTTPS.

APIs that require secure contexts include:

  • Service Workers (required for PWAs and push notifications)
  • Geolocation API
  • Web Bluetooth API
  • Web USB API
  • Notifications API
  • Clipboard API (write access)
  • Payment Request API
  • Web Authentication API (WebAuthn)

Testing Production-Like Configurations

If your production site uses HTTPS (and it should), testing with HTTP in development can mask bugs related to:

  • Mixed content issues (HTTPS page loading HTTP resources)
  • Cookie security flags (Secure, SameSite)
  • HSTS (HTTP Strict Transport Security) behavior
  • Content Security Policy directives
  • Redirect behavior between HTTP and HTTPS

OAuth and Third-Party Integrations

Many OAuth providers (Google, GitHub, Facebook) require HTTPS callback URLs, even for development. While some allow http://localhost as an exception, others do not, and custom development domains always require HTTPS.

Method 1: mkcert (Recommended)

mkcert is a simple tool that creates locally-trusted development certificates. It works by creating a local certificate authority, installing it in your system's trust store, and then issuing certificates signed by that CA. Browsers trust these certificates without any warnings.

Installation

# macOS (Homebrew)
brew install mkcert

# Windows (Chocolatey)
choco install mkcert

# Linux (various package managers)
# Check the mkcert GitHub repository for your distribution

Setup

First, install the local CA:

mkcert -install

This creates a root certificate and adds it to your system's trust store (and Firefox's trust store, if Firefox is installed). You only need to do this once.

Creating Certificates

# Certificate for localhost
mkcert localhost

# Certificate for localhost and custom domains
mkcert localhost 127.0.0.1 ::1 myapp.local dev.myapp.com

# Wildcard certificate for a custom domain
mkcert "*.myapp.local" myapp.local localhost

mkcert creates two files: a certificate file (localhost.pem or localhost+N.pem) and a key file (localhost-key.pem or localhost+N-key.pem).

Using mkcert Certificates

Node.js / Express

const https = require('https');
const fs = require('fs');
const express = require('express');

const app = express();

const options = {
  key: fs.readFileSync('./localhost-key.pem'),
  cert: fs.readFileSync('./localhost.pem'),
};

https.createServer(options, app).listen(3000, () => {
  console.log('HTTPS server running on https://localhost:3000');
});

Vite

// vite.config.js
import fs from 'fs';

export default {
  server: {
    https: {
      key: fs.readFileSync('./localhost-key.pem'),
      cert: fs.readFileSync('./localhost.pem'),
    },
  },
};

Next.js

Next.js 13.5+ supports HTTPS in development natively:

next dev --experimental-https

This uses mkcert internally if it is installed. You can also configure it manually through a custom server.

Nginx (Local Reverse Proxy)

server {
    listen 443 ssl;
    server_name localhost;

    ssl_certificate /path/to/localhost.pem;
    ssl_certificate_key /path/to/localhost-key.pem;

    location / {
        proxy_pass http://localhost:3000;
    }
}

Never commit mkcert certificates or keys

mkcert's root CA private key is stored on your machine. If someone obtains it, they can create certificates that your system trusts for any domain. Never share or commit the CA files. The generated certificate and key files are safe to add to .gitignore but should not be committed either, since they are machine-specific.

Method 2: Self-Signed Certificates

If you cannot install mkcert, you can create self-signed certificates using OpenSSL. The downside is that browsers will show security warnings because the certificate is not signed by a trusted CA.

Generating a Self-Signed Certificate

openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout localhost.key \
  -out localhost.crt \
  -subj "/CN=localhost" \
  -addext "subjectAltName=DNS:localhost,IP:127.0.0.1,IP:::1"

This creates a certificate valid for 365 days, covering localhost, 127.0.0.1, and ::1 (IPv6 loopback).

Trusting the Certificate

To avoid browser warnings, you can manually trust the self-signed certificate:

macOS: Double-click the .crt file to add it to Keychain Access. Find it in the login keychain, double-click it, expand "Trust," and set "When using this certificate" to "Always Trust."

Windows: Double-click the .crt file, click "Install Certificate," choose "Local Machine," and place it in the "Trusted Root Certification Authorities" store.

Linux: Copy the .crt file to /usr/local/share/ca-certificates/ and run sudo update-ca-certificates.

Firefox: Firefox uses its own certificate store. Go to Settings > Privacy & Security > Certificates > View Certificates > Authorities > Import.

This approach works but is more manual than mkcert and needs to be repeated for each certificate.

For more on self-signed certificates, see Self-Signed Certificates.

Method 3: Tunneling Services

Services like ngrok, Cloudflare Tunnel, and localtunnel expose your local server through a public HTTPS URL. You do not need any certificate setup on your machine.

# ngrok
ngrok http 3000
# Provides: https://abc123.ngrok.io

# Cloudflare Tunnel
cloudflared tunnel --url http://localhost:3000
# Provides: https://random-name.trycloudflare.com

These services handle HTTPS termination on their infrastructure and proxy traffic to your local server over HTTP.

Tunneling services are useful for:

  • Sharing your local development with team members or clients.
  • Testing webhooks from third-party services that require HTTPS callbacks.
  • Quick demos without certificate setup.

Downsides include latency (traffic routes through the service's servers), potential rate limits, and dependency on an external service.

Method 4: Framework-Specific Solutions

Create React App

HTTPS=true npm start

This starts the development server with a self-signed certificate. Browsers will show a warning, but you can proceed through it.

Webpack Dev Server

// webpack.config.js
module.exports = {
  devServer: {
    https: true,
    // Or with custom certificates:
    https: {
      key: fs.readFileSync('./localhost-key.pem'),
      cert: fs.readFileSync('./localhost.pem'),
    },
  },
};

Django

pip install django-sslserver
python manage.py runsslserver

Or use django-extensions with Werkzeug:

pip install django-extensions Werkzeug pyOpenSSL
python manage.py runserver_plus --cert-file localhost.crt --key-file localhost.key

Ruby on Rails

rails server -b 'ssl://localhost:3000?key=localhost-key.pem&cert=localhost.pem'

Custom Local Domains

Using localhost for everything becomes inconvenient when you work on multiple projects or need subdomains. You can use custom local domains by editing your hosts file.

Setting Up Custom Domains

Add entries to your hosts file (/etc/hosts on macOS/Linux, C:\Windows\System32\drivers\etc\hosts on Windows):

127.0.0.1    myapp.local
127.0.0.1    api.myapp.local
127.0.0.1    admin.myapp.local

Then generate certificates for these domains:

mkcert myapp.local "*.myapp.local" localhost 127.0.0.1

Alternatively, use .localhost domains. Browsers treat any subdomain of localhost as a secure context without needing a certificate, and modern systems resolve *.localhost to 127.0.0.1 automatically. This means myapp.localhost works in most environments without any hosts file modification or certificate setup.

Security Considerations

Do not use development certificates in production. mkcert certificates and self-signed certificates are for development only. They are not publicly trusted and should never be deployed to production servers.

Protect your mkcert root CA. The mkcert root CA key can create trusted certificates for any domain on your machine. If compromised, an attacker could create certificates for google.com or your-bank.com that your system trusts. Keep it secure and never share it.

Do not skip certificate validation in code. It is tempting to disable SSL certificate verification in HTTP clients during development (e.g., verify=False in Python requests). This masks real certificate issues and can accidentally reach production. Use properly trusted certificates instead.

References

  1. Filippo Valsorda, "mkcert," GitHub. https://github.com/FiloSottile/mkcert
  2. W3C, "Secure Contexts," https://w3c.github.io/webappsec-secure-contexts/
  3. Mozilla, "Secure contexts," MDN Web Docs. https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts

Monitor your production SSL certificates

Development certificates do not expire (or at least do not matter if they do). Production certificates absolutely do. SSL Certificate Expiry monitors your live certificates and alerts you before they expire.

Try SSL Certificate Expiry