acme.sh: A Lightweight Alternative to Certbot for Let’s Encrypt

I’ve been using Certbot for years and it works fine. But a while back I started running into situations where Certbot felt too heavy — too many dependencies, too much Python version drama, and the way it modifies your Nginx config automatically sometimes caused more problems than it solved. That’s when I started looking at acme.sh, and honestly, I haven’t looked back for most of my setups.

acme.sh is a pure shell script. No Python, no Node.js, no Ruby. Just bash. It runs on basically anything with a shell and curl. That alone makes it attractive when you’re working on minimal VPSes or containers where you don’t want to pull in a whole Python ecosystem just to renew certificates.

Installing acme.sh

Installation is dead simple:

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

Or if you prefer not piping curl to sh (which is a reasonable preference):

git clone https://github.com/acmesh-official/acme.sh.git
cd acme.sh
./acme.sh --install -m you@example.com

It installs to ~/.acme.sh/ and adds a daily cron job for renewals automatically. No root required, though you’ll probably need it for writing to /etc/ssl/ or reloading Nginx.

After installation, either reload your shell or source the profile:

source ~/.bashrc

Getting Your First Certificate

For a simple HTTP-01 challenge with a webroot:

acme.sh --issue -d example.com -d www.example.com --webroot /var/www/html

The webroot needs to be publicly accessible — acme.sh will place a challenge file under /.well-known/acme-challenge/. Make sure your Nginx or Apache serves that directory.

If you’re running Nginx and want acme.sh to handle the webroot automatically:

acme.sh --issue -d example.com --nginx

This is the mode I use least, because I’d rather know exactly where challenge files go. But it does work.

DNS Challenge — Where acme.sh Really Shines

This is where acme.sh beats Certbot for me. The DNS API integrations are excellent. If you’re using Cloudflare:

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

acme.sh --issue -d example.com -d '*.example.com' --dns dns_cf

That’s a wildcard certificate, issued via DNS challenge, fully automated. acme.sh stores the credentials and reuses them for renewals. It supports dozens of DNS providers: Route53, Namecheap, Gandi, Digital Ocean, Linode, and many more.

I’ve used this approach on servers that don’t have port 80 or 443 open — air-gapped services, internal APIs, staging servers behind a VPN. The DNS challenge doesn’t care where the server is, as long as you can update DNS records.

Installing the Certificate

After issuance, you need to install the cert to wherever your web server expects it:

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 is executed after each successful renewal. It can be any shell command. I typically use systemctl reload nginx or a custom script that copies files and reloads multiple services.

This is cleaner than Certbot’s approach in my opinion — you’re explicit about where files go and what happens after renewal.

Renewals

The cron job installed by acme.sh looks like this:

0 0 * * * /root/.acme.sh/acme.sh --cron --home /root/.acme.sh > /dev/null

It runs daily and only renews certificates that are within 30 days of expiry. If you want to force a renewal for testing:

acme.sh --renew -d example.com --force

Switching Between ACME Servers

By default, acme.sh uses ZeroSSL as its CA (they changed the default from Let’s Encrypt a few years back). If you want Let’s Encrypt specifically:

acme.sh --set-default-ca --server letsencrypt

Or specify it per-issue:

acme.sh --issue -d example.com --server letsencrypt --webroot /var/www/html

I noticed this change caught a few people off guard when ZeroSSL became the default. ZeroSSL works fine, but the certificates require an email registration and behave slightly differently in some monitoring tools. I usually stick with Let’s Encrypt explicitly.

Wildcard Certificates

Wildcard certs with acme.sh are genuinely easy compared to some alternatives. Once you have DNS API credentials set up:

acme.sh --issue -d example.com -d '*.example.com' --dns dns_cf

This gives you a certificate that covers example.com and everything under it: www.example.com, api.example.com, app.example.com. Note you need both the base domain and the wildcard explicitly — the wildcard alone doesn’t cover the apex.

If you’re managing many subdomains, wildcard certificates simplify your setup significantly. You issue once and install everywhere, rather than managing separate certs per subdomain.

Running as Root vs. Non-Root

acme.sh doesn’t require root to issue or renew certificates. But you’ll need root (or sudo) to write to /etc/nginx/ssl/ or reload system services. Two approaches:

Run the whole thing as root — simplest, most common. Just run as root or prefix with sudo.

Run as a non-root user, use sudoers for the reload command:

# In /etc/sudoers.d/acme
myuser ALL=(ALL) NOPASSWD: /bin/systemctl reload nginx

Then in acme.sh:

acme.sh --install-cert -d example.com \
  --fullchain-file /etc/ssl/certs/example.com.pem \
  --key-file /etc/ssl/private/example.com.key \
  --reloadcmd "sudo systemctl reload nginx"

I usually just run as root on single-purpose VPSes. But on shared systems or when following least-privilege principles, the sudoers approach works well.

Comparing acme.sh and Certbot

Both tools do the same job. The choice comes down to preferences and requirements.

acme.sh advantages: no dependencies, DNS API support is broader and easier to configure, more control over where files go, doesn’t touch your web server config.

Certbot advantages: more people use it (easier to find help), official ACME client from EFF, slightly better documentation for beginners, has plugins for Apache and Nginx that handle configuration automatically.

For servers where I want minimal dependencies and I’m comfortable with shell scripts, I use acme.sh. For quick setups where I want something battle-tested and widely documented, I still sometimes reach for Certbot.

Debugging Issues

If something goes wrong, run with --debug:

acme.sh --issue -d example.com --webroot /var/www/html --debug 2

This dumps verbose output including DNS lookups, HTTP requests, and API responses. Most issues I’ve run into are either wrong API credentials (DNS challenge) or incorrect webroot paths (HTTP challenge).

Check the log file at ~/.acme.sh/acme.sh.log for historical failures.

Also worth checking: make sure the domain actually resolves to the server and that your firewall allows port 80 for HTTP challenges. Obvious stuff, but I’ve wasted time on both.

A Few Things I’ve Learned the Hard Way

Rate limits are real. Let’s Encrypt has a limit of 5 failed validations per hour per domain, and 50 issued certs per domain per week. If you’re testing, use the staging server:

acme.sh --issue -d example.com --webroot /var/www/html --staging

Staging certs aren’t trusted by browsers, but they let you test the flow without burning production rate limits.

Also: if you’re moving from Certbot to acme.sh, check where your Nginx config points. Certbot stores certs in /etc/letsencrypt/live/. When you switch to acme.sh and install to a different path, you need to update your Nginx config or create symlinks. More than once I’ve switched tools and wondered why Nginx still loaded the old cert — it was still pointing to the Certbot path.

For more on certificate management workflows, see the SSL certificate monitoring guide — keeping track of expiry across multiple certificates is important whether you’re using Certbot, acme.sh, or anything else.

Scroll to Top