Why manage Let's Encrypt yourself on a VPS
Let's Encrypt is a free certificate authority that issues TLS certificates validated by an automated challenge (HTTP-01 or DNS-01). On shared hosting, you are stuck with the provider's TLS config: imposed protocol versions, no OCSP stapling, opaque renewal. On a VPS, you own the certbot (or acme.sh), the reverse proxy and the TLS termination. You decide on the protocols (TLS 1.2/1.3 only), the cipher suites, HSTS and OCSP stapling. You can also issue a wildcard certificate (*.example.com) via the DNS-01 challenge, cover several domains in a single certificate (SAN), and wire renewal to your own alerts. In short: total control over transport security, a prerequisite for an A+ score on SSL Labs and maximum browser-side trust.
What you gain by self-hosting the TLS chain
- 100% free certificates, valid for 90 days and renewed automatically without intervention.
- Wildcard
*.example.compossible via the DNS-01 challenge: a single certificate for all your subdomains. - Full control of the config: TLS 1.3, modern cipher suites, HSTS and OCSP stapling.
- Multi-domain SAN certificates (
example.com,www.example.com,api.example.com) on a single IP. - Scriptable and observable renewal: reload hooks, Slack/email alerts before expiry.
- No dependency on a third-party panel: the same recipe works across all your VPSes and environments.
Realistic prerequisites
TLS termination consumes very few resources: a VPS with 1 vCPU / 512 MB to 1 GB of RAM is more than enough to serve HTTPS for one or more sites. On the software side: Docker and docker compose (or a native Nginx/Caddy), a domain name pointing to the VPS's public IP (a propagated A/AAAA record), and ports 80 and 443 open in the firewall — port 80 is essential for the HTTP-01 challenge. For a wildcard certificate, plan for API access to your DNS provider (a token) to automate the DNS-01 challenge. Check propagation before starting: dig +short example.com must return the VPS's IP.
Deploy automated HTTPS in a few steps
Point the domain and open the ports
Create an A record example.com (and AAAA if IPv6) to the VPS's IP, then allow web traffic: ufw allow 80,443/tcp. Confirm resolution with dig +short example.com before going further — a challenge always fails if the DNS is not yet pointing.
Choose the reverse proxy that handles TLS
The simplest: Caddy, which obtains and renews Let's Encrypt automatically. A minimal Caddyfile — example.com { reverse_proxy app:3000 } — is enough to serve HTTPS as soon as docker compose up -d. For fine-grained control, go instead with Nginx + certbot, or Traefik with its built-in ACME resolver.
Issue the first certificate (HTTP-01)
With Nginx + Certbot in a container: docker run --rm -v ./certs:/etc/letsencrypt -v ./webroot:/var/www certbot/certbot certonly --webroot -w /var/www -d example.com -d www.example.com. Certbot places a challenge file in the webroot served by Nginx, Let's Encrypt validates it, and deposits fullchain.pem + privkey.pem.
Issue a wildcard via DNS-01 (optional)
For *.example.com, the HTTP-01 challenge is unsuitable: use DNS-01. With acme.sh and an API token: acme.sh --issue --dns dns_cf -d example.com -d '*.example.com'. The script creates a _acme-challenge TXT record, waits for propagation, then cleans up automatically.
Harden the TLS config
In Nginx, force ssl_protocols TLSv1.2 TLSv1.3;, enable ssl_stapling on;, and add add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;. Redirect all of port 80 to 443. Reload without downtime: nginx -s reload.
Automate renewal
Schedule renewal twice a day with a proxy reload: 0 0,12 * * * docker run --rm -v ./certs:/etc/letsencrypt certbot/certbot renew --quiet --deploy-hook "nginx -s reload". Caddy and Traefik do it natively; check expiry with echo | openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -noout -dates.
Before going to production, test against Let's Encrypt's staging environment (--staging with Certbot, --server letsencrypt_test with acme.sh): the production limit is 5 issuances per domain per week, and a poorly tuned script can quickly make you hit it. Also add expiry monitoring: a probe that runs openssl x509 -checkend 604800 (7 days) and triggers an alert if automatic renewal has silently failed — a broken cron is the number one cause of an HTTPS certificate expiring in the middle of the night.