Guide
Laravel Forge SSL renewal failed
Forge renewal logs green while nginx still serves an old chain. Confirm live TLS on the site hostname, re-run Let’s Encrypt from Forge, and reload the daemon.
Updated May 16, 2026 824 words
Laravel forge ssl expired on a Forge server means the nginx listener is still binding last cycle’s PEM—even when the Forge SSL screen shows a recent Let’s Encrypt request.
Forge teams often manage dozens of PHP apps on one Ubuntu box. Renewal failures surface as customer NET::ERR_CERT_DATE_INVALID while SSH checks still show fresh files under /etc/letsencrypt/live. The split is almost always presentation, not issuance.
Treat Forge as orchestration: verify what strangers receive on port 443 before you redeploy Laravel or recycle PHP-FPM.
First response
Open the incident with the public site URL, screenshot of the browser warning, and output from an external TLS probe on that exact hostname (not the server IP alone). Note whether SSL was issued through Forge’s Let’s Encrypt integration or an imported commercial certificate.
If you have SSH, capture sudo certbot certificates (or the Forge-managed cert path Forge documents for your stack) and the notAfter from openssl against the live domain. Paste Forge’s SSL log lines around the last renewal attempt—look for HTTP-01 failures, missing webroot, or nginx test failures that blocked reload.
Document PHP/Laravel version only after TLS is proven expired externally. Queue workers and Horizon are irrelevant when nginx never loaded the renewed fullchain. Link follow-up tasks to the listener, not the artisan layer.
If Forge shows renewal success in email but the site fails, SSH in as the forge user and list the site’s nginx configuration under /etc/nginx/sites-enabled/—grep ssl_certificate for the path in use. Compare file mtime on that PEM against the Let’s Encrypt live directory. Incidents during zero-downtime deploys often correlate with Envoyer symlinking releases while SSL paths still reference an old deployment directory.
Capture whether Octane, FrankenPHP, or classic PHP-FPM serves the app; none of those change the certificate nginx presents. If you terminate TLS at Cloudflare, run openssl against DNS-only origin IP with correct SNI before blaming Forge.
See also How to fix an expired SSL certificate and Certbot renewal failed troubleshooting.
What this error means
Browsers validate the certificate chain nginx presents during the TLS handshake. When notAfter is in the past, PKIX validation stops before HTTP reaches Laravel’s public/index.php.
Forge provisions certificates per site and writes nginx server blocks that reference /etc/nginx/ssl/<site-id> or Let’s Encrypt paths. A successful ACME order does not automatically mean workers picked up new material: nginx -t failure, a missed service nginx reload, or a stale symlink under the site’s SSL directory leaves the old leaf on the wire.
The laravel forge ssl expired symptom can also mean you renewed the wrong site on a shared server—SNI routes shop.example.com to a different server block than api.example.com. Each Forge site record needs its own valid cert for every hostname listed in the site’s Aliases panel.
Common causes
Scheduled Let’s Encrypt renewal fails when port 80 is blocked by a CDN orange-cloud proxy, when .well-known is routed to Laravel instead of nginx static handling, or when a redirect forces HTTPS before the HTTP-01 challenge is served.
Forge’s “Protect” or custom nginx snippets sometimes override the default location /.well-known/acme-challenge/ block. After OS upgrades, certbot or the LetsEncrypt binaries Forge calls may lag until you update the server’s packages from the Forge dashboard.
Imported wildcard or commercial certs expire silently because Forge does not auto-renew them; teams forget the mailbox that received the reissue approval. Multi-server clusters drift when you renew on a primary but Forge load balancing or a floating IP still hits a secondary with the old bundle.
Disk-full /tmp or permission changes on forge user-owned challenge directories produce intermittent renewal success in logs but no deploy step. Finally, mixing Cloudflare “Full (strict)” with an expired origin cert looks like an edge problem when openssl to the origin IP shows the truth.
Forge daemons that watch certificate folders may restart PHP without reloading nginx—operators see new files on disk and assume presentation changed. Scheduled certbot renew on a server also managed manually in Forge can renew a different lineage than the UI expects if two accounts own overlapping server_name lists. When laravel forge ssl expired follows a server provider migration, check that restored snapshots did not resurrect year-old /etc/letsencrypt/archive trees without matching renewal configs.
Confirm from the outside
Run checks from a network that is not the Forge server itself:
echo | openssl s_client -connect shop.example.com:443 -servername shop.example.com 2>/dev/null \
| openssl x509 -noout -dates -subject -issuer -serial
Compare serial and notAfter to what Forge’s SSL tab claims. Test every alias hostname configured under Sites → Your Site → Meta → Aliases.
curl -vI --connect-timeout 15 https://shop.example.com/ 2>&1 \
| grep -Ei 'expire|SSL certificate problem|subject:|issuer:'
On the server (SSH as forge or root per your policy):
sudo nginx -t
sudo ls -la /etc/nginx/ssl/
sudo certbot certificates 2>/dev/null | sed -n '1,80p'
Forge UI path: Servers → [server] → Sites → [site] → SSL → Logs. Retry renewal only after HTTP-01 to http://shop.example.com/.well-known/acme-challenge/test returns reachable from the public internet (use curl from your laptop).
Browser spot-check
Open DevTools → Security on the failing URL and note certificate transparency logs if enabled. Laravel Forge teams sometimes test from the server’s own curl localhost, which hits a default vhost with a different cert than public DNS. Compare incognito on mobile LTE against office network to rule out corporate TLS inspection showing a substitute error.
How to read the output
Past notAfter= on the leaf means customers fail regardless of Forge’s green checkbox. Mismatched subject SANs versus the URL bar indicate you fixed the wrong site. If serial unchanged after “renewal succeeded,” nginx was not reloaded or points at a different directory than certbot updated.
Fix checklist
- In Forge, open SSL and read the latest failure reason; download or copy the Let’s Encrypt log snippet into the ticket.
- Ensure DNS A/AAAA records point at this server (or pause CDN proxy) until HTTP-01 succeeds.
- Confirm nginx serves challenges: temporarily disable forcing HTTPS for
/.well-knownif a custom config steals the path. - Click Obtain / Renew Certificate (or re-install imported PEM with full chain).
- SSH:
sudo nginx -tthensudo service nginx reload(orsystemctl reload nginx). See nginx reload after certbot renew. - Re-run the same openssl command from outside; serial must change and
notAftermust be in the future. - Hit Laravel routes (
/,/login, API health) over HTTPS from CI or mobile data. - Re-enable CDN SSL mode only after origin presents a valid chain.
Roll out and verify
Schedule Forge SSL work during a window when you can SSH and reload nginx without a solo maintainer racing a deploy pipeline. After renewal, compare certificate serials on every load-balanced backend if the site uses multiple servers—Forge server transfers sometimes copy nginx configs but not matching /etc/letsencrypt/live trees.
Run application smoke tests that matter for revenue: OAuth callbacks, Stripe webhooks, and SPA axios calls to the API subdomain. Horizon and queues do not exercise TLS; hit a Sanctum-protected route or health URL from GitHub Actions using the same DNS resolvers customers use.
Archive redacted openssl transcripts and Forge SSL log excerpts in the ticket closure. Note the Forge recipe version and whether .env APP_URL still matches the hostname you validated. If you use Envoyer or GitHub Actions deploy hooks, confirm deploy did not overwrite custom ssl_certificate paths in site-specific nginx includes.
When to use automation
Forge email reminders help, but they do not prove nginx presentation. Run periodic live TLS checks on each customer-facing hostname and alias, the same names users type—not merely “server IP up.” Pair certificate expiry with registrar RDAP where revalidation depends on domain ownership.
Use ssldays free check after every renewal and before change windows. Scheduled monitoring with alerts at 30, 14, 7, and 1 day catches failed HTTP-01 early on busy Forge servers.
Frequently asked questions
Does `php artisan` fix SSL?
No. TLS terminates in nginx (or a upstream proxy) before PHP runs.
Wildcard on Forge?
Use DNS-01 with a supported DNS provider integration; HTTP-01 alone will not issue *.example.com.
Multiple sites on one IP?
Each server_name needs matching certificates; default_server fallback often serves an expired default cert.
Zero-downtime deploy broke SSL?
Deploy scripts that replace nginx configs can drop ssl_certificate directives—diff server blocks before closing the incident.
See what the server presents right now
Free live certificate check — no signup.