Skip to content
Go back

Let's Encrypt Without Certbot

By SumGuy 6 min read
Let's Encrypt Without Certbot

You’ve Been Using Certbot Because Every Tutorial Says To

And honestly? Certbot is fine. It works. But it’s also the PHP 4 of ACME clients — everyone uses it because that’s what the StackOverflow answer from 2017 shows, not because it’s the best tool for every job.

Here’s the full menu. By the end of this, you’ll know what ACME actually is, why different clients exist, and how to get wildcard certs from the command line using nothing but a shell script and a Cloudflare API token.

What ACME Actually Does (One Paragraph)

ACME (Automatic Certificate Management Environment) is the protocol Let’s Encrypt uses to prove you own a domain before handing you a cert. Here’s the whole thing: your client contacts the CA (Let’s Encrypt), says “I want a cert for example.com”, and the CA responds with a challenge — either “put this file at example.com/.well-known/acme-challenge/xyz” (HTTP-01) or “add this TXT record to your DNS” (DNS-01). Your client completes the challenge. The CA verifies it. Cert issued. The whole exchange is signed with your account key so nobody can impersonate your client. That’s ACME. The client doing the work can be anything that speaks the protocol.

HTTP-01 vs DNS-01: Pick the Right Challenge

HTTP-01 is the default for most clients. Your server needs to be publicly reachable on port 80. Simple, works everywhere, no API credentials required. The downside: it can’t issue wildcard certs, and it won’t work for internal/private servers that aren’t on the public internet.

DNS-01 adds a TXT record to your domain’s DNS to prove ownership. It requires API access to your DNS provider, which is more setup — but it’s the only way to get wildcard certs (*.example.com), and it works for servers that have no inbound internet access at all. Your internal homelab box can get a valid public cert even though nothing can reach port 80 on it.

Rule of thumb: HTTP-01 for simple public sites. DNS-01 for wildcards, internal servers, or anything behind a firewall.

The Client Lineup

Caddy — Best Default Choice

If you’re not already using Caddy, this is your sign. ACME cert management is baked into the server itself. Zero configuration:

Caddyfile
example.com {
reverse_proxy localhost:8080
}

That’s it. Caddy handles cert issuance, renewal, and reloading automatically. No cron jobs. No separate tooling. No “did the renewal hook fire?” debugging at 2 AM. If you’re starting fresh and have no strong reason to use nginx, Caddy is the answer.

acme.sh — Most Flexible

A pure shell script. No Python. No Go. No system dependencies beyond curl and openssl, which you already have. Runs on anything with a POSIX shell. Supports 100+ DNS providers natively. This is the one to reach for when you need maximum flexibility or you’re on a weird system.

lego — Go Binary, Traefik’s Engine

lego is the ACME client that Traefik uses internally, also available as a standalone binary. Single static binary, no runtime dependencies, supports all the same DNS providers you’d expect. If you’re in a Go-heavy stack or already running Traefik, lego is a natural fit.

Step CA — Run Your Own ACME CA

Step CA from Smallstep lets you run your own internal certificate authority that speaks the ACME protocol. Your internal services get TLS from your own CA, your clients trust your CA’s root cert, and you’re not dependent on Let’s Encrypt at all. Overkill for a personal homelab, genuinely useful for an org with multiple internal services that need mutual TLS.

Certbot — Still Fine

Certbot works. The main complaints: it depends on Python (which means system Python version shenanigans on some distros), it installs itself with snap on Ubuntu now (which some people hate), and it’s a separate process from your web server that requires renewal hooks wired up manually. None of these are dealbreakers, they’re just friction that other clients eliminate.

acme.sh: Practical Wildcard Cert with Cloudflare DNS-01

Install is one line:

Terminal window
curl https://get.acme.sh | sh -s email=you@example.com

It installs to ~/.acme.sh/ and adds a daily cron entry automatically. Now set your Cloudflare API token as an environment variable:

Terminal window
export CF_Token="your-cloudflare-api-token"
export CF_Account_ID="your-cloudflare-account-id"

Issue a wildcard cert using DNS-01:

Terminal window
~/.acme.sh/acme.sh --issue \
--dns dns_cf \
-d example.com \
-d "*.example.com"

acme.sh adds the TXT record, waits for DNS propagation, Let’s Encrypt verifies, cert issued. The credentials get saved so renewals work automatically without you re-entering them.

Install the cert to a location your web server can read, with a reload hook:

Terminal window
~/.acme.sh/acme.sh --install-cert \
-d example.com \
--cert-file /etc/ssl/example.com/cert.pem \
--key-file /etc/ssl/example.com/key.pem \
--fullchain-file /etc/ssl/example.com/fullchain.pem \
--reloadcmd "systemctl reload nginx"

The --reloadcmd runs every time the cert is renewed. This is how you avoid the “cert renewed but nginx is still serving the old one” situation. You can put anything here — docker kill -s HUP your-container, systemctl reload haproxy, whatever applies.

Renewal Hooks: Don’t Skip This

Whatever client you use, make sure you have a reload hook wired up. A renewed cert sitting on disk while your server serves the old one is the most common “why is my cert expired?!” situation, and it’s entirely avoidable.

For acme.sh the --reloadcmd above handles it. For Certbot, it’s a script in /etc/letsencrypt/renewal-hooks/deploy/. For Caddy, it’s automatic — you don’t need to think about it.

Test your renewal process before the cert expires:

Terminal window
# acme.sh dry run
~/.acme.sh/acme.sh --renew -d example.com --force
# certbot dry run
certbot renew --dry-run

ZeroSSL: Same Protocol, Different CA

Let’s Encrypt isn’t the only free public CA. ZeroSSL speaks the same ACME protocol and issues 90-day certs for free. Most ACME clients support it:

Terminal window
# acme.sh with ZeroSSL as default CA
~/.acme.sh/acme.sh --set-default-ca --server zerossl
# or specify per-issue
~/.acme.sh/acme.sh --issue \
--server zerossl \
--dns dns_cf \
-d example.com

Why use ZeroSSL? Redundancy, mostly. If Let’s Encrypt has an outage during your renewal window, having a backup CA you already know how to use is genuinely useful. Also ZeroSSL has a web dashboard for managing certs if that matters to you.

Which Client Should You Use

The point isn’t that Certbot is bad. The point is that you now know it’s a choice, not a requirement. The ACME protocol is an open standard. Your cert issuer and your client are decoupled. Pick the one that fits your setup instead of the one that fits every tutorial from 2017.

Your 2 AM self will appreciate having the renewal hook wired up correctly regardless of which client you choose.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it may appear here.


Previous Post
Semantic Versioning: The Part Everyone Gets Wrong
Next Post
Grafana Dashboard Variables: One Dashboard for All

Related Posts