SSL Certificates on a VPS: Set Up Renewal Once and Stop Thinking About It

Certificates expire every 60-90 days, and expiry is the most preventable outage a web studio sees. How we set up Let's Encrypt on client VPSes so renewal happens by itself and breakage announces itself before it happens.

SSL certificates on a VPS: set up renewal once and stop thinking about it

Every web studio has one story like this. A client calls on a Monday morning: the site shows a browser warning, customers see "not secure", the contact form refuses to submit. Nothing was changed. The certificate just expired, and nobody noticed for days.

Expired certificates are the most preventable production problem there is. The fix is not remembering to renew. Remembering fails at exactly the wrong moment, because the person who remembers is on holiday. The fix is setting things up so renewal happens without a human, and so that when it does break, something tells you days in advance.

What we use and why

We use Let's Encrypt on every client VPS. It is free, it is trusted by every browser, and it comes with a renewal model built for automation. Wildcard certificates are possible but rarely needed; one certificate covering the domain and www alias handles most sites, and a separate one for admin or API subdomains keeps the blast radius small: a renewal problem on admin.client.com shouldn't take down the main site, and vice versa.

The tool that matters is certbot with the nginx plugin. The plugin edits the nginx configuration itself, adds the redirect from HTTP to HTTPS, and installs a systemd timer that runs twice a day. You can read that timer with systemctl list-timers, and it should show a recent successful run. If the timer has never fired, renewal is not happening.

Renewal must be automatic, and tested

A Let's Encrypt certificate lives 90 days by design. That lifetime is a message: don't do this by hand. Manual renewal works once, twice, maybe the third time, and then someone leaves the company or the notes get lost.

Two settings make renewal unattended and self-testing:

  • certbot renew runs quietly and only acts when the certificate is within 30 days of expiry, so most runs do nothing;
  • the deploy hook reloads nginx after a successful renewal. Without it, nginx keeps serving the old certificate even after a new one lands on disk;

The step most setups skip is testing. Certbot ships certbot renew --dry-run, which runs the whole renewal against the Let's Encrypt staging environment. It touches nothing in production, and it proves the renewal path works: DNS resolves, the challenge passes, the timer runs. We run it at setup, after any nginx change, and before handing the VPS over to long-quiet operation. A renewal path that has never been tested should be assumed broken.

Rate limits are real

Let's Encrypt enforces limits, and the one that bites people is five duplicate certificates per week. If you delete a certificate and reissue it five times in a week while debugging, the next request gets rejected, and now you're waiting days with a production problem. This happens on projects that re-run provisioning scripts repeatedly.

If you're iterating on server setup, use the staging environment: it issues certificates that browsers won't trust, but it has much higher limits and lets you break the provisioning loop as many times as you need. Switch to production only when the setup works end to end. This habit has saved more debugging sessions than any other on this topic.

When renewal silently fails

Automation moves the failure somewhere quieter. The timer runs, renewal fails, nothing visible changes until the expiry date arrives. The usual causes, roughly in order of how often we see them:

  • DNS points somewhere else: the domain was moved to a new host, an A record changed, and the challenge can't reach this server;
  • nginx holds a server block that no longer matches the certificate's domains, so certbot can't find where to install the renewal;
  • port 80 is closed or redirected, because "nobody visits over HTTP anyway", and the HTTP-01 challenge needs exactly that port;
  • an old certbot or broken plugin after a system upgrade.

None of these announce themselves. The page keeps working until day 60, then falls off a cliff. What catches them is a monitoring job that runs openssl s_client or a simple library call against the production domain, computes the days left, and sends a warning at 21 days and again at 10. That is twenty lines in any language you like, running once a day, and it covers the gap between "the timer is green" and "the certificate is actually renewable".

One certificate, one place, no surprises

Two habits close out the topic. First, the certificate should live in exactly one place with standard names, referenced from nginx config, with no expired copies scattered under /etc/letsencrypt/archive. Second, treat the redirect as part of the certificate work: HTTP should 301 to HTTPS in the same nginx block certbot edits, so there's no second config drifting in a file nobody touched since the launch.

None of this is interesting work. That's the point. Certificates should be the least remarkable part of a VPS, and with a timer that renews, a hook that reloads, a dry-run that proves the path, and a daily check that measures days remaining, expiry becomes something you never have to think about again.