How to Create an SSL Certificate: Let's Encrypt with Certbot and Caddy, Tested (2026)

How to Create an SSL Certificate: Let's Encrypt with Certbot and Caddy, Tested (2026)

If you want to create an SSL certificate for your own server today, it costs nothing and takes a few seconds. The standard route is Let’s Encrypt. On a server running nginx, you install certbot and run sudo certbot --nginx -d example.com -d www.example.com. Certbot proves you control the domain, fetches the certificate, adds it to your config and sets up automatic renewal. With Caddy as your web server it’s even simpler: you write the domain name into the config and everything else happens on its own.

That’s the short answer. The long one matters more, because creating the certificate rarely goes wrong. What bites you is the renewal three months later. A certificate without working renewal is a time bomb, and most of the traps don’t produce an error. In our test, after a simulated server move, certbot renew printed “No renewals were attempted” and exited with code 0, meaning success, even though no certificate could be renewed anymore. And after a perfectly successful renewal, nginx kept serving the old certificate.

We ran all of this on our own server on November 5, 2026, using Certbot 2.9.0, nginx 1.24, Caddy 2.11.7 and Pebble, the official test certificate authority from Let’s Encrypt. Pebble speaks the same ACME protocol as the real CA but issues certificates no browser trusts. That let us provoke failures without hitting Let’s Encrypt’s rate limits. Every number in this article comes from those runs. If you want the background on what HTTPS is and what a certificate actually proves, read What is HTTPS?. This article is about doing it.

Video: Sarvin Style Coding walks through Certbot with nginx, both directly on a VPS and in Docker (August 2026).

How to create an SSL certificate: the four routes

Before you type a command, think about what the certificate is for. That decides which route fits.

RouteBest forEffortCostTrusted by browsers
Caddy (automatic)new servers, reverse proxies, Docker1 line of config$0yes
Certbot with nginx or Apacheexisting nginx/Apache servers2 commands$0yes
Certbot with DNS challengewildcard *.example.com, servers without public port 80DNS API access$0yes
Self-signed (openssl)testing, internal networks, development1 command$0no, warning
Paid certificate (OV/EV)when contracts or compliance require itapplication, vetting$50–500/yearyes

For almost every website, a free Let’s Encrypt certificate is the right choice. The encryption is exactly as strong as a paid one. Paid certificates additionally verify the organization behind the domain, but browsers stopped showing that difference years ago. There’s more on DV, OV and EV in our HTTPS article.

A quick note on the name: strictly speaking, “SSL” hasn’t existed for years. The protocol is TLS, and SSL has been switched off everywhere for security reasons. The certificate is the same, though. Whether you say SSL certificate, TLS certificate or HTTPS certificate, you mean an X.509 certificate that binds your domain name to a public key.

What has to be in place first

Let’s Encrypt only issues a certificate once you prove you control the domain. That happens automatically through the ACME protocol. For it to work, three things must be true:

  1. The DNS record points to your server. An A record (IPv4) and, if you use it, an AAAA record (IPv6) for every name that goes on the certificate, including www. See What is DNS? for how that works. Watch out for IPv6: if your domain has an AAAA record, Let’s Encrypt prefers to validate over IPv6. If that record points to an old address, validation fails even though IPv4 is correct.
  2. Port 80 is reachable from the internet. The most common validation method (HTTP-01) fetches a file over plain HTTP. A firewall that only opens 443 blocks it. In our test with the port closed, the error read Type: connection and connect: connection refused. Our Linux server setup guide covers which ports to open on a fresh server.
  3. A web server is already running, or port 80 is free if Certbot should start its own temporary server.

How domain validation works

There are three validation methods, called “challenges” in ACME terms:

  • HTTP-01: Certbot places a file containing a random token at http://your-domain/.well-known/acme-challenge/. Let’s Encrypt downloads it. If the content matches, you’ve proven you control the web server. This is the default.
  • DNS-01: You create a TXT record _acme-challenge.your-domain containing the token. This is the only way to get wildcard certificates, and it works even if the server isn’t reachable from the internet at all.
  • TLS-ALPN-01: Validation happens on port 443 using a special TLS extension. Caddy likes to use it; with Certbot it rarely matters.

A small robot courier delivers an envelope to a server while a large magnifying glass inspects it from above

One detail from our test that surprises people: the CA doesn’t check just once. Our nginx access log showed three requests for the same file per validation. The real Let’s Encrypt additionally validates from several locations around the world (“multi-perspective validation”). If your server geo-blocks certain countries, validation can fail even though it works from where you’re sitting.

Route 1: create an SSL certificate with Certbot and nginx

This is the classic setup on Ubuntu and Debian. Install Certbot and the nginx plugin:

sudo apt update
sudo apt install certbot python3-certbot-nginx

Your nginx needs a server block with the right server_name, even if it only listens on port 80 for now:

server {
    listen 80;
    server_name example.com www.example.com;
    root /var/www/example;
}

Then request the certificate:

sudo certbot --nginx -d example.com -d www.example.com

The first time, Certbot asks for an email address and agreement to the terms. Then it validates the domain, stores the certificate under /etc/letsencrypt/live/example.com/, adds listen 443 ssl and the certificate paths to your server block, sets up an HTTP-to-HTTPS redirect and reloads nginx. That’s it.

If you run nginx as a reverse proxy in front of an app, our nginx reverse proxy guide has the full config. For Apache the process is identical, just with python3-certbot-apache and certbot --apache.

If Certbot shouldn’t touch your config: webroot

Some admins don’t want a tool editing their nginx config. That’s what certonly with webroot mode is for. Certbot only places the challenge file in a directory your web server already serves:

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

Then you add the paths yourself:

server {
    listen 443 ssl;
    http2 on;
    server_name example.com www.example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    root /var/www/example;
}

In our test, issuing a two-name certificate via webroot took 2.4 seconds, standalone mode 3.1 seconds. Speed is not a factor.

Webroot has two failure modes that look very different:

  • The directory doesn’t exist: Certbot aborts immediately with /var/www/wrong does not exist or is not a directory without even contacting the CA. That’s the harmless case.
  • The directory exists, but nginx doesn’t serve it. This happens when -w points somewhere other than the root in your server block, or when a location rule sends /.well-known/ to your app. Certbot writes the file, Let’s Encrypt gets a 404, and the error says Type: unauthorized with returned 404. It sounds like a permissions problem, but it’s a path problem.

The second case is nasty because it often works the first time and only fails at renewal, after someone has reorganized the nginx config. A robust fix is a dedicated ACME location that always points to the same directory regardless of what the rest of the site does:

location ^~ /.well-known/acme-challenge/ {
    root /var/www/letsencrypt;
}

With -w /var/www/letsencrypt in Certbot, this works for every site on the server, whether it’s WordPress, a Node app or a reverse proxy behind it.

Standalone: when no web server is running yet

certbot certonly --standalone -d example.com starts its own small web server on port 80 for validation. That’s handy for servers without a web server, like a mail server that just needs a certificate. The catch comes later: at renewal time, port 80 must be free again. If nginx is running there by then, renewal fails. That’s exactly what happened in our dry run (the test used port 5002; on a real server it’s port 80):

Failed to renew certificate app.gmtest.example with error:
Could not bind TCP port 5002 because it is already in use by another process

If you started with standalone and later installed a web server, switch renewal to webroot, or add --pre-hook "systemctl stop nginx" --post-hook "systemctl start nginx". The second option means a few seconds of downtime on every renewal.

Route 2: create an SSL certificate with Caddy, no Certbot needed

Caddy is a web server that handles HTTPS itself. The complete config for a site with a certificate looks like this:

example.com {
    root * /var/www/example
    file_server
}

Or as a reverse proxy in front of an app:

app.example.com {
    reverse_proxy localhost:3000
}

On startup Caddy fetches the certificate, redirects HTTP to HTTPS and renews in time. There’s no timer, no hook and no reload to forget. Against our test CA, Caddy was serving the site over HTTPS 5.6 seconds after starting, and a plain HTTP request automatically got a 308 redirect to HTTPS.

On our own server, Caddy manages over a thousand certificates, including the one for getmind.io. At the time of writing ours was issued by Let’s Encrypt (intermediate YE1) on September 6, 2026 and is valid until December 5, 2026, the full 90 days. Caddy renews once a third of the lifetime remains. When to pick Caddy and when nginx is covered, with our own benchmarks, in Caddy vs Nginx.

Two things still matter with Caddy:

  • The data directory is important. Caddy stores certificates and keys in ~/.local/share/caddy or /var/lib/caddy/.local/share/caddy. If Caddy runs in Docker without a volume for /data, every container restart fetches a new certificate, and eventually you hit the rate limit. That’s why the volume is always included in our Docker Compose examples.
  • Wildcards need a DNS module. For *.example.com you have to build Caddy with your DNS provider’s plugin (xcaddy build --with github.com/caddy-dns/cloudflare). The standard binary only does HTTP and TLS-ALPN validation.

Since August 2025, nginx also has an official route without Certbot: the ngx_http_acme_module from the nginx.org package repositories. It fetches and renews certificates inside nginx itself but, as far as we know, doesn’t do wildcards yet. If you use Ubuntu’s nginx package you don’t have the module and stay with Certbot.

Route 3: create a wildcard certificate with the DNS challenge

A wildcard certificate for *.example.com covers all first-level subdomains. That’s convenient when new subdomains keep appearing. Let’s Encrypt issues wildcards only via DNS validation. We tried with HTTP-01 and Certbot exited with code 1:

Client with the currently selected authenticator does not support
any combination of challenges that will satisfy the CA.

The message doesn’t tell you that the asterisk in the name is the problem. The first time you see it, it’s easy to spend an hour looking in the wrong place.

The right way is a DNS plugin for your provider. For Cloudflare:

sudo apt install python3-certbot-dns-cloudflare

# API token with "Zone:DNS:Edit" for this one zone only
sudo mkdir -p /root/.secrets
echo "dns_cloudflare_api_token = YOUR_TOKEN" | sudo tee /root/.secrets/cloudflare.ini
sudo chmod 600 /root/.secrets/cloudflare.ini

sudo certbot certonly --dns-cloudflare \
  --dns-cloudflare-credentials /root/.secrets/cloudflare.ini \
  -d example.com -d '*.example.com'

Important: *.example.com does not cover example.com itself, nor a.b.example.com. That’s why both names are in the command. The single quotes around the asterisk stop the shell from expanding it as a file pattern.

If there’s no plugin for your DNS provider, manual hooks help. That’s a small script that sets the TXT record via API (--manual-auth-hook) and removes it again (--manual-cleanup-hook). Certbot passes the domain in $CERTBOT_DOMAIN and the token in $CERTBOT_VALIDATION. In our test with Pebble’s DNS test server, a wildcard plus the apex domain went through in 2.4 seconds. With a real DNS provider it takes longer, because the record has to reach all nameservers first. The plugins wait 10 to 60 seconds by default for that.

A word from experience: a DNS API token that can edit the whole zone is a very powerful key. Whoever has it can point your domain to a server they control. Give the token permission for the single zone it needs and store it with 600 permissions. We’ve also had a token that simply wasn’t valid for a particular zone (Cloudflare error 9109). In that case HTTP-01 via webroot is the fallback, and that’s perfectly fine.

Route 4: create a self-signed certificate

For an internal test system, a development environment or a device on your home network that’s never reachable from the internet, you can issue a certificate yourself. One command does it:

openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
  -keyout internal.key -out internal.crt -days 365 \
  -subj "/CN=internal.local" \
  -addext "subjectAltName=DNS:internal.local"

That took us under a hundredth of a second. The certificate encrypts just as well as one from Let’s Encrypt, but nobody except you vouches for it. Browsers show a warning, and tools like curl refuse the connection (exit code 60 in our test) until you explicitly trust it, for example with curl --cacert internal.crt.

The most important line is -addext "subjectAltName=...". Since Chrome 58 (2017), modern browsers only look at the Subject Alternative Name (SAN) field, not the old Common Name. Our test shows why this trap is so persistent: curl 8.5 also accepted the certificate without a SAN, because it still falls back to the Common Name. If you only test with curl, you see green while Chrome fails with NET::ERR_CERT_COMMON_NAME_INVALID.

For local development, mkcert is more convenient. It creates a local CA, installs it in your system and browser trust stores, and issues certificates that work without warnings. Never share that local CA’s key, because with it someone could create valid certificates for any domain on your machine.

Self-signed certificates are not a solution for a public website. Visitors have to click past a security warning, and that trains them to ignore warnings.

cert.pem or fullchain.pem? The chain you can’t see

After issuance, /etc/letsencrypt/live/example.com/ contains four files:

FileContentsUse for
cert.pemonly your certificatealmost never directly
chain.pemonly the intermediateOCSP stapling, some apps
fullchain.pemyour certificate + intermediatethis one in nginx, Apache, HAProxy
privkey.pemthe private keysecret, permissions 600

A browser only trusts a short list of root certificates. Your certificate isn’t signed directly by a root, though, but by an intermediate. The server has to send that intermediate along, or the client can’t complete the chain.

Three linked glowing chain rings attached to a server, next to a second chain with a gap

We deliberately configured nginx with cert.pem instead of fullchain.pem. Result with curl: SSL certificate problem: unable to get local issuer certificate, exit code 60. With fullchain.pem: fine immediately. The nasty part: desktop browsers often don’t notice, because they fill in missing intermediates from a cache or download them. The site looks perfect in your own browser while apps, scripts, payment-provider webhooks and some Android devices reject the connection.

You can check the chain your server actually sends with:

openssl s_client -connect example.com:443 -servername example.com </dev/null \
  | grep -E "^ ?[0-9] s:|Verify return"

You should see at least two lines (0 s: for your domain, 1 s: for the intermediate) and Verify return code: 0 (ok) at the end. For getmind.io there are four entries, because Let’s Encrypt also sends a cross-signed certificate to the older ISRG Root X2.

Video: Cloud Codes compares a Caddy setup with automatic HTTPS to the classic nginx-plus-Certbot routine (July 2026).

Renewing the certificate: where things really go wrong

A Let’s Encrypt certificate is currently valid for 90 days. When installed from the Ubuntu package, Certbot sets up a systemd timer that runs certbot renew twice a day. It renews once fewer than 30 days remain. You can see the timer with:

systemctl list-timers | grep certbot

How such timers work and how to write your own is covered in Create a systemd service. Sounds like “set and forget”. In our tests, though, there were three places where the automation fails silently.

A conveyor loop circles a server and swaps certificate cards, with a clock nearby

Trap 1: the new certificate is on disk, nginx serves the old one

nginx only reads certificate files at startup or reload. When Certbot renews in certonly mode, the new certificate lands on disk but nginx keeps the old one in memory. We measured it: after certbot renew --force-renewal the file had the new serial 67E4…, while nginx kept serving the old 176E…. You only notice when the old one expires, up to 30 days later.

The --nginx plugin reloads nginx on its own. With certonly you need a deploy hook:

sudo certbot certonly --webroot -w /var/www/letsencrypt -d example.com \
  --deploy-hook "systemctl reload nginx"

With the hook, the new serial reached nginx immediately in our test. Certbot saves the hook in /etc/letsencrypt/renewal/example.com.conf (as renew_hook), so it runs on every future renewal. For existing certificates you can set it globally as an executable script in /etc/letsencrypt/renewal-hooks/deploy/. The same applies to every other service that uses the certificate: Postfix, Dovecot, HAProxy, a Node app. Each one needs its reload.

Trap 2: the dry run waits two and a half minutes

The most important command after any change is the dry run:

sudo certbot renew --dry-run

It runs the renewal against Let’s Encrypt’s staging server without replacing real certificates. In our test it first sat silent for 155 seconds. The log explained why: Non-interactive renewal: random delay of 152.6 seconds. When renewing without a terminal, Certbot deliberately waits a random time of up to eight minutes, so that not every server in the world hits Let’s Encrypt at the same second. In a script or CI pipeline that looks exactly like a hung process. For manual tests from scripts there’s --no-random-sleep-on-renew. Leave the delay in place for the timer.

The dry run also immediately uncovered the standalone trap from earlier. Of two certificates, one succeeded and the other failed on the occupied port. That’s exactly what it’s for.

Trap 3: server migration, and Certbot says “all good”

This is the most expensive trap, and we’ve walked into it ourselves. During a server move, a certificate was copied to the new server as files. The site worked, the padlock was green. What was missing was the renewal config. Three months later the certificate expired and the site was unreachable for several hours.

We recreated both typical migration mistakes:

  • Copied only the live/ directory (with cp -rL, so real files): nginx can read the files and everything works. certbot renew prints No renewals were attempted. and exits with code 0. certbot certificates says No certificates found. As far as Certbot is concerned, the certificate doesn’t exist, because the file under renewal/ is missing. Monitoring that only checks the timer’s exit code will never see an error here.
  • Copied live/ with cp -a: the files in live/ are only symlinks to ../../archive/. Without archive/ they point nowhere and nginx won’t even start. That’s the better failure, because you notice it right away.

Moving boxes with a padlock symbol are carried from an old server to a new one, with an hourglass nearby

Our rule since that outage:

  1. When moving servers, issue fresh certificates on the new server. That takes seconds once DNS points there.
  2. If that’s not possible, transfer the entire /etc/letsencrypt directory, with archive/, live/, renewal/, accounts/ and renewal-hooks/, for example with tar or rsync -a. Check webroot paths and hooks afterwards.
  3. Then always run certbot certificates (must list the certificate) and certbot renew --dry-run (must report success).
  4. Monitor the expiry date independently of Certbot (see the next section).

Monitoring expiry: a check that doesn’t depend on Certbot

Monitoring that asks Certbot whether Certbot works will miss exactly the failures Certbot doesn’t notice. So we check from the outside what the server actually sends:

#!/bin/bash
# cert-check.sh: warns when a served certificate is about to expire
set -euo pipefail
WARN_DAYS=14
for host in example.com www.example.com shop.example.com; do
  end=$(echo | timeout 10 openssl s_client -connect "$host:443" -servername "$host" 2>/dev/null \
        | openssl x509 -noout -enddate | cut -d= -f2) || { echo "ERROR $host: no connection"; continue; }
  days=$(( ( $(date -d "$end" +%s) - $(date +%s) ) / 86400 ))
  if [ "$days" -lt "$WARN_DAYS" ]; then
    echo "WARNING $host: only $days days left"
  fi
done

Run the script once a day and send the output somewhere you’ll actually read it: email, a messenger or your monitoring. How to set up a daily job is in Create a cron job. The 14-day threshold is deliberately below the 30 days at which Certbot renews: by the time the warning fires, automation has been failing for two weeks, and you still have two weeks left.

For a single file on disk there’s a shorter version: openssl x509 -in fullchain.pem -noout -checkend 1209600 returns exit code 1 if the certificate expires within 14 days. But remember trap 1: the file can be new while the server still serves the old certificate. That’s why we prefer checking over the network.

2027: Let’s Encrypt moves to 64 days

If you create an SSL certificate today, you should know about the upcoming changes. On October 7, 2026, Let’s Encrypt announced that starting February 10, 2027, all new certificates will be valid for only 64 days by default. By February 2028 the lifetime drops further to 45 days. The reason is an industry rule under which no public certificate may be valid for more than 47 days from March 2029. At the same time, Let’s Encrypt is cutting the period during which a completed domain validation can be reused from 30 to 10 days.

What that means for you:

  • With Caddy or default Certbot you don’t need to do anything. Both renew relative to the lifetime or well in advance. Certbot renews at 30 days remaining, so with 64-day certificates after about 34 days.
  • Hard-coded intervals need updating. Let’s Encrypt explicitly recommends grepping scripts, cron jobs and runbooks for numbers like 60, 80 or 83 days. A cron job that “renews every 60 days” will renew too late with 64-day certificates as soon as anything goes wrong once.
  • Semi-manual workflows become twice as frequent. If you still copy certificates by hand into a hosting panel, a firewall or a device, you’ll soon be doing it every few weeks. Now is the time to automate that.
  • Modern clients ask the CA when to renew. The mechanism is called ACME Renewal Information (ARI). It lets Let’s Encrypt tell a client “please renew now”, for example when certificates have to be revoked early. Whether your client supports ARI depends on its version. Ubuntu 24.04 ships Certbot 2.9.0; newer versions are available via Snap.

Your monitoring script should use a relative threshold too. With 45-day certificates, 14 days of warning is almost a third of the lifetime, which still works.

Rate limits: why you test against staging

Let’s Encrypt limits how many certificates you can get in a given time. For normal websites that’s irrelevant, but it matters when you’re experimenting. The most important limits according to the official documentation:

  • 50 certificates per registered domain (e.g. example.com including all subdomains) per 7 days,
  • 5 certificates for the exact same set of names per 7 days,
  • 300 new orders per account per 3 hours,
  • 5 failed validations per hostname per hour.

Let’s Encrypt can’t reset a limit you’ve hit; it only refills gradually. If you request a real certificate ten times in a row with a broken nginx config, you lock yourself out for a while. That’s why there’s --test-cert (staging server, certificates without browser trust, much higher limits) and --dry-run. Only get the real certificate once that works. It’s also why we did everything for this article against the Pebble test CA.

The second common cause of rate-limit trouble is Docker without a persistent volume. Every container restart fetches a new certificate for the same names, and after five restarts in a week you’re done.

Common errors when creating a certificate and what they mean

MessageCauseFix
Type: connection, connection refused / Timeout during connectport 80 closed or firewalledopen port 80 (ufw allow 80), check cloud firewall
Type: unauthorized, returned 404challenge file isn’t where the web server serves fromcompare -w and root, use a dedicated ACME location
Type: dns, NXDOMAINDNS record missing or not propagated yetcheck A/AAAA record, wait
does not exist or is not a directorywrong webroot pathfix the path
Could not bind TCP port 80standalone mode, but a web server is runningswitch to webroot or plugin
does not support any combination of challengeswildcard with HTTP validationuse a DNS plugin
too many certificates already issuedrate limitwait, use --test-cert next time
Browser: ERR_CERT_COMMON_NAME_INVALIDname missing from certificate (SAN)list every name with -d, set SAN when self-signing
curl: unable to get local issuer certificateserver doesn’t send the chainfullchain.pem instead of cert.pem

When something fails, always check /var/log/letsencrypt/letsencrypt.log. It contains the CA’s full response, including the URL it tried to fetch.

Security: protecting the private key

The certificate itself is public; every visitor receives it. Only privkey.pem is secret. Whoever has the key can impersonate your website. Certbot creates it with 600 permissions and the live/ and archive/ directories with 700. In our test, Certbot 2.9 generated an ECDSA key on the P-256 curve by default. It’s smaller and faster than RSA and supported by every current client.

A few rules that have served us well:

  • Never paste the key into chat, email or a Git repository. If a service running as another user needs to read it, use groups or have a deploy hook create a copy with the right permissions.
  • /etc/letsencrypt belongs in your backups, encrypted. How we structure backups is in the 3-2-1 backup strategy.
  • If you suspect the key was stolen: revoke the certificate (certbot revoke --cert-name example.com) and issue a new one.
  • HTTPS alone doesn’t protect against everything. What comes next, like HSTS and redirects, is in What is HTTPS?, and against automated attacks on logins, Fail2ban helps.

Frequently asked questions

How do I create an SSL certificate?

The easiest way is Let’s Encrypt. On a server with nginx, install certbot and python3-certbot-nginx and run sudo certbot --nginx -d your-domain.com. With Caddy as your web server, writing the domain name into the config is enough. Either way, the domain must point to your server via DNS and port 80 must be reachable.

Is a Let’s Encrypt SSL certificate really free?

Yes, with no hidden costs. Let’s Encrypt is a nonprofit certificate authority funded by donations and sponsors. The encryption is exactly as strong as with a paid certificate. Commercial providers mainly charge for organization vetting, support and warranties.

How long is a Let’s Encrypt certificate valid?

Currently 90 days. From February 10, 2027, Let’s Encrypt issues 64-day certificates by default, dropping to 45 days by February 2028. With automatic renewal via Certbot or Caddy, you don’t need to change anything.

How do I renew an SSL certificate?

With Certbot it happens automatically through a systemd timer that runs certbot renew twice a day. You can test it manually with sudo certbot renew --dry-run. What matters is that the web server is reloaded after renewal. With --nginx, Certbot does that itself; with certonly you need --deploy-hook "systemctl reload nginx".

Can I create an SSL certificate for an IP address?

For a long time only commercial providers offered that. Let’s Encrypt now offers short-lived certificates (6 days) for IP addresses through a separate profile. You need a current ACME client that supports profiles; the Certbot 2.9 in Ubuntu 24.04 doesn’t. We haven’t tested this ourselves. For a normal website, a domain name is almost always the better choice.

How do I create a wildcard certificate?

Only via DNS validation. With Certbot and your DNS provider’s plugin, for example certbot certonly --dns-cloudflare --dns-cloudflare-credentials ~/.secrets/cloudflare.ini -d example.com -d '*.example.com'. You have to list the apex domain separately because *.example.com doesn’t cover it.

What’s the difference between an SSL and a TLS certificate?

There is none. SSL is the protocol’s old name, TLS the current one. In both cases the certificate is the same X.509 certificate. “SSL certificate” just stuck as a term.

How do I create a self-signed certificate?

With openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout key.pem -out cert.pem -days 365 -subj "/CN=internal.local" -addext "subjectAltName=DNS:internal.local". Without the SAN, modern browsers reject the certificate. Self-signed certificates aren’t suitable for public websites because every visitor sees a warning.

Why does my browser say “not secure” even though I have a certificate?

The most common reasons: the certificate has expired, the name you visited (often www.) isn’t on the certificate, the page loads images or scripts over http:// (mixed content), or the certificate is self-signed. Clicking the icon to the left of the address shows the exact cause.

Can I create an SSL certificate with my web host?

With almost every host, yes, usually with one click in the control panel. Behind the scenes that’s Let’s Encrypt as well. This guide is mainly for your own servers and VPSs. If you’re still deciding whether your own server is worth it, see What is a VPS?.

Do I need a new certificate after moving servers?

It’s the safest option. Switch DNS over and let Certbot or Caddy fetch a fresh certificate on the new server. If you want to bring the old one along, copy the entire /etc/letsencrypt directory and then verify with certbot certificates and certbot renew --dry-run. Copying only the certificate files makes renewal fail silently.

Conclusion: creating an SSL certificate is easy, renewing is the real work

Creating an SSL certificate with Certbot or Caddy takes seconds and costs nothing. For new servers we recommend Caddy, because it doesn’t let the renewal and reload pitfalls arise in the first place. On existing nginx or Apache servers, Certbot with the right plugin is the way to go. Wildcard certificates come via the DNS challenge, and self-signed certificates are for testing and internal networks only.

The real work starts after the first green padlock. Our tests showed where automation fails quietly: a reload that never happens, a port that’s now taken, and a server move after which Certbot reports “all good” because it no longer knows the certificate exists. Three habits protect you: run certbot renew --dry-run after every change, always use a deploy hook with certonly, and run an expiry check from the outside that asks the server, not Certbot. With 64-day certificates from February 2027, these habits become more important, not less.