// feedback Sign in to leave feedback about this page. Sign In →
all topics

Server Setup

Using Let's Encrypt

Free TLS certificates that renew themselves — what problem they solve, and how to wire auto-renewal into a Lightsail site without fighting your own deploy scripts.

TL;DR

Let’s Encrypt is a free, automated, nonprofit certificate authority (CA) run by the Internet Security Research Group (ISRG). It issues domain-validated TLS certificates — the same kind you’d otherwise buy from Sectigo, DigiCert, or your registrar — at no cost, using a protocol called ACME (Automatic Certificate Management Environment) instead of a web form and a credit card. The certbot client, maintained by the EFF, is the standard way to speak ACME from a Linux server.

The Lightsail setup article mentioned Let’s Encrypt as one of two ways to get a certificate, and built its template around the other one — a commercial CA’s .crt / .key files dropped into website/csr/ and installed with copythem.sh. This article covers the Let’s Encrypt path in full: what it actually solves, what breaks when it isn’t automated, and step-by-step directions for adding it to a site already built from that template — without certbot fighting your deploy scripts for control of the Apache config.

What Problem It Solves

Before Let’s Encrypt launched in 2016, getting a browser-trusted certificate meant paying a CA anywhere from $10 to $100+ a year, proving domain ownership through a manual email or DNS step, and remembering to repeat the whole process annually. That friction had real consequences:

  • Cost kept small sites on plain HTTP. A hobby project or internal tool often wasn’t worth an annual cert fee, so it shipped without TLS — and modern browsers now flag plain HTTP sites as “Not Secure” right in the address bar.
  • Manual renewal is a once-a-year task nobody remembers. A cert that must be renewed by hand, on a human’s schedule, eventually gets forgotten. An expired certificate doesn’t degrade gracefully — browsers hard-block the page with a full-screen warning until it’s fixed.
  • HTTP/2 and modern browser features expect HTTPS. Geolocation, service workers, and HTTP/2’s connection multiplexing are unavailable or crippled over plain HTTP in most browsers.

Let’s Encrypt removes the cost entirely and, more importantly, makes the whole process scriptable. Because issuance is an API call instead of a web form, renewal can run unattended on a timer — which turns an easy-to-forget yearly chore into a problem that, once set up correctly, you never think about again.

How It Works: the ACME Protocol

Let’s Encrypt doesn’t take your word for it that you own yoursite.com. It proves it, automatically, using a challenge-response exchange called domain validation. The most common form for a single server is the HTTP-01 challenge:

Three-box diagram. Certbot on your Lightsail server requests a certificate from the Let's Encrypt CA. The CA issues a random token and asks its own validation servers to fetch it back over plain HTTP from your domain. Apache on your domain serves the token file from the .well-known/acme-challenge/ path in the docroot, proving control of the domain rather than just the server. On success the CA signs the certificate and certbot downloads it to /etc/letsencrypt/live/.
Domain validation happens over plain HTTP on port 80 — the certificate that comes out the other end is what enables HTTPS, not a prerequisite for it.
  1. Certbot asks Let’s Encrypt for a certificate covering one or more domain names.
  2. Let’s Encrypt issues a random token and asks its own validation servers — from multiple network locations, to resist localized attacks — to fetch that token back from your domain at http://yoursite.com/.well-known/acme-challenge/<token>.
  3. Certbot places the matching file at that exact path (either by writing directly into your docroot — the webroot plugin — or by briefly running its own listener on port 80).
  4. If the fetched content matches, Let’s Encrypt has proof you control the domain (not just a server, but the specific one DNS points at) and signs a certificate.

Wildcard certificates (*.yoursite.com) use a different challenge — DNS-01, which requires proving control of DNS itself by publishing a TXT record via your DNS provider’s API. That needs a DNS plugin specific to your registrar and is outside the scope of this article; the HTTP-01 flow above covers a single domain plus its www alias, which is what nearly every small site needs.

Why Certificates Only Last 90 Days

Commercial certs are often valid for one or two years. Let’s Encrypt certificates are valid for exactly 90 days, and that’s a deliberate design choice, not a limitation:

  • It limits the damage from a leaked key. A certificate whose private key leaks is only useful to an attacker for a bounded window, not years.
  • It forces automation. A 90-day cycle is too frequent to renew by hand reliably, which pushes every user toward scripted renewal — and a scripted process doesn’t get forgotten the way an annual one does.
Timeline from day 0 to day 90. Day 0: certificate issued. Days 60 to 90: certbot's systemd timer checks twice daily and renews once fewer than 30 days remain, shown as a shaded renewal window with several check points inside it. Day 90: hard expiry, which must never be reached. A note explains that a renewal-hooks deploy script reloads Apache automatically so the new certificate takes effect with no manual step and no outage.
Certbot doesn’t wait until day 89. Its timer checks twice a day and renews as soon as a cert is within 30 days of expiry, leaving a wide margin for a failed attempt to be retried before anything actually breaks.

Problems a Site Can Have Without This Set Up Correctly

Most Let’s Encrypt failures aren’t protocol problems — they’re operational ones that show up months after the initial setup, when nobody is watching:

  • Silent expiry. The single most common outage: renewal was never automated, or the automation quietly broke, and the cert expires with no warning until a visitor sees a browser error. Section below covers verifying the timer is actually active.
  • Port 80 closed or firewalled. HTTP-01 validation happens over plain HTTP. If port 80 is blocked at the Lightsail firewall or inside ufw, both initial issuance and every renewal fail the same way.
  • DNS not fully propagated. Requesting a certificate before the domain’s A record has propagated everywhere fails validation from some of Let’s Encrypt’s vantage points even if it resolves correctly from your own machine.
  • Renewed cert never reaches Apache. Certbot writes new certificate files to disk, but Apache holds the old ones open in memory until it’s reloaded. Without a deploy hook, the cert file on disk is current but the live connection still serves the old (soon-to-expire) one.
  • Rate limits hit while testing. Let’s Encrypt allows 5 duplicate certificates for the same domain set per week. Repeatedly re-running a real request while debugging a config burns through that quickly — use --dry-run (below) or the --staging flag while testing.
  • Missing the www alias. A cert issued for only yoursite.com shows a name-mismatch warning to anyone who visits www.yoursite.com. Request both names together.

Fitting This Into the Lightsail Template

Most Let’s Encrypt tutorials tell you to run certbot --apache and let it handle everything. On a site built from the Lightsail template, don’t do that — it will work at first and then break your HTTPS on some future content deploy, in a way that gives no error and takes real digging to trace back to its cause.

Here’s the mechanism. The template treats your local working directory as the source of truth for Apache config: uploadall.bat SCPs mysitename-ssl.conf to the server and overwrites whatever is already there, every time. Certbot’s Apache plugin doesn’t know that convention exists — it edits the live vhost file on the server directly, adding its own SSLCertificateFile lines to the copy that’s currently running. That works fine right up until the next time you push an unrelated content change with uploadall.bat (or, on this template, even uploadcontent-web.bat followed by a later uploadall.bat). At that point your local template’s older, pre-certbot version of mysitename-ssl.conf silently replaces the one certbot patched, the site’s HTTPS reverts to whatever it pointed at before — typically a certificate path that no longer matches reality — and Apache can fail to start, or serve a broken or expired cert, for a reason that has nothing to do with whatever you actually just deployed.

The fix is to use certbot’s webroot plugin instead of the Apache plugin. It only ever writes challenge files inside your existing docroot to prove domain ownership, and it never touches Apache config at all. You keep full control of mysitename-ssl.conf in your local template — you make one manual edit, pointing its cert paths at /etc/letsencrypt/live/ instead of the /etc/ssl/ paths that copythem.sh populates for a commercial-CA cert — and because that edit lives in your local template like any other change, it survives every future deploy instead of being overwritten by it. Everything else about the deploy workflow stays exactly as documented in the Lightsail article.

Side-by-side comparison. Left, labeled Commercial CA csr workflow: local website/csr/ holds mysitename.key, mysitename.crt, and mysitename-intermediate.pem, deployed by uploadall.bat and copythem.sh into /etc/ssl/certs/ and /etc/ssl/private/ on the server, renewed manually about once a year. Right, labeled Let's Encrypt webroot workflow: no local csr files at all, certbot runs directly on the server and writes fullchain.pem and privkey.pem into /etc/letsencrypt/live/mysitename.com/, renewed automatically every 90 days by certbot.timer and a deploy hook. A caption notes both paths feed the same two directives in mysitename-ssl.conf, only the file paths change.
The two paths converge on the same two Apache directives — SSLCertificateFile and SSLCertificateKeyFile — in mysitename-ssl.conf. Everything upstream of those two lines is different, and everything downstream is identical.
Do this after setup.sh has enabled the HTTP vhost and your domain resolves to the server — HTTP-01 validation needs a live, reachable http://yoursite.com to write the challenge file into.

If your site is already running on HTTPS — which it is, if you’re following this article to switch from a commercial CA — mysitename-ssl.conf also carries a rule that redirects every plain-HTTP request to HTTPS, including whatever path the ACME challenge would use:

<VirtualHost *:80>
  RewriteEngine On
  RewriteCond %{HTTPS} off
  RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI}
</VirtualHost>

That’s not a problem. Let’s Encrypt’s validation servers follow HTTP→HTTPS redirects during the HTTP-01 challenge as a matter of course — it’s standard behavior, not an edge case, since redirecting to HTTPS is the default state of most real sites. The redirect just adds one hop: the validation request lands on port 443 instead of port 80, but the :443 vhost serves the exact same DocumentRoot that :80 does, so the challenge file certbot wrote into your docroot is still sitting right where it’s expected. The only requirement is that whatever’s currently installed on port 443 completes a normal TLS handshake — true for both a renewal and a first-time swap from a commercial cert, since in either case a valid certificate is already live when the request comes in.

The case that would break is a brand-new site that has never had any certificate on port 443 at all. If update-ssl.sh ran before any certificate existed, the redirect target would have nothing valid to complete a TLS handshake with, and validation could never succeed — a chicken-and-egg deadlock. That’s exactly what the blockquote above is protecting against: on a fresh site, stay on the plain HTTP-only vhost (no redirect at all) until the first certificate is issued, whichever CA it comes from.

Step by Step: Adding Let’s Encrypt to an Existing Site

Step 1 — Install Certbot

SSH into your server and install certbot without the Apache plugin:

# Ubuntu 24.04 ships certbot in the default repos
sudo apt update
sudo apt install certbot -y

Step 2 — Request the Certificate via Webroot

Point -w at your site’s docroot (the same path mysitename.conf already serves) and list every name the cert should cover:

sudo certbot certonly --webroot \
  -w /var/www/mysitename/mysitename.com \
  -d mysitename.com -d www.mysitename.com

Certbot asks for an email address (used only for expiry warnings and urgent notices from Let’s Encrypt, never spam) and agreement to the subscriber agreement, then writes four files:

/etc/letsencrypt/live/mysitename.com/
├── fullchain.pem    ← certificate + intermediate chain (use this, not cert.pem)
├── privkey.pem      ← private key
├── cert.pem         ← leaf certificate only
└── chain.pem        ← intermediate chain only
Testing without burning rate limit: add --dry-run to any certbot command to simulate the full issuance flow against Let’s Encrypt’s staging environment. It exercises the same validation path without counting against the real rate limits or producing a browser-trusted cert — run it first if you’re unsure the webroot path or DNS is correct.

Step 3 — Point Your SSL vhost at the New Cert

Edit website/apache2/mysitename-ssl.conf in your local working directory — not on the server — so the change survives the next deploy:

# Before (commercial CA, installed by copythem.sh)
SSLCertificateFile      /etc/ssl/certs/mysitename.crt
SSLCertificateKeyFile   /etc/ssl/private/mysitename.key
SSLCertificateChainFile /etc/ssl/certs/mysitename-intermediate.pem

# After (Let's Encrypt, managed by certbot)
SSLCertificateFile    /etc/letsencrypt/live/mysitename.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/mysitename.com/privkey.pem

fullchain.pem already includes the intermediate chain, so the separate SSLCertificateChainFile directive isn’t needed — remove it if present.

Step 4 — Deploy and Reload

# From your local working directory
uploadall.bat

Then SSH in and re-enable the SSL vhost so Apache picks up the config change:

cd /var/www/mysitename/website/apache2
sudo ./update-ssl.sh

Test at https://mysitename.com. The browser padlock should show a certificate issued by R-series, Let’s Encrypt rather than your previous CA.

Step 5 — Verify Auto-Renewal Is Actually Active

Installing certbot via apt also installs a systemd timer that runs the renewal check twice a day. Confirm it’s enabled — don’t just assume it is:

systemctl status certbot.timer
systemctl list-timers | grep certbot

If it’s not listed or not active, enable it explicitly:

sudo systemctl enable --now certbot.timer

Step 6 — Add a Deploy Hook to Reload Apache

A renewed certificate on disk does nothing until Apache reloads. Certbot runs every executable script it finds in /etc/letsencrypt/renewal-hooks/deploy/ after any successful renewal — regardless of whether that renewal was triggered by the timer, cron, or a manual run — so this is the most reliable place to put the reload, rather than a one-off flag you might forget to repeat:

sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-apache.sh > /dev/null <<'EOF'
#!/bin/bash
systemctl reload apache2
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-apache.sh

Step 7 — Do a Full Dry Run of Renewal

Confirm the entire pipeline — validation, hook, reload — works before trusting it to run unattended for the next 90 days:

sudo certbot renew --dry-run

A clean dry run means the timer will handle real renewals the same way with no further action from you. Check /var/log/letsencrypt/letsencrypt.log if anything reports failure.

Multiple Sites on One Server

Following the Lightsail article’s pattern of one /var/www/<sitename>/ tree per site, request a separate certificate per domain — certbot keeps them independent under /etc/letsencrypt/live/<domain>/, and the single certbot.timer renews all of them on its normal schedule:

sudo certbot certonly --webroot -w /var/www/mysitename/mysitename.com \
  -d mysitename.com -d www.mysitename.com

sudo certbot certonly --webroot -w /var/www/othersitename/othersitename.com \
  -d othersitename.com -d www.othersitename.com

Repeat Steps 3–4 for each site’s own -ssl.conf. Step 6’s deploy hook only needs to exist once — it reloads Apache for every site’s renewal, since a single systemctl reload apache2 picks up every enabled vhost.

Checklist

Prerequisites:
  [ ] Domain resolves to the server; HTTP vhost already live (setup.sh run)
  [ ] Port 80 open at the Lightsail firewall and in ufw

Issue the certificate:
  [ ] certbot installed (apt install certbot — no apache plugin)
  [ ] certonly --webroot run with both bare domain and www names
  [ ] --dry-run tested first if anything about the setup was uncertain
  [ ] fullchain.pem and privkey.pem confirmed present under /etc/letsencrypt/live/

Wire it into the template:
  [ ] Local mysitename-ssl.conf updated to point at /etc/letsencrypt/live/ paths
  [ ] SSLCertificateChainFile directive removed (fullchain.pem already includes it)
  [ ] uploadall.bat run, then update-ssl.sh re-run server-side
  [ ] https://yoursite.com confirmed showing a Let’s Encrypt-issued cert

Auto-renewal:
  [ ] certbot.timer confirmed active (systemctl status certbot.timer)
  [ ] renewal-hooks/deploy/reload-apache.sh created and executable
  [ ] certbot renew --dry-run passes end to end
  [ ] Calendar reminder set to spot-check the cert expiry date once, ~60 days out
top