# Setting Up HTTPS for Local Development

> Follow this guide to configure HTTPS for local development to simulate secure environments for testing purposes.

- **Author:** Kubilay Tunca
- **Published:** 2024-08-22
- **Category:** For Developers
- **Tags:** HTTPS, Local Development, Secure Coding
- **Canonical URL:** https://cyber-security-in-plain-english.com/post/developers/technologies/adding-https-to-your-local-dev-setup

---

# Introduction

In modern web development, HTTPS is essential for securing data in transit, protecting user privacy, and ensuring trust. While HTTPS is standard in production environments, setting it up for local development often gets overlooked. By configuring HTTPS locally, developers can simulate secure environments, test SSL-specific features, and catch potential issues early.

This guide provides a step-by-step approach to setting up HTTPS for local development using self-signed certificates, tools, and best practices.

## Why Use HTTPS in Local Development?

### 1. **Simulating Real-World Scenarios**

- Testing applications in a secure environment ensures that they work seamlessly in production.

### 2. **Catching SSL/TLS Issues Early**

- Identifying HTTPS-related problems during development saves time and reduces deployment risks.

### 3. **Enabling Secure APIs**

- Many APIs require HTTPS, even in development, to accept requests.

### 4. **Compliance Testing**

- Some features, like browser-based geolocation, require HTTPS to function.

## Prerequisites

### Tools and Libraries Needed:

1. **OpenSSL**: For generating self-signed certificates.
2. **Node.js or Python**: To run local servers with HTTPS.
3. **A Web Browser**: For testing HTTPS configurations.

## Generating a Self-Signed Certificate

### Step 1: Install OpenSSL

Install OpenSSL if it’s not already available on your system.

- **For macOS**: OpenSSL is included. If not, use Homebrew: `brew install openssl`.
- **For Windows**: Download the installer from [openssl.org](https://www.openssl.org/).
- **For Linux**: Use your package manager, e.g., `sudo apt-get install openssl`.

### Step 2: Create a Certificate and Private Key

Run the following OpenSSL commands to generate a certificate and private key:

```bash
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout local-dev.key -out local-dev.crt
```

#### Explanation:

- `-x509`: Creates a self-signed certificate.
- `-nodes`: Prevents encrypting the private key.
- `-days 365`: Specifies the certificate's validity period.
- `-newkey rsa:2048`: Generates a new RSA key with 2048 bits.

When prompted, provide the following details (use dummy values for local development):

- Country Name: `US`
- State: `California`
- Locality: `San Francisco`
- Organization: `LocalDev`
- Common Name: `localhost`

This generates two files:

- `local-dev.key`: The private key.
- `local-dev.crt`: The self-signed certificate.

### Step 3: Configure the Development Server

#### Using Node.js

1. Install dependencies:

```bash
npm install express https fs
```

2. Create an HTTPS server:

```javascript
const https = require('https')
const fs = require('fs')
const express = require('express')

const app = express()
const options = {
	key: fs.readFileSync('local-dev.key'),
	cert: fs.readFileSync('local-dev.crt')
}

app.get('/', (req, res) => {
	res.send('Hello, HTTPS World!')
})

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

3. Run the server:

```bash
node server.js
```

Visit `https://localhost:3000` in your browser. You’ll see a security warning because the certificate isn’t signed by a trusted authority.

#### Using Python

1. Create a self-signed certificate with OpenSSL.
2. Start a Python HTTPS server:

```bash
python -m http.server --bind localhost --certfile local-dev.crt --keyfile local-dev.key 8443
```

Visit `https://localhost:8443` in your browser.

## Handling Self-Signed Certificate Warnings

### Option 1: Add the Certificate to Trusted Authorities

- Add the `local-dev.crt` to your operating system’s trusted root certificates.
- **macOS**: Use Keychain Access.
- **Windows**: Use the Certificates MMC snap-in.
- **Linux**: Add it to `/etc/ssl/certs/`.

### Option 2: Bypass Warnings (Development Only)

- Manually accept the certificate warning in your browser.

## Best Practices for HTTPS in Local Development

1. **Automate Certificate Generation**

- Use tools like `mkcert` to simplify the process of creating local certificates.

2. **Enable HSTS**

- Simulate production behavior by enabling HTTP Strict Transport Security (HSTS).

3. **Redirect HTTP to HTTPS**

- Use middleware to redirect HTTP requests to HTTPS.

**Example (Node.js):**

```javascript
app.use((req, res, next) => {
	if (!req.secure) {
		return res.redirect(`https://${req.headers.host}${req.url}`)
	}
	next()
})
```

4. **Use Secure Cookies**

- Mark cookies as `Secure` to ensure they are only sent over HTTPS.

5. **Monitor for SSL/TLS Issues**

- Use browser developer tools to identify insecure resources and mixed content.

## Troubleshooting Common Issues

### 1. **Browser Shows “Connection Not Secure”**

- Ensure the self-signed certificate is added to trusted authorities.

### 2. **Server Fails to Start**

- Verify the paths to `local-dev.key` and `local-dev.crt`.
- Check if the port (e.g., `3000` or `8443`) is already in use.

### 3. **Mixed Content Warnings**

- Ensure all assets (e.g., CSS, JS, images) are served over HTTPS.

## Using mkcert: The Modern Approach to Local HTTPS

While OpenSSL works, it produces certificates that your browser does not automatically trust—resulting in the intimidating "Your connection is not private" warning every time you open your dev server. `mkcert` eliminates this entirely by creating a local certificate authority (CA) and registering it with your operating system's trust store. Chrome, Firefox, Safari, and every OS-level HTTP client then trust it without any manual override.

This makes `mkcert` the recommended tool for local HTTPS in 2024 and beyond. It is free, open source, cross-platform, and requires zero configuration.

### Installing mkcert

**macOS:**

```bash
brew install mkcert
brew install nss  # Required for Firefox support
```

**Windows (Chocolatey):**

```bash
choco install mkcert
```

**Windows (Scoop):**

```bash
scoop bucket add extras
scoop install mkcert
```

**Linux (Ubuntu / Debian):**

```bash
sudo apt install libnss3-tools
curl -JLO "https://dl.filippo.io/mkcert/latest?for=linux/amd64"
chmod +x mkcert-v*-linux-amd64
sudo cp mkcert-v*-linux-amd64 /usr/local/bin/mkcert
```

**Linux (Arch):**

```bash
sudo pacman -Syu mkcert
```

### Creating Trusted Certificates with mkcert

The workflow is two commands:

**Step 1 — Install the local CA (run only once per machine):**

```bash
mkcert -install
```

This creates a local root CA and registers it with your OS and browsers simultaneously. On macOS and Windows you will be prompted for your administrator password. After this single step, all certificates issued by mkcert will be unconditionally trusted on your machine.

**Step 2 — Generate a certificate for your project:**

```bash
# Navigate to your project root first
cd my-project

mkcert localhost 127.0.0.1 ::1
```

This produces two files:

- `localhost+2.pem` — the certificate (public)
- `localhost+2-key.pem` — the private key (keep this secret)

You can include as many hostnames and IP addresses as you need in a single certificate:

```bash
mkcert myapp.local "*.myapp.local" localhost 127.0.0.1 ::1
```

To generate the files with predictable names, use the `-cert-file` and `-key-file` flags:

```bash
mkcert -cert-file cert.pem -key-file key.pem localhost 127.0.0.1
```

### Protecting the Root CA Key

The file `rootCA-key.pem` (stored in the directory printed by `mkcert -CAROOT`) is highly sensitive. Anyone who possesses it can forge trusted certificates for any domain on any machine that trusts the corresponding public CA—including your bank, your email provider, or your company's internal services.

```bash
# Find the CA root directory
mkcert -CAROOT
# Example output: /Users/you/Library/Application Support/mkcert
```

Best practices for protecting it:

- Never commit it to version control (add `*.pem` and `certs/` to `.gitignore`)
- Never share it with teammates—each developer must run `mkcert -install` independently
- Never copy it to a shared machine or cloud storage

### Node.js and the System Trust Store

Node.js does not read the operating system trust store. Even after running `mkcert -install`, Node-based tools—`node-fetch`, `axios`, `jest` test runners hitting local APIs—will reject mkcert certificates with `UNABLE_TO_VERIFY_LEAF_SIGNATURE`.

The fix is the `NODE_EXTRA_CA_CERTS` environment variable:

```bash
# Bash / Zsh — add to ~/.bashrc or ~/.zshrc
export NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem"

# PowerShell — add to $PROFILE
$env:NODE_EXTRA_CA_CERTS = "$(mkcert -CAROOT)\rootCA.pem"
```

Restart your terminal after making this change so that all new shells inherit the variable.

---

## Setting Up HTTPS in Different Development Environments

### Node.js with Express (Full Example)

The earlier section showed the basics. Here is a production-realistic setup with HTTP-to-HTTPS redirection and security headers:

```javascript
// server.js
const https = require('https')
const http = require('http')
const fs = require('fs')
const express = require('express')

const app = express()

// Redirect plain HTTP to HTTPS
const redirectApp = express()
redirectApp.use((req, res) => {
	const httpsUrl = `https://${req.headers.host.split(':')[0]}:3443${req.url}`
	res.redirect(301, httpsUrl)
})

// Security headers for every response
app.use((_req, res, next) => {
	res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains')
	res.setHeader('X-Content-Type-Options', 'nosniff')
	res.setHeader('X-Frame-Options', 'DENY')
	res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin')
	next()
})

app.get('/', (_req, res) => {
	res.send('Secure Hello from Express!')
})

const httpsOptions = {
	key: fs.readFileSync('cert.pem').toString() === '' ? null : fs.readFileSync('key.pem'),
	cert: fs.readFileSync('cert.pem')
}

// Actually replace with:
const tlsOptions = {
	key: fs.readFileSync('./key.pem'),
	cert: fs.readFileSync('./cert.pem')
}

http.createServer(redirectApp).listen(3000, () => console.log('HTTP  → redirect on :3000'))
https
	.createServer(tlsOptions, app)
	.listen(3443, () => console.log('HTTPS server → https://localhost:3443'))
```

Run with:

```bash
npm install express
node server.js
```

### Python with Flask

Flask supports TLS natively via Python's `ssl` module:

```python
# app.py
import ssl
from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    return 'Secure Flask server running!'

if __name__ == '__main__':
    context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
    context.load_cert_chain('cert.pem', 'key.pem')
    app.run(host='0.0.0.0', port=5443, ssl_context=context)
```

Run with:

```bash
pip install flask
python app.py
# Visit https://localhost:5443
```

For quick prototyping, Flask also accepts a shorthand tuple:

```python
app.run(ssl_context=('cert.pem', 'key.pem'), port=5443)
```

### Python with Django

The `django-extensions` package provides `runserver_plus`, which wraps Werkzeug's development server and adds TLS support:

```bash
pip install django-extensions werkzeug pyOpenSSL
```

Add to `settings.py`:

```python
INSTALLED_APPS = [
    # ...
    'django_extensions',
]
```

Start the secure server:

```bash
python manage.py runserver_plus --cert-file cert.pem --key-file key.pem 0.0.0.0:8443
```

---

## Configuring HTTPS in Docker

Running HTTPS inside Docker requires mounting your certificates into the container and configuring the server to use them. The most flexible approach is to run Nginx as a TLS-terminating reverse proxy in front of your application container.

### Project Structure

```
my-project/
├── certs/
│   ├── cert.pem
│   └── key.pem
├── nginx/
│   └── default.conf
├── app/
│   └── index.js
└── docker-compose.yml
```

### Nginx Configuration

```nginx
# nginx/default.conf
server {
    listen 80;
    server_name localhost;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name localhost;

    ssl_certificate     /etc/nginx/certs/cert.pem;
    ssl_certificate_key /etc/nginx/certs/key.pem;

    # Enforce modern TLS only
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    location / {
        proxy_pass         http://app:3000;
        proxy_set_header   Host              $host;
        proxy_set_header   X-Real-IP         $remote_addr;
        proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
    }
}
```

### Docker Compose

```yaml
# docker-compose.yml
version: '3.8'

services:
  app:
    build: ./app
    expose:
      - '3000'
    environment:
      - NODE_ENV=development

  nginx:
    image: nginx:alpine
    ports:
      - '80:80'
      - '443:443'
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
      - ./certs:/etc/nginx/certs:ro
    depends_on:
      - app
```

Start everything with:

```bash
docker compose up
```

Visit `https://localhost`. Nginx handles TLS termination and forwards plain HTTP internally to the `app` container—a pattern that mirrors how most production deployments (load balancer → app servers) work.

### How a Request Flows Through the Docker Stack

```mermaid
sequenceDiagram
    participant B as Browser
    participant N as Nginx :443
    participant A as Node.js App :3000

    B->>N: HTTPS GET / (TLS ClientHello)
    N-->>B: ServerHello + Certificate
    B->>N: Encrypted HTTP/1.1 request
    N->>A: Plain HTTP proxy_pass
    A-->>N: HTTP 200 response
    N-->>B: Encrypted HTTP/1.1 response
```

### Using Caddy as a Zero-Config TLS Reverse Proxy

[Caddy](https://caddyserver.com/) is a modern web server that provides automatic HTTPS. For local development with a custom hostname, add an entry to your hosts file and create a `Caddyfile`:

```
# /etc/hosts — add this line
127.0.0.1 myapp.local
```

```
# Caddyfile
myapp.local {
    tls /path/to/cert.pem /path/to/key.pem
    reverse_proxy localhost:3000
}
```

Run:

```bash
caddy run
```

Caddy also supports automatic certificate management via its internal CA (`tls internal`), which behaves similarly to mkcert for local hostnames.

---

## Configuring HTTPS with Vite and Create React App

### Vite

Vite has native HTTPS support. You can use the official `@vitejs/plugin-basic-ssl` for a zero-config quick start, or point Vite directly at mkcert certificates for fully trusted TLS.

**Option A — Basic SSL plugin (self-signed, triggers browser warnings):**

```bash
npm install -D @vitejs/plugin-basic-ssl
```

```javascript
// vite.config.js
import { defineConfig } from 'vite'
import basicSsl from '@vitejs/plugin-basic-ssl'

export default defineConfig({
	plugins: [basicSsl()],
	server: {
		https: true,
		port: 5173
	}
})
```

**Option B — mkcert certificates (trusted, recommended):**

```bash
# Generate certificates in the project root
mkcert -cert-file cert.pem -key-file key.pem localhost 127.0.0.1
```

```javascript
// vite.config.js
import { defineConfig } from 'vite'
import fs from 'fs'

export default defineConfig({
	server: {
		https: {
			key: fs.readFileSync('./key.pem'),
			cert: fs.readFileSync('./cert.pem')
		},
		port: 5173
	}
})
```

**Option C — vite-plugin-mkcert (fully automated):**

```bash
npm install -D vite-plugin-mkcert
```

```javascript
// vite.config.js
import { defineConfig } from 'vite'
import mkcert from 'vite-plugin-mkcert'

export default defineConfig({
	plugins: [mkcert()],
	server: {
		https: true
	}
})
```

This plugin automatically installs the mkcert CA and regenerates certificates on first run. It is the most developer-friendly option for Vite projects.

### Create React App

Create React App reads HTTPS configuration from environment variables. Create a `.env.local` file in the project root:

```bash
# .env.local
HTTPS=true
SSL_CRT_FILE=./cert.pem
SSL_KEY_FILE=./key.pem
```

Or pass the variables directly in `package.json`:

```json
{
	"scripts": {
		"start:https": "HTTPS=true SSL_CRT_FILE=cert.pem SSL_KEY_FILE=key.pem react-scripts start"
	}
}
```

**Windows (PowerShell / cmd):**

```json
{
	"scripts": {
		"start:https": "set HTTPS=true&& set SSL_CRT_FILE=cert.pem&& set SSL_KEY_FILE=key.pem&& react-scripts start"
	}
}
```

### Next.js

For Next.js 13.5 and later, the `--experimental-https` flag spins up a locally trusted HTTPS server in one command:

```bash
next dev --experimental-https
```

For full control, create a custom HTTPS server:

```javascript
// server.js
const { createServer } = require('https')
const { parse } = require('url')
const next = require('next')
const fs = require('fs')

const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

const tlsOptions = {
	key: fs.readFileSync('./key.pem'),
	cert: fs.readFileSync('./cert.pem')
}

app.prepare().then(() => {
	createServer(tlsOptions, (req, res) => {
		const parsedUrl = parse(req.url, true)
		handle(req, res, parsedUrl)
	}).listen(3443, () => {
		console.log('Next.js ready → https://localhost:3443')
	})
})
```

Run with:

```bash
node server.js
```

---

## Tool Comparison: mkcert vs OpenSSL vs Caddy

Choosing the right tool depends on your workflow, operating system diversity, and how much control you need over certificate attributes.

| Feature                        | mkcert                    | OpenSSL                   | Caddy                       |
| ------------------------------ | ------------------------- | ------------------------- | --------------------------- |
| **Trust store integration**    | Automatic (OS + browsers) | Manual                    | Automatic (ACME / local CA) |
| **Ease of setup**              | Very easy (2 commands)    | Complex (many flags)      | Easy (Caddyfile syntax)     |
| **Wildcard certificates**      | Yes (`"*.app.local"`)     | Yes (requires SAN config) | Yes                         |
| **Custom hostnames**           | Yes                       | Yes                       | Yes                         |
| **Works fully offline**        | Yes                       | Yes                       | Yes (local CA mode)         |
| **Built-in reverse proxy**     | No                        | No                        | Yes                         |
| **HTTP/2 support**             | Depends on server         | Depends on server         | Yes, automatic              |
| **Production use**             | No (dev only)             | Yes                       | Yes                         |
| **Cross-platform**             | macOS, Linux, Windows     | macOS, Linux, Windows     | macOS, Linux, Windows       |
| **Learning curve**             | Low                       | High                      | Medium                      |
| **Certificate renewal**        | Manual (re-run mkcert)    | Manual                    | Automatic                   |
| **Client certificates (mTLS)** | Limited                   | Full support              | Full support                |

### When to Choose Each Tool

**Choose mkcert when:**

- You want the simplest, fastest developer experience with zero browser warnings
- Your team spans macOS, Linux, and Windows and you need a consistent setup
- You only need trusted server certificates for existing local servers

**Choose OpenSSL when:**

- You need full control over certificate extensions and attributes
- You are generating client certificates for mutual TLS (mTLS) scenarios
- You need intermediate CA chains, PKCS#12 bundles, or specific cipher configurations
- You are scripting certificate workflows in CI/CD pipelines

**Choose Caddy when:**

- You want TLS termination, HTTP/2, and reverse proxying in a single binary
- You want automatic certificate renewal in a local staging environment
- You are already using Caddy in production and want parity locally

---

## How the TLS Handshake Works in Local Development

Understanding the handshake at a high level makes troubleshooting significantly easier. When your browser first connects to `https://localhost:3443`, the following sequence occurs before any application data is exchanged:

```mermaid
sequenceDiagram
    participant C as Client (Browser)
    participant S as Server (localhost:3443)

    C->>S: ClientHello (supported TLS versions, cipher suites, random nonce)
    S-->>C: ServerHello (chosen cipher suite, session ID, random nonce)
    S-->>C: Certificate (cert.pem containing the public key)
    S-->>C: ServerHelloDone
    C->>C: Verify certificate chain against trusted CAs
    C->>S: ClientKeyExchange (pre-master secret, encrypted with server public key)
    C->>S: ChangeCipherSpec (switching to symmetric encryption)
    C->>S: Finished (hash of entire handshake)
    S-->>C: ChangeCipherSpec
    S-->>C: Finished
    Note over C,S: Secure tunnel established
    C->>S: Encrypted application data (HTTP request)
    S-->>C: Encrypted application data (HTTP response)
```

The critical step is **"Verify certificate chain against trusted CAs."** When using a raw self-signed certificate, the browser cannot find the issuer in its trust store and aborts with `ERR_CERT_AUTHORITY_INVALID`. When using mkcert, the browser walks up the certificate chain, finds the mkcert root CA registered in the OS trust store, and completes the handshake silently.

### What the Browser Validates

1. **Validity period**: Is today between `notBefore` and `notAfter` in the certificate?
2. **Hostname match**: Does a Subject Alternative Name (SAN) DNS entry match `localhost`?
3. **Issuer trust**: Is the certificate's issuer CA in the browser's trusted root program?
4. **Key usage**: Does the certificate allow server authentication (`extendedKeyUsage: serverAuth`)?

A common mistake with hand-crafted OpenSSL certificates is omitting the SAN extension. Chrome 58+ ignores the `Common Name` field for hostname validation and requires a matching SAN entry. To generate a SAN-enabled self-signed certificate with OpenSSL, use a config file:

```ini
# san.cnf
[req]
distinguished_name = req_distinguished_name
x509_extensions    = v3_req
prompt             = no

[req_distinguished_name]
CN = localhost

[v3_req]
keyUsage         = keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName   = @alt_names

[alt_names]
DNS.1 = localhost
DNS.2 = myapp.local
IP.1  = 127.0.0.1
IP.2  = ::1
```

Generate:

```bash
openssl req -x509 -nodes -days 825 -newkey rsa:2048 \
  -keyout local-dev.key -out local-dev.crt \
  -config san.cnf -extensions v3_req
```

The 825-day limit is the maximum validity period Chrome will accept for leaf certificates.

---

## Testing Your Local HTTPS Setup

After configuring HTTPS, verify the setup is correct before writing any application code against it.

### Using curl

`curl` is the quickest way to probe your HTTPS server from the command line:

```bash
# Full verification — passes for mkcert certs once CA is installed
curl -v https://localhost:3443

# Skip certificate validation — useful for raw self-signed certs (dev only)
curl --insecure https://localhost:3443

# Explicitly specify a trusted CA file
curl --cacert "$(mkcert -CAROOT)/rootCA.pem" https://localhost:3443
```

In the verbose output, look for:

```
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* Server certificate:
*   subject: CN=localhost
*   issuer: CN=mkcert ...
*   SSL certificate verify ok.
```

`SSL certificate verify ok` confirms the certificate is trusted.

### Using OpenSSL s_client

For deeper certificate inspection:

```bash
openssl s_client -connect localhost:3443 -servername localhost
```

The output includes:

- The full certificate chain (look for `Certificate chain` section)
- The negotiated cipher suite
- The certificate's `notBefore` / `notAfter` validity window
- `Verify return code: 0 (ok)` for a successful chain verification

If you see return code `18` (self-signed certificate) or `20` (unable to get local issuer certificate), the CA is not trusted.

### Browser DevTools

Open DevTools (`F12`) → **Security** tab. A correctly configured HTTPS connection shows:

- "This page is secure (valid HTTPS)"
- The certificate issuer (should be `mkcert <hostname>@<machine>` for mkcert certificates)
- The TLS version and cipher suite in use

The **Console** tab will surface mixed content warnings if any resources load over plain HTTP while the parent page is served over HTTPS.

### Automated HTTPS Check with Node.js

```javascript
// check-https.js
const https = require('https')
const { execSync } = require('child_process')

// Tell Node.js to trust the mkcert CA
process.env.NODE_EXTRA_CA_CERTS = `${execSync('mkcert -CAROOT').toString().trim()}/rootCA.pem`

const req = https.request(
	{ hostname: 'localhost', port: 3443, path: '/', method: 'GET' },
	(res) => {
		console.log('Status          :', res.statusCode)
		console.log('TLS authorized  :', res.socket.authorized)
		console.log('Cipher          :', res.socket.getCipher().name)
		console.log('TLS version     :', res.socket.getProtocol())
	}
)

req.on('error', (err) => console.error('Error:', err.message))
req.end()
```

Run with `node check-https.js`. A healthy output looks like:

```
Status          : 200
TLS authorized  : true
Cipher          : TLS_AES_256_GCM_SHA384
TLS version     : TLSv1.3
```

---

## Advanced Troubleshooting for HTTPS Issues

### ERR_CERT_AUTHORITY_INVALID

The browser cannot find the issuer CA in its trust store.

**Common causes and fixes:**

| Cause                             | Fix                                                        |
| --------------------------------- | ---------------------------------------------------------- |
| mkcert CA not installed           | Run `mkcert -install`                                      |
| Firefox on Linux uses NSS         | Install `libnss3-tools`, then `mkcert -install`            |
| Firefox needs a restart           | Close all Firefox windows and reopen                       |
| Upgraded mkcert, new CA generated | Run `mkcert -install` again                                |
| Different user account            | Run `mkcert -install` as the same user running the browser |

### ERR_CERT_COMMON_NAME_INVALID

The certificate was not issued for the hostname the browser is accessing.

**Fix**: Regenerate the certificate including every hostname you will use:

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

Update your server configuration to point at the newly generated files, then restart the server.

### Certificate Trusted in Browser but Not in Node.js

Node.js does not read the OS trust store. Set the CA path:

```bash
export NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem"
```

Then restart any running Node processes.

### Mixed Content Blocking

Mixed content occurs when a secure page loads sub-resources over HTTP. Browsers block passive mixed content and log an error for active mixed content.

Common culprits and fixes:

- **Hardcoded `http://` URLs in source**: do a global search for `http://localhost` and `http://127.0.0.1`
- **WebSocket connections**: change `ws://` to `wss://`
- **fetch / axios base URLs**: update to `https://`
- **`<img>`, `<script>`, `<link>` src attributes**: ensure none point to HTTP origins

### Port Already in Use

```bash
# macOS / Linux — find the process using port 3443
lsof -i :3443

# Windows — list all processes and their ports
netstat -ano | findstr :3443
# Kill by PID
taskkill /PID <pid> /F
```

### Expired Certificates

Both mkcert and OpenSSL certificates have a finite validity period:

```bash
# Check expiry date of any certificate
openssl x509 -in cert.pem -noout -dates
# notBefore=Aug 22 09:00:00 2024 GMT
# notAfter =Aug 22 09:00:00 2025 GMT
```

When a certificate expires, regenerate it:

```bash
# mkcert
mkcert -cert-file cert.pem -key-file key.pem localhost 127.0.0.1

# OpenSSL
openssl req -x509 -nodes -days 825 -newkey rsa:2048 \
  -keyout local-dev.key -out local-dev.crt -config san.cnf -extensions v3_req
```

Set a calendar reminder ahead of the expiry date, or automate regeneration in a `predev` npm script.

---

## Common Mistakes and Anti-Patterns

### 1. Committing Certificate Files to Version Control

Private keys in a repository are a serious security risk. Even if you delete the file in a later commit, it remains accessible in git history.

Add the following entries to your `.gitignore`:

```bash
# TLS certificates and keys — never commit these
*.pem
*.key
*.crt
*.p12
*.pfx
certs/
.cert/
```

Use a tool like [git-secrets](https://github.com/awslabs/git-secrets) or a pre-commit hook to scan for accidentally staged key files.

### 2. Using Production Certificates in Development

Copying a real Let's Encrypt certificate into your dev environment exposes the private key if the repository is shared. Always generate dedicated development certificates. They cost nothing with mkcert and carry zero production risk.

### 3. Using `--insecure` / `-k` Flags Without Removal Strategy

It is tempting to silence TLS errors with:

```bash
curl --insecure https://localhost:3443
```

The danger is that these flags spread into shared scripts and CI pipelines and eventually reach staging or production probes, masking real certificate errors. Instead, install the mkcert CA once and remove the insecure flag permanently.

### 4. Omitting Subject Alternative Names (SANs)

Hand-crafted OpenSSL certificates that rely solely on the `Common Name` field fail in Chrome 58+ with `ERR_CERT_COMMON_NAME_INVALID`. Always include at least one SAN entry as shown in the OpenSSL SAN config example above.

### 5. Not Mirroring Production TLS Configuration

If production enforces TLS 1.2 with specific cipher suites, configure your local server identically. Mismatches can hide compatibility bugs—for example, a client that works locally but fails in staging because it used a TLS 1.3 feature not available in the production load balancer.

### 6. Forgetting to Set `NODE_EXTRA_CA_CERTS` for Server-Side Tests

Unit and integration tests that make HTTPS requests to local servers will fail with `UNABLE_TO_VERIFY_LEAF_SIGNATURE` unless `NODE_EXTRA_CA_CERTS` is set. Add the export to your `.env.test` or your test setup file:

```javascript
// jest.setup.js or vitest.setup.js
const { execSync } = require('child_process')
process.env.NODE_EXTRA_CA_CERTS = `${execSync('mkcert -CAROOT').toString().trim()}/rootCA.pem`
```

### 7. Running `mkcert` on a Shared or CI Machine as a Long-Term Solution

mkcert is designed for individual developer workstations. Running `mkcert -install` on a shared build server installs a CA that every user on that machine trusts, which is an unnecessary security risk. For CI pipelines, use `curl --cacert` with the mkcert root CA file, or configure the test environment to skip TLS verification in a controlled and documented way.

---

## Using Custom Local Hostnames

By default, HTTPS certificates for `localhost` and `127.0.0.1` are sufficient for most development tasks. However, there are compelling reasons to use custom local hostnames—names like `myapp.local`, `api.myapp.local`, or `auth.myapp.local`—instead of bare `localhost`.

### Why Custom Hostnames Matter

The most important reason is **cookie isolation**. If you are running multiple applications on different ports of `localhost`, they all share the same cookie jar. A session cookie set by your API server at `localhost:4000` is visible to your frontend at `localhost:3000`, and also to any other service running on `localhost`. This is not how production behaves, where each subdomain is isolated. Using custom hostnames such as `api.myapp.local` and `app.myapp.local` gives each service its own cookie namespace, making development behaviour faithful to production.

A second reason is **OAuth redirect URIs**. Many OAuth providers—Google, GitHub, Microsoft—maintain a list of allowed redirect URIs and validate that the redirect URI in any given OAuth flow exactly matches a registered entry. Registering `https://myapp.local:3000/callback` in your OAuth application is often more reliable than trying to register a `localhost` URI with a non-standard port, which some providers reject or flag.

A third reason is that some browser security features are gated on origin matching. The `SameSite=Strict` cookie policy, for instance, treats cross-port requests on `localhost` as same-origin, which can obscure cross-site request forgery (CSRF) bugs that would surface in production.

### Configuring `/etc/hosts`

The `/etc/hosts` file provides a machine-local DNS override. Any hostname entry here resolves before the system hits a real DNS server. To create `myapp.local`:

```bash
# macOS / Linux — open with sudo to allow writes
sudo nano /etc/hosts

# Windows — open Notepad as Administrator, then open:
# C:\Windows\System32\drivers\etc\hosts
```

Add the following line:

```
127.0.0.1   myapp.local
127.0.0.1   api.myapp.local
127.0.0.1   auth.myapp.local
```

After saving, verify the entry resolves:

```bash
ping myapp.local
# Should show 127.0.0.1
```

### Generating Certificates for Custom Hostnames

With mkcert:

```bash
mkcert -cert-file cert.pem -key-file key.pem myapp.local "*.myapp.local" localhost 127.0.0.1
```

The wildcard `"*.myapp.local"` covers any single-level subdomain—`api.myapp.local`, `auth.myapp.local`, etc.—without needing a separate certificate for each.

### Automating Hosts File Management

Editing `/etc/hosts` manually quickly becomes tedious on large projects. The [hostile](https://github.com/feross/hostile) npm package provides a simple CLI:

```bash
npm install -g hostile

# Add entries
hostile set 127.0.0.1 myapp.local
hostile set 127.0.0.1 api.myapp.local

# List current entries
hostile list

# Remove an entry
hostile remove myapp.local
```

Alternatively, `dnsmasq` can be configured to resolve entire TLDs locally. For example, routing all `*.local` hostnames to `127.0.0.1` without editing `/etc/hosts` for each new service. This is particularly useful on macOS where dnsmasq integrates cleanly with the system resolver.

Once custom hostnames are configured, your browser, `curl`, and all Node.js clients will resolve them to your local machine. As long as the TLS certificate includes those hostnames in its SAN list and the mkcert CA is installed, HTTPS works seamlessly with no warnings.

---

## Certificate Lifecycle and Renewal Planning

Certificates issued by mkcert have a two-year validity period by default. OpenSSL self-signed certificates created with `-days 365` expire after one year. Letting a certificate expire in a shared development environment can halt work for everyone on the team without warning—browsers start refusing connections and error messages can be confusing if developers are not aware of the root cause.

### Checking Expiry Programmatically

You can incorporate an expiry check into your project's start script so developers are warned before the certificate expires rather than after it does. Here is a small Node.js helper:

```javascript
// scripts/check-cert.js
const { execSync } = require('child_process')
const fs = require('fs')

const certFile = process.env.SSL_CRT_FILE || './cert.pem'

if (!fs.existsSync(certFile)) {
	console.warn(
		`[TLS] Certificate not found at ${certFile}. Run: mkcert -cert-file cert.pem -key-file key.pem localhost`
	)
	process.exit(0)
}

const output = execSync(`openssl x509 -in ${certFile} -noout -enddate`).toString()
const match = output.match(/notAfter=(.+)/)
const expiry = match ? new Date(match[1]) : null
const daysLeft = expiry ? Math.floor((expiry - Date.now()) / 86400000) : 0

if (daysLeft < 30) {
	console.warn(
		`[TLS] WARNING: Certificate expires in ${daysLeft} days (${expiry?.toDateString()}).`
	)
	console.warn(
		'[TLS] Regenerate with: mkcert -cert-file cert.pem -key-file key.pem localhost 127.0.0.1'
	)
}
```

Add this to your `package.json` `predev` hook so it runs automatically every time someone starts the development server:

```json
{
	"scripts": {
		"predev": "node scripts/check-cert.js",
		"dev": "vite"
	}
}
```

### Using 825-Day Certificates with OpenSSL

When generating self-signed certificates manually, 825 days is the practical maximum. Apple devices and Chrome enforce a cap of 825 days on the maximum leaf certificate validity they will accept. Certificates with a longer `notAfter` date are treated as invalid regardless of the issuer CA:

```bash
openssl req -x509 -nodes -days 825 -newkey rsa:2048 \
  -keyout local-dev.key -out local-dev.crt \
  -config san.cnf -extensions v3_req
```

Using 825 days rather than 365 reduces maintenance burden without hitting the platform cap.

### Storing Certificates Outside the Repository

A practical team workflow is to store certificates in a well-known location outside the project directory and reference them via environment variables. This avoids both accidental commits and the need for each developer to regenerate certificates per project:

```bash
# ~/.config/dev-certs/ (macOS / Linux) or %APPDATA%\dev-certs\ (Windows)
mkdir -p ~/.config/dev-certs
mkcert -cert-file ~/.config/dev-certs/cert.pem -key-file ~/.config/dev-certs/key.pem \
  localhost 127.0.0.1 myapp.local "*.myapp.local"
```

Then in each project's `.env.local`:

```bash
SSL_CRT_FILE=~/.config/dev-certs/cert.pem
SSL_KEY_FILE=~/.config/dev-certs/key.pem
```

Developers generate the certificate once and reuse it across all projects on their machine. Regeneration is needed only when adding new hostnames or when the certificate approaches expiry.

---

## HTTPS and Browser-Exclusive APIs

One of the most concrete and often-overlooked benefits of enabling HTTPS in local development is access to browser APIs that are gated behind a secure context. A **secure context** in the browser means either `localhost` (treated as implicitly secure) or any HTTPS origin. However, there are specific APIs and features where even `localhost` is not enough—the connection must genuinely be HTTPS.

### Service Workers

Service workers are the backbone of Progressive Web Apps (PWAs). They intercept network requests, enable offline functionality, and manage push notifications. The Service Worker API is strictly limited to secure contexts. While `localhost` is allowed for development, as soon as you need to test a service worker against a custom hostname—`myapp.local`, for example—you must serve the application over HTTPS.

If your service worker fails to register and the console shows `SecurityError: Failed to register a ServiceWorker: The URL protocol of the current origin ('http://myapp.local') is not supported.`, the fix is to enable HTTPS for that origin.

### Geolocation, Camera, and Microphone

The browser's `navigator.geolocation`, `getUserMedia()` (camera and microphone access), and `navigator.mediaDevices` APIs all require a secure context in modern browsers. Chrome removed permission prompts for these APIs on non-secure origins entirely. If you are developing a location-aware feature or a video-chat application, local HTTPS is not optional—it is mandatory for the feature to function at all.

### Web Crypto API

The `window.crypto.subtle` interface, which provides access to cryptographic primitives like AES encryption, ECDSA signing, and HKDF key derivation, is only available in secure contexts. Applications that use client-side cryptography for end-to-end encryption or key wrapping will encounter `TypeError: Cannot read properties of undefined (reading 'subtle')` on plain HTTP.

### Clipboard API

The modern asynchronous Clipboard API (`navigator.clipboard.readText()` and `navigator.clipboard.writeText()`) requires a secure context and a user gesture. Copy-to-clipboard features fail silently or fall back to the deprecated `document.execCommand('copy')` on insecure origins. Testing the true Clipboard API behaviour requires HTTPS.

### Web Authentication API (WebAuthn)

The WebAuthn API, which underpins passkeys and hardware security key authentication (FIDO2/U2F), requires both a secure context and a proper relying party ID that matches the current origin's hostname. Testing WebAuthn flows—including passkey creation and assertion—is impossible without HTTPS served from a valid hostname.

### HTTP/2 and HTTP/3

Modern browsers only negotiate HTTP/2 and HTTP/3 over TLS connections (h2 and h3 ALPN extensions). Applications that benefit from HTTP/2 multiplexing—reducing the number of parallel TCP connections needed to load many small assets—cannot be tested accurately on a plain HTTP development server. Enabling local HTTPS allows you to verify performance characteristics that would otherwise only appear in production.

---

## Securing Cookies and Authentication Flows Locally

Setting up HTTPS locally is particularly important when developing authentication systems. Cookie flags and OAuth callback behaviour differ fundamentally between HTTP and HTTPS development environments.

### The `Secure` Cookie Flag

A cookie marked with the `Secure` flag is only transmitted by the browser on HTTPS connections. When developing on `http://localhost`, session cookies and CSRF tokens set with `Secure` are silently dropped—the browser records no error and the developer cannot tell why their authentication flow is broken. Enabling HTTPS allows you to test the exact same cookie policy your production server enforces.

In Express, set the `Secure` flag only when serving over HTTPS:

```javascript
res.cookie('sessionId', token, {
	httpOnly: true,
	secure: process.env.NODE_ENV !== 'development' || req.secure,
	sameSite: 'strict',
	maxAge: 3600000
})
```

Or, set `secure: true` unconditionally once you have HTTPS running locally—this is the most production-faithful approach and the one that will surface the broadest range of real-world issues.

### `SameSite` Attributes and Cross-Origin Requests

The `SameSite=Lax` attribute (the default in modern browsers) allows cookies to be sent on top-level navigations but not on cross-origin sub-resource requests. `SameSite=Strict` blocks the cookie on all cross-origin requests, including navigations. `SameSite=None` requires `Secure` to be set as well, or the cookie is rejected entirely.

If your application involves an API on a different subdomain from the frontend—`api.myapp.local` vs `app.myapp.local`—testing the correct `SameSite` behaviours requires distinct hostnames served over HTTPS. A single `localhost` with different ports is not sufficient because the browser treats all `localhost` ports as the same origin.

### OAuth 2.0 Redirect URIs

When integrating with third-party OAuth 2.0 providers (Google, GitHub, Microsoft, Auth0), the provider compares the `redirect_uri` parameter in the authorization request against a pre-registered allow-list. Most providers accept `http://localhost` as a special case for development. However, some providers require HTTPS even in development, and many developers find it cleaner to register a proper local HTTPS URI from the start so the development configuration remains identical to production.

With local HTTPS:

1. Register `https://myapp.local:3443/auth/callback` in your OAuth application settings.
2. Generate an mkcert certificate for `myapp.local`.
3. Point your browser to `https://myapp.local:3443`.

The OAuth flow completes without requiring any development-only workarounds in your application code.

### JWT Validation and HTTPS-Only Token Endpoints

Some identity providers issue tokens only to clients connecting over HTTPS and reject token exchange requests from HTTP origins. If you encounter `401 Unauthorized` errors when trying to exchange an authorization code for an access token in your local environment, ensure that your redirect URI is HTTPS, that your local server is actually serving over TLS, and that the `Host` header matches a registered redirect URI exactly.

---

## Team Onboarding and Shared HTTPS Workflows

Configuring HTTPS locally is straightforward for a single developer, but ensuring a consistent setup across an entire team—spanning different operating systems and seniority levels—requires deliberate documentation and automation.

### Providing a Setup Script

A small cross-platform setup script eliminates the "works on my machine" problem for TLS configuration. Here is a minimal example using Node.js that can be run on any platform:

```javascript
// scripts/setup-https.js
const { execSync, spawnSync } = require('child_process')
const fs = require('fs')

const certFile = 'cert.pem'
const keyFile = 'key.pem'

// Check if mkcert is installed
const which = spawnSync('mkcert', ['-version'], { encoding: 'utf-8' })
if (which.status !== 0) {
	console.error('mkcert is not installed. See https://github.com/FiloSottile/mkcert#installation')
	process.exit(1)
}

// Install the local CA if not already done
execSync('mkcert -install', { stdio: 'inherit' })

// Generate project certificates
execSync(
	`mkcert -cert-file ${certFile} -key-file ${keyFile} localhost 127.0.0.1 myapp.local "*.myapp.local"`,
	{
		stdio: 'inherit'
	}
)

console.log(`\nCertificates written to ${certFile} and ${keyFile}`)
console.log('Add both files to .gitignore if you have not already done so.')
```

Add it to `package.json`:

```json
{
	"scripts": {
		"setup": "node scripts/setup-https.js"
	}
}
```

New team members run `npm run setup` after cloning the repository and their local HTTPS environment is ready.

### Documenting the Setup in the README

The project README should include a dedicated section explaining:

1. That the project requires HTTPS for local development and why.
2. Which tool generates the certificates (mkcert, OpenSSL, etc.) and the installation command for each supported OS.
3. The exact command to generate the project-specific certificate.
4. How the certificate paths are configured (environment variables, config files).
5. How to run the development server after setup.
6. A note that certificate files must not be committed to version control.

Clear documentation prevents support requests and ensures that the purpose of the HTTPS requirement is understood—not just mechanically followed.

### CI/CD Considerations

Automated test pipelines that make HTTPS requests to a locally spawned server face the same trust challenge. In CI, you generally have two options:

**Option A — Use the mkcert CA in the pipeline:**
The CI runner generates certificates with mkcert at pipeline start, exports the root CA path, and sets `NODE_EXTRA_CA_CERTS` for Node.js test runners. This is the highest-fidelity approach.

**Option B — Use the `--cacert` flag in curl-based health checks:**
If your CI pipeline uses `curl` smoke tests rather than Node.js HTTPS requests, pass `--cacert` pointing at the mkcert root CA PEM file. Avoid `--insecure` in CI—it hides real certificate issues.

**Option C — Accept self-signed certificates only in test scope:**
For unit and integration tests where you control both client and server, set `NODE_TLS_REJECT_UNAUTHORIZED=0` as a last resort, but restrict it to the test environment exclusively and document why it is set. Never allow this variable to propagate to a staging or production environment.

---

## Real-World Use Cases for HTTPS in Local Development

1. **Testing Secure APIs**

Simulating production-like HTTPS endpoints for API testing is one of the most immediate benefits of local HTTPS. Many third-party APIs—payment gateways, identity providers, webhook delivery services—send callbacks exclusively over HTTPS. Without local HTTPS, you need a tunneling service like ngrok to receive those callbacks, which adds an external dependency to your workflow. With local HTTPS and a custom hostname, you can register `https://api.myapp.local/webhooks/stripe` as a callback URL in your test account and receive payloads directly without routing traffic through a third-party server.

2. **Building Progressive Web Apps (PWAs)**

Many PWA features, like service workers, background sync, push notifications, and the Web App Manifest installation prompt, require a secure context. Developing a PWA over plain HTTP results in features being silently unavailable, making it impossible to test the full application lifecycle locally. HTTPS unlocks the complete set of PWA capabilities and gives you confidence that what works locally will work when deployed.

3. **Developing Secure Authentication Systems**

OAuth 2.0 flows, JWT validation, session cookie lifecycle, CSRF token handling, and WebAuthn passkey flows all behave differently under HTTP versus HTTPS. Testing authentication over plain HTTP leaves entire categories of production bugs undetected—particularly those involving the `Secure` cookie flag, `SameSite` policy enforcement, and HTTPS-only token endpoints. A local HTTPS setup ensures your authentication implementation is tested under conditions that match production as closely as possible.

4. **Cross-Device Testing**

When testing your application on a mobile device connected to the same local network, the device's browser will also validate TLS certificates. Exporting the mkcert root CA to your mobile device and installing it as a trusted CA (described in the mkcert README for both iOS and Android) allows you to use `https://192.168.x.x:3443` on the device with full certificate trust, enabling realistic mobile testing without deploying to a real server.

## Future Trends in HTTPS for Development

1. **Automatic Certificate Management**

- Tools like `mkcert` and Docker Compose will streamline HTTPS setup for local environments.

2. **Enhanced Local SSL Tools**

- Expect improvements in browser and OS support for local HTTPS testing.

3. **Integration with Zero-Trust Architectures**

- HTTPS will play a crucial role in securing microservices and distributed systems.

---

## HTTPS Performance Considerations in Local Development

Developers sometimes worry that adding TLS to a local server will slow down their iteration cycle. In practice, the performance overhead of TLS in a loopback connection (traffic that never leaves the machine) is negligible for most workloads. The TLS handshake adds a few milliseconds on the first connection, and modern TLS 1.3 reduces round trips from two down to one. On subsequent requests using persistent connections, the overhead is effectively zero.

That said, there are performance-related scenarios worth understanding:

### TLS Session Resumption

When using HTTP/1.1, browsers open multiple TCP connections to a server to parallelize asset loading. Each new connection requires a TLS handshake. HTTP/2, however, multiplexes many request/response pairs over a single connection, amortising the handshake cost across all requests on a page. Because HTTP/2 is TLS-only in browsers, enabling local HTTPS is a prerequisite for profiling realistic HTTP/2 performance locally.

If you are building a performance-sensitive application and you want DevTools waterfall charts to reflect production behaviour, local HTTPS with a server that supports HTTP/2 (Nginx, Caddy, or Node.js `http2` module) gives you the most accurate picture.

### OCSP Stapling and Local Certificates

In production, browsers perform Online Certificate Status Protocol (OCSP) checks to verify that a certificate has not been revoked. For local certificates this step is skipped—self-signed and mkcert certificates do not have OCSP endpoints, and the browser is configured not to require them for locally installed CAs. This means local HTTPS connections are actually _faster_ than production connections in one respect: there is no OCSP round-trip to a remote server.

### Large Certificate Chains

If you generate a complex CA chain for testing purposes—root CA, intermediate CA, leaf certificate—include the full chain in the certificate file presented by your server. Browsers that cannot retrieve intermediate certificates will fail the handshake entirely rather than degrade gracefully. With mkcert this is handled automatically; with hand-crafted OpenSSL chains, concatenate the intermediates in order:

```bash
cat leaf.crt intermediate.crt > fullchain.crt
```

### Keeping Hot Reload Fast

Vite, Next.js, and Create React App all support Hot Module Replacement (HMR). HMR typically uses a WebSocket connection. When HTTPS is enabled, the WebSocket must also use `wss://` instead of `ws://`. Modern framework dev servers handle this automatically when HTTPS is configured through their official configuration options, but third-party bundler plugins sometimes require an explicit `wss` URL in the client configuration. Always verify HMR works after enabling HTTPS by making a small code change and confirming the browser updates without a full page reload.

---

## Quick Setup Reference

After reading this guide in full, here is a condensed reference for the most common setups. Use this as a checklist when onboarding to a new project or machine.

### New Machine Checklist

Before doing anything project-specific, set up the machine foundation once:

1. Install mkcert for your OS (see the mkcert section above).
2. Run `mkcert -install` to register the local CA with your OS and browsers.
3. Add `export NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem"` to your shell profile.
4. Restart your terminal.

### Per-Project Checklist

For each project that needs local HTTPS:

1. Generate certificates:
   ```bash
   mkcert -cert-file cert.pem -key-file key.pem localhost 127.0.0.1 myapp.local
   ```
2. Add `cert.pem` and `key.pem` to `.gitignore`.
3. Set `SSL_CRT_FILE` and `SSL_KEY_FILE` environment variables in `.env.local` (or pass paths directly to your server configuration).
4. Configure your framework dev server (Vite, CRA, Next.js, Express, Flask) to use the certificate.
5. Add a `predev` step that checks certificate expiry.
6. Document the HTTPS setup in the project README.

### Decision Tree: Which Tool to Use?

When you are deciding how to set up local HTTPS on a new project, the following questions guide the choice:

- **Do you just need trusted certificates with no reverse proxy?** Use mkcert.
- **Do you need full control over certificate extensions (SANs, key usage, EKU)?** Use OpenSSL with a config file.
- **Do you need TLS termination, HTTP/2, and automatic certificate management in one binary?** Use Caddy.
- **Are you running multiple services behind a single entry point in Docker?** Use Nginx or Caddy as a TLS-terminating reverse proxy in your Docker Compose stack.
- **Are you on a team using Vite?** Use `vite-plugin-mkcert` for the most seamless experience.
- **Are you building a Create React App project?** Use environment variables pointing at mkcert certificates.

This decision tree covers the vast majority of real-world local HTTPS scenarios. When in doubt, start with mkcert—it is the easiest to set up, the hardest to misconfigure, and the most commonly used tool in the industry.

## Conclusion

Setting up HTTPS for local development is a vital step toward building secure, production-ready applications. By following the steps in this guide, you can simulate secure environments, test SSL-specific features, and avoid potential issues in deployment. The combination of mkcert for trusted certificates, a framework-native HTTPS configuration, and a short team onboarding script removes virtually all friction from the process. Start implementing HTTPS in your local development workflow today and ensure your applications are built with security in mind from the very first line of code.

---

## About the author

Kubilay Tunca — Senior Full Stack Developer and Author. Founded Cyber Security in Plain English to translate complex security concepts into clear, practical advice, and writes the accompanying books on security, privacy, secure development, and AI systems.

## Books by this author

- **The Digital Fortress** — Your Everyday Guide to a Safer Digital Life. A warm, plain-English guide for people with real lives and finite patience. Learn the handful of habits that genuinely protect your money, accounts, and family, and get honest permission to ignore the rest. [Amazon](https://buy.cyber-security-in-plain-english.com/digital-fortress) · [Details](https://cyber-security-in-plain-english.com/books/the-digital-fortress)
- **The Anonymity Playbook** — Digital Survival for Whistleblowers, Journalists, Activists, and Everyone Else. A practitioner’s field manual for journalists protecting sources, whistleblowers, and activists. It explains how the surveillance actually works, what each technique costs you, and exactly where it fails. [Amazon](https://buy.cyber-security-in-plain-english.com/anonymity-playbook) · [Details](https://cyber-security-in-plain-english.com/books/the-anonymity-playbook)
- **Secure Software Development** — Practical patterns for building secure software. A hands-on security guide for developers and IT professionals who ship real software. Build, deploy, and maintain secure systems without slowing down or drowning in theory. [Amazon](https://buy.cyber-security-in-plain-english.com/secure-software-development) · [Details](https://cyber-security-in-plain-english.com/books/secure-software-development)
- **The Secure Harness** — Shipping Production Code with AI Coding Agents. A calm, practical guide to letting agents do useful work inside boundaries you set, enforce, and audit. Ships with 15 copy-pasteable artifacts: hook scripts, permission configs, release gates, and MCP templates. [Amazon](https://buy.cyber-security-in-plain-english.com/secure-harness) · [Details](https://cyber-security-in-plain-english.com/books/the-secure-harness)
- **The AI Native Engineer** — Build, Evaluate, and Ship AI Systems That Work in Production. Sixteen hands-on chapters, one real product. Grow it from a single model call into a retrieved, tool-using, observable, production-grade system, with evaluation treated as a habit from the first feature. [Amazon](https://buy.cyber-security-in-plain-english.com/ai-native-engineer) · [Details](https://cyber-security-in-plain-english.com/books/the-ai-native-engineer)

Full catalogue with contents and intended audience: https://cyber-security-in-plain-english.com/books

_As an Amazon Associate I earn from qualifying purchases. Buying through these links costs you nothing extra and helps pay for the blog._
