# LetsEncrypt DNS Validation Template Using Cloudflare

**URL:** <https://meta.discourse.org/t/letsencrypt-dns-validation-template-using-cloudflare/313501>\
**Category:** Self-hosting\
**Tags:** letsencrypt\
**Created:** [June 25, 2024, 7:17am UTC](https://meta.discourse.org/t/letsencrypt-dns-validation-template-using-cloudflare/313501 "2024-06-25T07:17:37Z")\
**Posts on this page:** 1\
**Showing post:** 4

<div class="post-metadata">

**Author:** ![Mark\_Wass](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/mark_wass/32/290526_2.png) [@Mark\_Wass](https://meta.discourse.org/u/Mark_Wass)\
**Post date:** [November 5, 2026, 1:09am UTC](https://meta.discourse.org/t/letsencrypt-dns-validation-template-using-cloudflare/313501/4 "2026-11-05T01:09:43Z")

</div>

Hi @LotusJeff,

Yes, DNS validation still works, but please don’t use the template in my first post any more. It is out of date with the current `discourse_docker`:

- Its `cert_exists` check fails with the current Let’s Encrypt chain, so it forces new certificates on every container start and can hit the duplicate certificate rate limit.
- Its nginx `replace` steps target lines that have moved, and the `add_header` one now overwrites an unrelated `ETag` header.

You don’t need a custom template now. Keep the stock Let’s Encrypt template and switch it to DNS validation in `app.yml`:

```yaml
templates:
  # ...
  - "templates/web.ssl.template.yml"
  - "templates/web.letsencrypt.ssl.template.yml"

env:
  # ...
  CF_Token: "YOUR_TOKEN" # Cloudflare API token with Zone > DNS > Edit on this zone
  CF_Zone_ID: "YOUR_ZONE_ID"

run:
  - exec:
      cmd:
        - mkdir -p /opt/dnsapi
        - curl -fsSL https://raw.githubusercontent.com/acmesh-official/acme.sh/3.0.6/dnsapi/dns_cf.sh -o /opt/dnsapi/dns_cf.sh
  - replace:
      filename: /usr/local/bin/letsencrypt
      from: "-w /var/www/discourse/public"
      to: "--dns dns_cf"

```

What this does:

- The stock template downloads only the main `acme.sh` script, without the DNS plugins. The `exec` step adds the Cloudflare plugin, and the stock start-up script copies it into `/shared/letsencrypt/dnsapi/`. Keep the version in the URL the same as the acme.sh version in `templates/web.letsencrypt.ssl.template.yml` (3.0.6 today).
- The `run:` section runs after the templates, so the `replace` switches the stock start-up script from HTTP to DNS validation.
- acme.sh reads `CF_Token` and `CF_Zone_ID` from the environment when it validates, and saves them with the certificate. The daily cron job does not see the container environment, so renewals use that saved copy.

If you used my old template, remove its line from `templates:` and rename `LETSENCRYPT_CF_TOKEN` and `LETSENCRYPT_CF_ZONE_ID` to `CF_Token` and `CF_Zone_ID`.

If you already have certificates (from HTTP validation or from my old template), move the old acme.sh folder aside before you rebuild:

```plaintext
cd /var/discourse
mv shared/standalone/letsencrypt shared/standalone/letsencrypt.old
./launcher rebuild app

```

Without this, acme.sh can skip the DNS step (it keeps the old method until renewal is due, and Let’s Encrypt can reuse a recent validation). Then the token is never saved and the first cron renewal fails. With a fresh folder, acme.sh registers a new account and does a real DNS validation, so the first start takes about a minute longer.

Keep the `.old` folder until the new certificates work. If the DNS step fails, the start-up script still overwrites the files in `shared/ssl`, and nginx will not start.

acme.sh stores the token in plain text under `shared/standalone/letsencrypt/`, so give the token only the permission it needs.

Tested with `discourse_docker` and Discourse `latest` as of November 2026.

---

_[View the full topic](https://meta.discourse.org/t/letsencrypt-dns-validation-template-using-cloudflare/313501)._
