>_ josetilabs.com
← blog

21 Oct 2026 · tools, linux, tls · 5 min

HTTPS on localhost with mkcert, without certificate warnings

mkcert creates a certificate authority on your machine, installs that CA into the system and browser trust stores, and signs a certificate for localhost, 127.0.0.1 and ::1. The browser stops complaining because the chain closes at a CA it already trusts. Measured: with the CA passed in –cacert, curl returns HTTP 200 and ssl_verify_result 0 in 0.006 s; without it, it fails with “unable to get local issuer certificate”.

What mkcert does

A self-signed certificate signs itself. The browser has no reason to trust it. mkcert splits the problem in two: the authority (rootCA.pem and rootCA-key.pem, created once) and the leaf certificate, signed by that authority. mkcert -install puts the authority in the trust stores the system and the browser read. After that, any certificate signed by it passes without a warning.

rootCA-key.pem is the secret. Anyone with that file can issue a trusted certificate for any name on your machine. Do not copy it to another host, do not commit it, do not share it.

Install on Linux

Download the binary from the releases page (v1.4.4 in October 2026). On Debian/Ubuntu, install libnss3-tools first, because mkcert only writes to the Firefox/Chrome store if certutil exists at the time of -install.

bash
sudo apt install libnss3-tools
# download mkcert-v*-linux-amd64 from the releases page
chmod +x mkcert
sudo mv mkcert /usr/local/bin/
mkcert -version

Generate the CA and the certificate

bash
mkcert -install
mkcert -CAROOT
mkcert -cert-file ./localhost.pem -key-file ./localhost-key.pem localhost 127.0.0.1 ::1

The first command creates and installs the CA. The second shows where it lives (typically ~/.local/share/mkcert). The third generates localhost.pem and localhost-key.pem, valid for localhost, 127.0.0.1 and ::1, expiring on 21 January 2029.

Pass all names in one call. A certificate with several SANs is easier to manage than five certificates.

Proof: local server with a padlock

I ran an HTTPS server on port 8443 using localhost.pem and localhost-key.pem. With the CA in –cacert, curl returns HTTP 200 and ssl_verify_result 0 in 0.006 s. Without the CA, it fails with “unable to get local issuer certificate”. The browser behaves the same way: installed CA, no warning; no CA, warning.

Terminal showing mkcert -install, issuing the certificate for localhost 127.0.0.1 and ::1, and curl on https://localhost:8443 returning http 200 with verify 0
From the installed CA to a curl with no warning, on one local machine.

Use in Node, nginx and containers

Node does not read the system store by default. Point the CA with an environment variable:

bash
NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem" node server.js

In nginx, use the generated files:

nginx
ssl_certificate /etc/nginx/certs/localhost.pem;
ssl_certificate_key /etc/nginx/certs/localhost-key.pem;

In a container that needs to call the local endpoint, copy only rootCA.pem and run update-ca-certificates. Never copy the key.

When not to use it

mkcert is a development tool, and its own documentation is blunt about that. For a public hostname, use Let’s Encrypt. For an internal service with several hosts and real revocation, use an internal CA such as step-ca. The line is simple: mkcert for one developer’s machine; beyond that, a CA someone operates.

The artifact: complete script

bash
#!/usr/bin/env bash
set -euo pipefail

if ! command -v mkcert >/dev/null 2>&1; then
    echo "mkcert not found."
    echo "Linux: download the binary from https://github.com/FiloSottile/mkcert/releases"
    echo "Debian/Ubuntu: apt install libnss3-tools before mkcert -install"
    exit 0
fi

CAROOT="$(mkcert -CAROOT)"
echo "CA at: $CAROOT"

mkcert -install

mkcert -cert-file ./localhost.pem -key-file ./localhost-key.pem \
    localhost 127.0.0.1 ::1

echo "certificate: ./localhost.pem"
echo "key: ./localhost-key.pem"
echo "use both in your local server"

How to check

mkcert -CAROOT shows the CA directory. openssl x509 -in localhost.pem -noout -subject -issuer -dates shows an issuer equal to the local CA and valid dates. curl --cacert "$(mkcert -CAROOT)/rootCA.pem" https://localhost:8443/ should return HTTP 200 with ssl_verify_result 0.

When it goes wrong

Certificate warning in the browser. Cause: mkcert -install did not find the browser store, because certutil was not installed. Fix: install libnss3-tools (Debian/Ubuntu) or nss-tools (RHEL) and run mkcert -install again.

curl: (60) SSL certificate problem. Cause: the CA was not passed. Fix: --cacert "$(mkcert -CAROOT)/rootCA.pem".

Certificate does not cover the name. Cause: the name used in the browser is not in the SAN list. Fix: regenerate including the exact name, for example mkcert ... app.localhost "*.localhost".

Port 8443 in use. Cause: another process. Fix: choose another port in the server or kill the process.

Table

Option Browser trust When to use
Self-signed certificate no never in local dev if you can avoid it
mkcert yes, on your machine local development
Let’s Encrypt yes, everywhere public hostname
step-ca yes, on the internal network several hosts, revocation

FAQ

Does mkcert work on Windows and macOS? Yes. On Windows it uses the system store; on macOS, the Keychain. Firefox uses its own NSS store on all three.

Do I need to run mkcert -install for every project? No. One CA per machine. Generate as many certificates as you want from it.

Does the certificate expire? Yes, in about 3 years (the measured certificate expires on 21 January 2029). Run the generation command again.

Can I use a wildcard? Yes: mkcert "*.localhost". Useful for local subdomains.

Is it safe in production? No. The local CA has no revocation and the key must not leave the machine.

Sources

mkcert on GitHub, checked in October 2026, version 1.4.4. The mkcert guide for local HTTPS details trust stores per operating system. For the other side of the connection, TLS handshake step by step. For a public certificate, Let’s Encrypt: 64 days of certificate at the edge.

Read next

// By e-mail

Posts published here also go out by e-mail. Delivery has not started yet: whoever leaves an address now joins the first batch.

I store only your e-mail, the date and the record of this consent. No cookies, no trackers, nothing shared with third parties. Details in the privacy policy.