9 min read Grounded in README.md Quick Start with Docker

From zero to your first free SSL certificate in 15 minutes

A tutorial that gets you from an empty Docker host to a fully-issued Let's Encrypt wildcard certificate via CertMate + Cloudflare. Real commands, no hand-waving. Skip the intro if you already have CertMate running.

This is the "just show me the commands" walkthrough. No theory — we already have a separate page for that. The goal is a working *.example.com wildcard certificate signed by Let's Encrypt, with auto-renewal scheduled, in under fifteen minutes of wall-clock time.

What you need before starting:

  • A Docker host with docker compose (a $5/month VPS works).
  • A domain whose DNS is on Cloudflare (we use Cloudflare for the walkthrough because its free tier covers everyone; the same shape works for Route53, Azure, Google, Hetzner — see delegation if your DNS lives elsewhere).
  • Five minutes to create a Cloudflare API token.

1. Create a scoped Cloudflare API token

Don't use a Global API Key. Go to My Profile → API Tokens → Create Token → Edit zone DNS. Restrict it to the specific zone you'll issue against. The token needs:

  • Zone → DNS → Edit on example.com
  • Zone → Zone → Read on example.com

Copy the token. You'll see it once. Treat it like a password.

2. Boot CertMate

mkdir certmate && cd certmate

# Pull the docker-compose example
curl -fsSL https://raw.githubusercontent.com/fabriziosalmi/certmate/main/docker-compose.yml \
  -o docker-compose.yml

# Generate a long random bearer token CertMate will accept for its
# own REST API. Save this — you'll use it below.
openssl rand -hex 32 > api-bearer-token
echo "API_BEARER_TOKEN=$(cat api-bearer-token)" > .env

# Boot
docker compose up -d
docker compose logs -f certmate    # wait for "Listening on 0.0.0.0:8000"

CertMate is now serving on http://localhost:8000. In production you'd put a reverse proxy in front; for the tutorial we hit it directly.

3. Configure the Cloudflare account

Two ways to do this: the web UI at http://localhost:8000Settings → DNS Providers, or the REST API. We'll use the REST API because that's what you'll script against later.

export TOKEN=$(cat api-bearer-token)

curl -X POST http://localhost:8000/api/dns/cloudflare/accounts \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": "production",
    "name": "Production Cloudflare",
    "credentials": {
      "api_token": "PASTE_YOUR_CLOUDFLARE_TOKEN_HERE"
    }
  }'

The response is 201 with the account metadata. The Cloudflare token is now stored on disk under the CertMate data volume, mode 600, accessible only by the certmate process user.

4. Issue the certificate

curl -X POST http://localhost:8000/api/certificates/create \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "domain": "example.com",
    "san_domains": ["*.example.com"],
    "dns_provider": "cloudflare",
    "auto_renew": true
  }'

This kicks off the DNS-01 dance described in that other page. CertMate writes a TXT record at _acme-challenge.example.com, waits for propagation (Cloudflare is fast — usually 5-10 seconds), tells Let's Encrypt to validate, retrieves the certificate, and cleans up the TXT record. End-to-end, around 30-90 seconds for a first issuance.

The response when it finishes:

{
  "domain": "example.com",
  "status": "issued",
  "expiry_date": "2027-08-15T12:34:56Z",
  "days_until_expiry": 90,
  "cert_path": "/app/certificates/example.com/cert.pem",
  "san_domains": ["*.example.com"]
}

5. Verify the certificate

# Pull the cert bytes out of the container
docker compose exec certmate \
  cat /app/certificates/example.com/fullchain.pem > fullchain.pem

# Read the SAN list
openssl x509 -in fullchain.pem -noout -text \
  | grep -A 1 "Subject Alternative Name"
# Should print:
#   Subject Alternative Name:
#     DNS:example.com, DNS:*.example.com

# Read the expiry
openssl x509 -in fullchain.pem -noout -enddate

6. Wire auto-renewal (you already did)

You set auto_renew: true in step 4, so there's nothing else to do. CertMate's scheduler wakes every few hours, picks up any certificate within 30 days of expiry, runs the DNS-01 dance again (same SAN list, new serial), and atomically swaps the files on disk.

To confirm:

curl -s http://localhost:8000/api/certificates/example.com \
  -H "Authorization: Bearer $TOKEN" \
  | jq '{ auto_renew, renewal_threshold_days, days_until_expiry }'

# {
#   "auto_renew": true,
#   "renewal_threshold_days": 30,
#   "days_until_expiry": 90
# }

7. (Optional but recommended) Wire a deploy hook

A certificate that nobody reads doesn't help. The minimal global hook reloads nginx after every renewal:

curl -X POST http://localhost:8000/api/deploy/config \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "deploy_hooks": {
      "enabled": true,
      "global_hooks": [
        {
          "id": "nginx-reload",
          "name": "Reload nginx after cert change",
          "command": "/usr/sbin/nginx -s reload",
          "enabled": true,
          "timeout": 30,
          "on_events": ["created", "renewed"]
        }
      ],
      "domain_hooks": {}
    }
  }'

See the deploy hooks page for the recipe catalogue (scp+reload, S3 push, PagerDuty on revocation).

What you have now

  • A wildcard Let's Encrypt certificate covering example.com and every *.example.com subdomain.
  • Auto-renewal scheduled, fires within 30 days of expiry.
  • A deploy hook that reloads nginx whenever a new cert lands.
  • A REST API you can script against, and a web UI for the operators who'd rather click.
  • An audit log writing one row per issuance, renewal, and hook firing (see audit).

Total elapsed time, assuming you typed slowly and read the prompts: about twelve minutes.