SSL / HTTPS¶
Frappe Manager provides built-in SSL certificate management using Let's Encrypt. You can secure your benches with trusted HTTPS certificates using either HTTP-01 (the default) or DNS-01 (Cloudflare) challenge types.
Overview¶
When you run fm ssl add, Frappe Manager:
- Installs acme.sh on first use into
~/frappe/services/nginx-proxy/ssl/acmesh/.acme.sh/ - Runs the ACME challenge (HTTP-01 or DNS-01) to prove domain ownership to Let's Encrypt
- Obtains the certificate and private key
- Installs the certificate into your bench's nginx configuration automatically
- Enables HTTPS redirect so all HTTP traffic redirects to HTTPS
- Handles renewal automatically when certificates are about to expire (< 30 days)
SSL is supported for both FM-managed benches and external Docker projects sharing FM's nginx-proxy network (standalone mode).
Quick Start
For most users (HTTP-01):
# Test first (staging server)
fm ssl add mybench example.com --dry-run
# Then issue for real
fm ssl add mybench example.com
For Cloudflare users (DNS-01):
# Save credentials once
fm ssl dns-config cloudflare --api-token YOUR_TOKEN
# Then issue
fm ssl add mybench example.com --challenge dns01
Automate renewals:
Challenge Types¶
FM supports two ways to prove domain ownership to Let's Encrypt:
| HTTP-01 | DNS-01 | |
|---|---|---|
| How it works | Let's Encrypt fetches a validation file from http://yourdomain/.well-known/acme-challenge/ |
You (FM) add a TXT record to your DNS zone programmatically |
| Port 80 required | ✅ Yes | ❌ No |
| Wildcard certs | ❌ No | ✅ Yes |
| Best for | Most production setups with public domains | Firewalled servers, wildcard domains, internal infrastructure |
| Supported DNS providers | N/A | Cloudflare (built-in support) |
| Setup complexity | Simple (default) | Requires API credentials |
Quick decision guide: - ✅ Use HTTP-01 if your domain points to the server and ports 80/443 are open (simplest, default) - ✅ Use DNS-01 if port 80 is blocked, you need wildcard certificates, or testing on internal networks
Before you start¶
HTTP-01 checklist¶
- Your domain's A record points to this server's public IP
- Port 80 and 443 are open in your firewall / security group
- No other process is occupying port 80 on the host
DNS-01 checklist¶
- You have Cloudflare managing the DNS zone for your domain
- You have created a Cloudflare API Token with Zone → DNS → Edit permission
- DNS credentials are saved with
fm ssl dns-config cloudflare
Always dry-run first
Run with --dry-run before issuing real certificates. The dry-run uses the Let's Encrypt staging server — it validates your setup without consuming your rate limit quota (50 certificates per registered domain per week, 5 per identical set of names per week).
HTTP-01 setup¶
1. Dry run¶
Validates that Let's Encrypt can reach your server and complete the challenge:
Expected output: a message like Cert verified OK or acme.sh's staging success banner. If it fails, see Troubleshooting below.
2. Issue the certificate¶
FM will:
- Start the acme.sh HTTP-01 challenge handler inside the FM nginx-proxy
- Obtain the certificate from Let's Encrypt
- Install it into the bench's nginx configuration
- Reload nginx
3. Verify¶
Then confirm from outside:
Or with openssl:
openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null \
| openssl x509 -noout -dates -subject
DNS-01 setup (Cloudflare)¶
1. Create a Cloudflare API Token¶
- Go to https://dash.cloudflare.com/profile/api-tokens
- Click Create Token → use the Edit zone DNS template
- Set Zone Resources to the specific zone(s) you need
- Copy the token — you will not see it again
API Token vs Global API Key
Always prefer the API Token. The Global API Key grants full account access and is a higher-risk credential. If you must use it, pass --api-key and --email instead of --api-token.
2. Save credentials¶
Global (applies to all benches unless overridden):
Bench-specific override:
Verify:
3. Dry run¶
acme.sh adds a _acme-challenge.example.com TXT record, waits for propagation, validates, then removes it.
DNS propagation
TXT record propagation typically takes 30 seconds to 5 minutes, but can take longer on some registrars. If the dry-run times out, retry or pass --wait-for-dns to let FM poll automatically (every 30 s, up to 5 minutes).
4. Issue the certificate¶
5. Wildcard certificates¶
DNS-01 is the only challenge type that supports wildcards. Issue a wildcard alongside the apex:
Note
Many browsers require both the apex (example.com) and the wildcard (*.example.com) to be covered. Issue both, or use a SAN cert covering both names.
6. CNAME delegation¶
If your DNS zone is hosted elsewhere but you want to delegate just the _acme-challenge subdomain to Cloudflare, add a CNAME in your primary DNS:
Then pass the delegation target to FM:
fm ssl add mybench example.com --challenge dns01 --cname _acme-challenge.example.com.cf-delegated.example.com
Standalone mode¶
For Docker projects that are not FM benches but are on the fm-global-frontend-network, FM's nginx-proxy can front them and handle their certificates.
Renew and list work the same way with --standalone:
Renewals¶
Manual¶
# Renew all certs for one bench
fm ssl renew mybench
# Renew one specific domain
fm ssl renew mybench example.com
# Force renewal even if not yet due (< 30 days remaining)
fm ssl renew mybench example.com --force
# Renew across all benches
fm ssl renew --all
Automatic (cron)¶
Let's Encrypt certificates are valid for 90 days. FM renews when fewer than 30 days remain. Set up a daily cron job:
Add:
This runs at 3 am every day. FM skips benches that don't need renewal, so running it daily is safe.
Certificate lifetime is shrinking
Starting March 2026 Let's Encrypt began issuing certificates with a maximum validity of 200 days, with a target of 45 days by early 2028. Automated renewal is no longer optional — manual renewal will become impractical. Set up the cron job now.
Removing a certificate¶
fm ssl remove mybench example.com
# Skip confirmation prompt
fm ssl remove mybench example.com --yes
Troubleshooting¶
Quick diagnostic checklist¶
Run these in order before diving deeper:
# 1. Is the domain resolving to this server?
dig +short example.com
# 2. Is port 80 reachable? (HTTP-01 only)
curl -v http://example.com/.well-known/acme-challenge/test
# 3. Is the certificate valid and trusted?
openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null \
| openssl x509 -noout -dates -issuer -subject
# 4. Does the cert chain look complete?
openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null \
| grep -E "^(depth|verify)"
# 5. Are FM containers running?
fm list
Error reference¶
HTTP-01 failures¶
| Symptom | Cause | Fix |
|---|---|---|
Connection refused on port 80 |
Port blocked by firewall | Open port 80 in your cloud provider's security group / ufw |
Timeout during connect |
Wrong IP in DNS A record | Update A record to point to this server; wait for propagation |
Invalid response from http://example.com/.well-known/acme-challenge/ |
Another web server is answering port 80 | Stop or reconfigure the conflicting service; only FM's nginx-proxy should own port 80 |
Too many certificates rate limit error |
Exceeded 5 certs for the same name set this week | Wait until next week or use --dry-run during testing |
DNS-01 failures¶
| Symptom | Cause | Fix |
|---|---|---|
DNS record not found |
TXT record not propagated yet | Wait 1–5 minutes and retry; use --wait-for-dns |
Authentication error |
API token lacks DNS Edit permission | Recreate the token with Zone → DNS → Edit |
Invalid API Token |
Token was revoked or typo | Run fm ssl dns-config cloudflare --show to inspect; re-save if needed |
| Old TXT record conflict | Previous failed attempt left a stale record | Delete _acme-challenge.example.com TXT records from Cloudflare dashboard manually |
Certificate errors in browser¶
| Symptom | Cause | Fix |
|---|---|---|
NET::ERR_CERT_DATE_INVALID |
Certificate expired | Run fm ssl renew mybench |
NET::ERR_CERT_AUTHORITY_INVALID |
Self-signed or incomplete chain | FM uses Let's Encrypt — check that fullchain.pem is served, not just cert.pem |
NET::ERR_CERT_COMMON_NAME_INVALID |
Domain mismatch | Confirm the cert's SANs cover the domain you're accessing: openssl x509 -noout -ext subjectAltName |
| Clock skew warning | Server time is wrong | Run timedatectl or date -u; sync NTP: sudo timedatectl set-ntp true |
Incomplete chain — the silent failure
Chrome auto-fetches missing intermediate certificates via the AIA extension, so a site may appear valid in Chrome but fail in curl, Firefox, and API clients. Always make sure nginx is serving fullchain.pem (which includes the intermediates), not just cert.pem. FM handles this automatically, but if you have manually customised nginx config, verify with:
Renewal failures¶
| Symptom | Fix |
|---|---|
| Cron ran but cert not renewed | Check cron logs: grep CRON /var/log/syslog; confirm FM is on $PATH in the cron environment |
force renewal failed |
Run fm ssl acme-sh --info -d example.com to inspect acme.sh state |
| DNS credentials expired | Re-save with fm ssl dns-config cloudflare --api-token NEW_TOKEN and run fm ssl renew mybench --force |
Certificate file locations¶
All SSL files are stored in the global nginx-proxy service directory. Understanding this structure is helpful for debugging:
| Path | Purpose |
|---|---|
~/frappe/services/nginx-proxy/ssl/acmesh/.acme.sh/ |
acme.sh installation and internal certificate database |
~/frappe/services/nginx-proxy/ssl/acmesh/<domain>/fullchain.pem |
Full certificate chain (includes intermediates) |
~/frappe/services/nginx-proxy/ssl/acmesh/<domain>/key.pem |
Private key for the domain |
~/frappe/services/nginx-proxy/certs/<domain>.crt |
Symlink to fullchain.pem (nginx-proxy reads this) |
~/frappe/services/nginx-proxy/certs/<domain>.key |
Symlink to key.pem (nginx-proxy reads this) |
~/frappe/services/nginx-proxy/vhostd/<domain> |
HTTP→HTTPS redirect configuration |
~/frappe/services/nginx-proxy/conf.d/<domain>.conf |
Nginx server block (standalone mode only) |
~/frappe/services/nginx-proxy/external_domains.toml |
Registry of standalone domain certificates |
Why symlinks?
FM uses symlinks so nginx-proxy can read certificates from a fixed location (certs/) while the actual certificate files are managed by acme.sh in its own directory structure (ssl/acmesh/).
Advanced: raw acme.sh access¶
FM exposes acme.sh directly for edge cases. Use this only if the fm ssl commands don't cover your need.
# List all certs acme.sh knows about
fm ssl acme-sh -- --list
# Detailed info for one domain
fm ssl acme-sh -- --info -d example.com
# Check acme.sh version
fm ssl acme-sh -- --version
# Upgrade bundled acme.sh
fm ssl acme-sh -- --upgrade
Danger
Commands run via fm ssl acme-sh bypass FM's certificate management. Certificates issued or removed this way will not be reflected in fm ssl list and FM will not manage their installation into bench nginx configs. Prefer fm ssl add/remove/renew for all normal workflows.
Security notes¶
- Credentials: Cloudflare API tokens are stored in FM's config directory (
~/.config/frappe-manager/). Restrict file permissions:chmod 600 ~/.config/frappe-manager/fm_config.toml. - Token scope: Use a per-zone API Token, not the Global API Key. If the token is compromised, you can revoke it without rotating your entire Cloudflare account.
- Certs in version control: Never commit
*.pemor*.keyfiles. Add them to.gitignore. - Staging vs production: Use
--dry-run(staging) during setup and testing. Production rate limits are shared across all users of a domain — hitting them blocks certificate issuance for everyone on that domain for up to a week.
See also
- fm ssl command reference — all flags and subcommands
- Cloudflare DNS Config — detailed Cloudflare token setup
- Environments — prod vs dev environment differences