Skip to content

Certificates for devices behind no proxy

Some things on a network need a valid certificate and cannot sit behind a reverse proxy — a NAS, a firewall, a router, a mail server. This stack issues and renews certificates for them and writes the files somewhere you can upload from. One container.

Last run on a host 8 Nov 2026

Pinned at 0.2.1 (released 4 May 2026) · the newest upstream release

In the upstream images: 3 CRITICAL · 46 HIGH with a published fix — per image

Needs: a domain whose DNS is at Cloudflare, and a scoped API token for it.

From core/acme-certs/:

  1. Copy the environment file and set ACME_EMAIL and CERT_DOMAIN:

    Terminal window
    cp .env.example .env
  2. Create the Cloudflare API token as a secret. It needs Zone → DNS → Edit on the target zone — a scoped token, not the global API key:

    Terminal window
    mkdir -p .secrets
    printf '%s' 'your-cloudflare-api-token' > .secrets/cf_token.txt
  3. Start the container. It runs a scheduler in the background and issues nothing on its own:

    Terminal window
    docker compose up -d
  4. Issue the first certificate:

    Terminal window
    ./scripts/wizard.sh nas.example.com

    The wizard asks for the key type and the certificate authority, then writes the files. For a wildcard, add -w.

Terminal window
ls volumes/output/nas.example.com/

Four files: cert.pem, fullchain.pem, privkey.pem and ca.pem. Upload one of them to a device and load the page — a certificate that exists on disk and one a browser accepts are different claims.

Most devices want fullchain.pem and privkey.pem. These four ask for something else:

Device Upload
Synology DSM cert.pem, privkey.pem, ca.pem as the intermediate
OPNsense, pfSense cert.pem, privkey.pem, ca.pem
Nginx Proxy Manager fullchain.pem and privkey.pem, ca.pem optional
Windows, IIS, .NET a PFX bundle — see below

Windows and IIS do not read PEM. Convert with a password you choose, and enter the same one when importing:

Terminal window
docker compose exec acme-certs \
/scripts/convert-to-pfx.sh example.com your-chosen-password

The container renews on its own through a scheduler that runs inside it. What it does not do is upload anything — a renewed certificate lands in volumes/output/ and every device still points at the copy you gave it. Plan for that: either a script that pushes the new file, or a calendar entry.

To renew by hand:

Terminal window
docker compose exec -e CERT_DOMAIN=example.com acme-certs /scripts/renew.sh

For scripting:

Terminal window
docker compose exec \
-e CERT_DOMAIN=example.com \
-e CERT_SAN='*.example.com' \
acme-certs /scripts/issue.sh

ACME_SERVER in .env selects it: Let’s Encrypt by default, with ZeroSSL and Buypass as alternatives. ZeroSSL registers the address in ACME_EMAIL first.

State volumes/acme/ — the acme.sh account key and the issued certificate store
Also needed .secrets/cf_token.txt
Reproducible volumes/output/ — rendered from volumes/acme/ on the next issue or renewal
Database None.

The account key in volumes/acme/ is the part that matters. Certificates can always be reissued; a lost account key means registering again with the CA and losing the issuance history and rate-limit standing tied to it. It is small and almost never changes, which makes it easy to leave out of a backup.

The private keys in both folders are what a device is trusting: anyone who reads them can impersonate the device until the certificates expire.

Restore order: restore volumes/acme/ and the token, then let the tool regenerate volumes/output/ rather than restoring it.

The image is this project’s own build of acme.sh; there is no third-party release feed for it.

Terminal window
# Bump APP_TAG in .env, then:
docker compose pull
docker compose up -d
docker compose exec acme-certs acme.sh --version

Changes in the bundled client are listed in the acme.sh releases.

Issuing fails at the DNS challenge. The token is missing Zone → DNS → Edit on that zone, or it is a global key rather than a scoped token.

The device rejects the file. Check the table above — several want cert.pem with a separate CA rather than fullchain.pem.

A device shows an expired certificate although renewal ran. Renewal writes to volumes/output/ and does not upload. The device still holds the old copy.