Self-Hosted SSL Certificate Errors: Fixes
The Problem
Your self-hosted service shows SSL/TLS errors in the browser:
NET::ERR_CERT_AUTHORITY_INVALID
SSL_ERROR_BAD_CERT_DOMAIN
ERR_CERT_DATE_INVALID
ERR_SSL_PROTOCOL_ERROR
SSL: CERTIFICATE_VERIFY_FAILED
Mixed Content: The page was loaded over HTTPS but requested HTTP resource
These errors prevent HTTPS access or cause browsers to warn users about security.
The Cause
SSL certificate errors in self-hosted environments have a few distinct root causes:
| Error | Cause | Urgency |
|---|---|---|
ERR_CERT_AUTHORITY_INVALID | Self-signed certificate or untrusted CA | Common — cert not from a trusted authority |
ERR_CERT_COMMON_NAME_INVALID | Certificate doesn’t match the domain | Misconfigured cert or wrong domain |
ERR_CERT_DATE_INVALID | Certificate expired | Renewal failed — fix immediately |
ERR_SSL_PROTOCOL_ERROR | TLS version mismatch or broken handshake | Server configuration issue |
CERTIFICATE_VERIFY_FAILED | App/container can’t verify upstream SSL | Missing CA certificates in container |
Mixed Content | Page loads HTTP resources over HTTPS | App or proxy misconfiguration |
The Fix
Fix 1: Let’s Encrypt Certificate Not Issuing
Symptom: Reverse proxy shows self-signed cert. Let’s Encrypt challenge fails.
For HTTP-01 challenge (most common):
Let’s Encrypt needs to reach your server on port 80 from the internet.
# Verify port 80 is reachable from outside
curl -I http://yourdomain.com/.well-known/acme-challenge/test
Checklist:
- Port 80 forwarded from your router to your server
- DNS A record points to your public IP (not a local IP)
- Firewall allows inbound port 80
- No other service (Apache, another Nginx) is binding port 80
- Cloudflare proxy (orange cloud) is disabled for the domain during initial cert issuance
For DNS-01 challenge (wildcard certs, Cloudflare Tunnel):
DNS challenge doesn’t require port 80 but needs API access to your DNS provider:
# Verify DNS API credentials work
# Caddy with Cloudflare:
CLOUDFLARE_API_TOKEN=your-token caddy run
# Traefik with Cloudflare:
# Check CF_API_EMAIL and CF_API_KEY are set correctly
Fix 2: Certificate Domain Mismatch
Symptom: ERR_CERT_COMMON_NAME_INVALID — browser says cert is for wrong.domain.com
The certificate’s Subject Alternative Names (SANs) must include the exact domain you’re accessing.
Verify what domain the cert covers:
echo | openssl s_client -connect yourdomain.com:443 -servername yourdomain.com 2>/dev/null | openssl x509 -noout -text | grep -A1 "Subject Alternative Name"
Common causes:
- Certificate issued for
example.combut accessingapp.example.com(need wildcard or per-subdomain cert) - Nginx Proxy Manager assigned the wrong certificate to the proxy host
- Traefik router matches the wrong certificate resolver
Fix for Nginx Proxy Manager: Edit the proxy host → SSL tab → ensure the correct domain certificate is selected. If missing, request a new Let’s Encrypt cert for the exact domain.
Fix for Caddy: Caddy auto-provisions certificates for every domain in its config. Verify the domain in Caddyfile matches what you access:
app.yourdomain.com {
reverse_proxy localhost:8080
}
Fix for Traefik: Verify the router’s Host rule matches the domain:
labels:
- "traefik.http.routers.myapp.rule=Host(`app.yourdomain.com`)"
- "traefik.http.routers.myapp.tls.certresolver=letsencrypt"
Fix 3: Expired Certificate
Symptom: ERR_CERT_DATE_INVALID — certificate expired
Let’s Encrypt certificates expire every 90 days. Auto-renewal should handle this.
Nginx Proxy Manager: Renewal is automatic. If it fails:
- Check the SSL certificate list — is renewal failing?
- Verify port 80 is still reachable (required for renewal)
- Check Nginx Proxy Manager logs:
docker compose logs npm - Delete the certificate and re-issue it
Caddy: Renewal is fully automatic. If failing:
docker compose logs caddy | grep -i "cert\|tls\|renew"
Traefik: Check the ACME storage file:
docker exec traefik cat /acme.json | python3 -m json.tool | grep -A5 "notAfter"
Manual renewal (any provider):
# Using certbot
certbot renew --dry-run # Test first
certbot renew # Actually renew
Fix 4: SSL Protocol Error
Symptom: ERR_SSL_PROTOCOL_ERROR — connection reset during handshake
Cause 1: Service listening on HTTP, not HTTPS
Your reverse proxy is sending HTTPS to a backend that only speaks HTTP:
Browser → (HTTPS) → Reverse Proxy → (HTTPS) → Container expecting HTTP
Fix: Tell the proxy to use HTTP to the backend:
Nginx Proxy Manager: In the proxy host, set scheme to http (not https)
Caddy:
app.yourdomain.com {
reverse_proxy http://localhost:8080 # Explicit HTTP
}
Cause 2: Port conflict
The service is on a different port than expected. Verify:
docker exec myapp ss -tlnp
# Check which port the service actually listens on
Fix 5: Container Can’t Verify Upstream SSL
Symptom: App inside container throws CERTIFICATE_VERIFY_FAILED when connecting to external HTTPS services
Cause: Minimal container images (Alpine, distroless) may not have CA certificates installed.
Fix for Alpine:
RUN apk add --no-cache ca-certificates
Fix for Debian/Ubuntu:
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
Fix in Docker Compose (mount host CA certs):
volumes:
- /etc/ssl/certs:/etc/ssl/certs:ro
Fix 6: Mixed Content Warnings
Symptom: Browser shows mixed content warning; some resources load over HTTP
Cause: The application generates HTTP URLs for assets, links, or API calls instead of HTTPS.
Fix — tell the app it’s behind HTTPS:
Most apps need to know they’re behind an HTTPS reverse proxy:
# Common env vars to set
FORCE_HTTPS: "true"
BASE_URL: "https://app.yourdomain.com"
TRUSTED_PROXIES: "172.16.0.0/12"
For Nginx Proxy Manager, enable “Force SSL” on the proxy host.
For apps behind Caddy or Traefik, set the X-Forwarded-Proto: https header (usually automatic).
Prevention
Use a Reverse Proxy That Auto-Manages Certs
| Proxy | Cert Management | Effort |
|---|---|---|
| Caddy | Fully automatic, zero config | Lowest |
| Traefik | Automatic with cert resolver config | Low |
| Nginx Proxy Manager | UI-driven, one-click Let’s Encrypt | Low |
Caddy is the least error-prone — it auto-provisions and renews certificates with zero configuration beyond listing the domain name.
Monitor Certificate Expiry
Add certificate monitoring to your Uptime Kuma instance:
- Monitor type: Keyword or Certificate Expiry
- URL:
https://app.yourdomain.com - Alert when: Certificate expires within 14 days
Use DNS Challenge for Internal Services
If your services aren’t publicly accessible (VPN-only or LAN-only), use DNS challenge validation for Let’s Encrypt. This doesn’t require port 80 to be open.
Related
Get self-hosting tips in your inbox
Get the Docker Compose configs, hardware picks, and setup shortcuts we don't put in articles. Weekly. No spam.
Comments