HTTPS without browser warnings in Docker
Self-signed certificates constantly trigger browser warnings and can't be used for APIs or service workers. Combining mkcert with a Docker reverse proxy solves the problem completely: local domains with real, trusted HTTPS, no cloud, no Let's Encrypt, no endless clicking.
Table of Contents
- 1. Why local HTTPS is more than a convenience
- 2. mkcert: a local CA without browser warnings
- 3. Installing mkcert and setting up the CA
- 4. Generating SSL certificates for local domains
- 5. Nginx as a reverse proxy in Docker
- 6. Traefik as a dynamic reverse proxy
- 7. Multi-domain setup for multiple projects
- 8. Team rollout: distributing the CA certificate
- 9. Reverse proxy options compared
- 10. Summary
- 11. FAQ
1. Why local HTTPS is more than a convenience
Local HTTPS with mkcert is not just a matter of convenience: there are technical reasons why HTTPS is necessary in a local development environment. Browser features such as service workers, the Geolocation API, the Web Crypto API, HTTP/2 and SameSite cookies with the Secure flag only work over HTTPS or on localhost. Anyone testing an application that relies on one of these features won't get far without local HTTPS. More importantly: if staging and production run on HTTPS, a development environment on plain HTTP is effectively a different stack, and problems that only surface in combination with TLS will never be caught locally.
Self-signed certificates are not a real solution for local HTTPS: they trigger browser security warnings, get rejected by HTTP clients in tests, and make working with APIs cumbersome, since every single tool has to be configured to ignore certificate checks. mkcert solves this elegantly: it creates a local Certificate Authority (CA) and installs it in the trust store of the operating system and the browsers. Certificates issued afterward are treated as trusted, with no browser warnings and no special configuration needed in the HTTP client.
2. mkcert: a local CA without browser warnings
mkcert is a simple Go tool by Filippo Valsorda built specifically for local development. It generates a local root CA and registers it in the system trust store (the NSS database for Firefox, Keychain on macOS, the certificate store on Windows). After that, a single command is enough to generate certificates for any local domain, and those certificates are treated as trusted by the browser because they are signed by the installed local CA.
The key difference from real CA certificates: the CA generated by mkcert is only trusted within the local trust store of your own machine, not on the internet. That's not a drawback, it's a security feature: the local CA cannot issue certificates for real domains that would be trusted by anyone else. The private key of the local CA lives exclusively on your own machine. For team setups, the CA needs to be shared once and installed on every developer's machine, after which each developer can issue certificates for local domains on their own.
# Install mkcert on different platforms
# macOS via Homebrew
brew install mkcert nss # nss is needed for Firefox support
# Linux (Debian/Ubuntu)
sudo apt install libnss3-tools
curl -L https://github.com/FiloSottile/mkcert/releases/latest/download/mkcert-v1.4.4-linux-amd64 \
-o /usr/local/bin/mkcert
chmod +x /usr/local/bin/mkcert
# Windows via Chocolatey
choco install mkcert
# Step 1: Create and install the local CA
# This modifies the system trust store, requires admin/sudo
mkcert -install
# Verify CA location
mkcert -CAROOT
# Output: /home/user/.local/share/mkcert (Linux)
# Output: /Users/user/Library/Application Support/mkcert (macOS)
# The CA certificate (rootCA.pem) must be distributed to all team members
# The private key (rootCA-key.pem) must NEVER be shared
3. Installing mkcert and setting up the CA
mkcert ships as a single binary and needs no runtime or dependency installation. On macOS, brew install mkcert is the simplest route; the NSS package needs to be installed separately for Firefox support. The command mkcert -install creates the local CA and installs the root certificate into every detected trust store: macOS Keychain, NSS for Firefox and Chrome, and the Windows certificate store. After this step the CA is active, and new browser sessions will treat issued certificates as trusted.
The directory where mkcert stores the CA files (mkcert -CAROOT) contains two files: rootCA.pem (the public CA certificate) and rootCA-key.pem (the private key). The public certificate can and should be shared across the team so that every developer installs the same CA and trusts the certificates it issues. The private key must remain strictly confidential: whoever holds it can issue certificates for any domain that will be trusted by everyone who has installed the CA.
4. Generating SSL certificates for local domains
With the local CA installed, generating certificates is a one-line command. mkcert accepts any number of domains and IP addresses in a single certificate: mkcert myproject.local *.myproject.local 127.0.0.1 ::1. The wildcard certificate for *.myproject.local covers every subdomain, which is handy for setups with multiple services under one domain. The generated files (myproject.local.pem and myproject.local-key.pem) go into the project directory and get mounted into the Docker container as secrets.
The certificates directory should not live in the repository: private keys don't belong in version control. A .gitignore entry for certs/*.pem prevents accidental commits. Instead, the repository should hold a setup script that documents and, if needed, runs the certificate generation command. For CI environments that need HTTPS for integration tests, mkcert can run as part of the test setup script, so the generated certificates only exist for the duration of the CI session.
#!/usr/bin/env bash
# scripts/setup-certs.sh: generate local SSL certificates for Docker development
set -euo pipefail
CERTS_DIR="./certs"
DOMAIN="${1:-myproject.local}"
# Ensure certs directory exists
mkdir -p "$CERTS_DIR"
# Check if mkcert is installed
if ! command -v mkcert &>/dev/null; then
echo "ERROR: mkcert is not installed. Run: brew install mkcert nss"
exit 1
fi
# Install local CA (safe to run multiple times, skips if already installed)
mkcert -install
# Generate certificate for the project domain and wildcard subdomains
mkcert \
-cert-file "${CERTS_DIR}/${DOMAIN}.pem" \
-key-file "${CERTS_DIR}/${DOMAIN}-key.pem" \
"${DOMAIN}" \
"*.${DOMAIN}" \
"localhost" \
"127.0.0.1" \
"::1"
echo "Certificates generated in ${CERTS_DIR}/"
echo "Add to /etc/hosts: 127.0.0.1 ${DOMAIN}"
echo "IMPORTANT: Never commit *-key.pem files to version control"
5. Nginx as a reverse proxy in Docker
Nginx is the classic choice for a reverse proxy in Docker development environments. The configuration is explicit and well documented, making it the natural entry point for teams with Nginx experience. An Nginx container accepts HTTPS connections on port 443, terminates TLS using the mkcert certificate, and forwards requests to the backend services. The backend service itself doesn't need to speak HTTPS at all, since TLS termination happens entirely inside the Nginx container.
The setup consists of three parts: the Nginx Docker service in the compose file, an Nginx configuration file with the SSL configuration, and a bind mount of the certificates directory into the Nginx container. The Nginx configuration defines a server block for HTTPS on port 443 with the SSL certificate files and a proxy_pass directive pointing to the backend service on the Docker network. HTTP requests on port 80 get redirected to HTTPS. For multiple projects on the same machine, a single Nginx container can serve as a central reverse proxy for all of them.
6. Traefik as a dynamic reverse proxy
Traefik is a more modern alternative to Nginx as a reverse proxy in Docker development environments. The key difference: Traefik configures itself dynamically through Docker labels on the service containers, instead of relying on a static configuration file. A new service simply starts up with the right labels, and Traefik automatically discovers it and sets up the routing. This is particularly useful for teams running several projects on the same developer machine.
With mkcert certificates and Traefik, a complete local HTTPS stack can be defined in a single compose file. Traefik reads the certificates from a defined directory provided via a bind mount. The TLS configuration for local domains is defined through Traefik middleware labels on the services. Once the Traefik setup is running, each new project only needs a new compose file with the right labels, with no need to restart Traefik or manually edit configuration files.
# compose.yml: Traefik reverse proxy with mkcert SSL certificates
services:
traefik:
image: traefik:v3.0
container_name: traefik-proxy
command:
# Enable Docker provider: auto-discover containers via labels
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
# Load TLS certificates from directory
- "--providers.file.directory=/etc/traefik/certs"
- "--providers.file.watch=true"
# Entrypoints: HTTP and HTTPS
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
# Redirect HTTP to HTTPS globally
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./certs:/etc/traefik/certs:ro
- ./traefik/dynamic.yml:/etc/traefik/certs/dynamic.yml:ro
networks:
- proxy
myapp:
image: nginx:alpine
labels:
- "traefik.enable=true"
- "traefik.http.routers.myapp.rule=Host(`myproject.local`)"
- "traefik.http.routers.myapp.entrypoints=websecure"
- "traefik.http.routers.myapp.tls=true"
networks:
- proxy
networks:
proxy:
external: true
7. Multi-domain setup for multiple projects
Anyone running multiple projects on the same machine with local HTTPS needs a strategy for domain management. The simplest approach: each project gets its own .local domain (for example shop.local, api.local, admin.local) with its own mkcert certificate and its own entry in /etc/hosts. A central Traefik container acting as a global proxy on the developer machine routes incoming requests to the right project network based on the hostname in the request.
For large teams running many parallel projects, an automated script can manage the /etc/hosts entries and certificates: the script reads a project configuration file (a list of domains), generates missing certificates with mkcert, and updates /etc/hosts accordingly. An alternative to /etc/hosts entries is a local DNS resolver such as dnsmasq, which resolves every *.local request to 127.0.0.1, eliminating manual entries for new domains entirely.
8. Team rollout: distributing the CA certificate
The mkcert CA approach scales well across teams when the rollout process is clearly defined. The key is a shared CA certificate: every developer installs the same rootCA.pem, created once by a single authorized person using mkcert -install. After that, developers can either issue their own certificates for local domains, or a central certificate bundle in the project repository can be generated automatically by the setup script.
The public CA certificate (rootCA.pem) can safely be stored in the team repository or an internal wiki: it contains no private key and is harmless on its own. Installing it on a new machine is trivial: mkcert -install after copying the CA directory into the mkcert CAROOT path. For automated onboarding scripts, mkcert can be configured to read the CA directory from a specific path instead of the default home directory, which makes it possible to manage several CAs for different projects or clients on the same machine.
9. Reverse proxy options compared
Several reverse proxy options are available for local Docker development environments. The choice depends on a preference for explicit configuration versus dynamic service discovery, and on the number of projects running in parallel.
| Proxy | Configuration | New Projects | Recommendation |
|---|---|---|---|
| Nginx | Static config files | Add config file + reload | Teams with Nginx experience, 1 to 3 projects |
| Traefik | Docker labels + dynamic.yml | Just labels on the service | Many projects, dynamic environments |
| Caddy | Caddyfile (minimal) | Add to Caddyfile | Simplest syntax, built-in TLS logic |
| HAProxy | haproxy.cfg | Complex config, reload required | Only if HAProxy is already in the stack |
| nginx-proxy | Env variables on the service | Set the VIRTUAL_HOST variable | Easy entry point, no labels needed |
Traefik is the recommended choice for most modern Docker development environments: dynamic service discovery via labels makes new projects accessible with minimal configuration, and HTTPS support with custom certificates is well documented. Nginx is the better choice when the team already has Nginx expertise and the configuration should be explicit and easy to follow. Caddy wins on the simplest configuration syntax and is a good option for smaller teams or individual developers with no specific proxy preference.
Mironsoft
Docker development environments, HTTPS setup and team onboarding
Setting up local HTTPS with mkcert and Docker?
We set up mkcert, a reverse proxy and the complete local HTTPS infrastructure for your development team, including onboarding documentation and setup scripts.
mkcert setup
Set up the local CA, generate certificates for all project domains and carry out the team rollout
Reverse proxy
Configure Traefik or Nginx as a central proxy for all local development projects
Multi-project
Create and maintain setup scripts and documentation for the whole team
10. Summary
Local HTTPS with mkcert and a Docker reverse proxy can be set up in about an hour and permanently eliminates browser warnings and restricted browser APIs. mkcert creates a local CA and installs it in the system trust store; certificates it issues are treated as fully trusted by the browser. Traefik as a dynamic reverse proxy accepts HTTPS connections and forwards them to backend services, without those services having to speak TLS themselves. For new projects, it's enough to set Docker labels on the service container.
The team rollout is straightforward: the public CA certificate is shared once and installed on every developer machine. A setup script in the repository generates new certificates for local domains as needed. The /etc/hosts entries or a local dnsmasq resolver make sure domains like myproject.local resolve to 127.0.0.1. With this combination of mkcert, a reverse proxy and DNS resolution, the local development environment matches the HTTPS production environment exactly, with no cloud dependencies and no Let's Encrypt limits.
Local SSL Certificates with mkcert: The Essentials at a Glance
mkcert installation
brew install mkcert nss (macOS). mkcert -install creates the local CA and registers it in the system trust store. One-time step per machine.
Generating certificates
mkcert domain.local *.domain.local 127.0.0.1. Never commit the certificate and key to Git. Use a setup script for a reproducible process.
Reverse proxy
Traefik for dynamic service discovery via labels. Nginx for explicit, easy to read configuration. Caddy for minimal configuration.
Team rollout
Share rootCA.pem (without the key). Copy it into the CAROOT directory on new machines and run mkcert -install. Never share rootCA-key.pem.