
A padlock in the browser only means the first 10% went well. The rest keeps you out of 2 a.m. emergencies. You need renewals that run on schedule, a clean certificate chain, and the right certificate served for every hostname.
This TLS certificate deployment tutorial walks through a production-ready Let’s Encrypt setup on a hosting VPS. It also covers the fast checks to use when renewals fail or browsers start complaining.
The examples use Ubuntu 24.04 LTS, Nginx 1.24+ or Apache 2.4+, and Certbot (the most common ACME client in hosting).
If you run a control panel, the same rules apply. The panel may write parts of your web server config.
Before you touch TLS: quick preflight checks
Do these first. They prevent most “why is ACME failing?” tickets.
- DNS resolves correctly: the A/AAAA records for the hostname point to your VPS IP.
- Port 80 reachable: HTTP-01 validation needs inbound TCP/80 unless you’re using DNS-01.
- Hostnames decided: list every name you plan to secure (apex, www, app subdomains).
- Web server vhost exists: Nginx server block or Apache VirtualHost for the hostname.
- Clock is sane: NTP enabled; big drift breaks TLS and can confuse debugging.
On the VPS, confirm the basics:
sudo timedatectl status
# Replace with your hostname
HOST=example.com
getent hosts $HOST
# Ports reachability from outside matters, but on-server you can at least confirm listening:
sudo ss -lntp | egrep '(:80|:443)'
If DNS is still in flux, plan the cutover before you issue certificates.
HostMyCode’s domain tools make it easier to move cleanly. If you need to register or transfer a domain, start at HostMyCode Domains and map your records first.
Related internal guide if you’re mid-migration: DNS propagation tutorial for low-downtime cutovers.
TLS certificate deployment tutorial: choose your validation method
Let’s Encrypt issues certificates after it verifies you control the domain. On a typical hosting VPS, you’ll usually pick one of these:
- HTTP-01 (recommended for most websites): serve a challenge file from
http://yourdomain/.well-known/acme-challenge/. Requires inbound port 80. - DNS-01 (best for wildcard certs or when port 80 is blocked): publish a TXT record. It’s more work, but it supports
*.example.com.
For a single site (or a handful of subdomains), HTTP-01 is faster and easier to automate.
If you need wildcard coverage for lots of subdomains, DNS-01 is the right tool.
Step 1: Install Certbot on Ubuntu 24.04
On Ubuntu 24.04 LTS, the snap package is the supported upstream path for Certbot. It helps you avoid stale distro packages and keeps ACME behavior current.
sudo apt update
sudo apt install -y snapd
sudo snap install core
sudo snap refresh core
sudo snap install --classic certbot
sudo ln -sf /snap/bin/certbot /usr/bin/certbot
certbot --version
If you want a hosting-friendly baseline for SSH and firewall, do that first. It’s easier before you expose new services.
This pairs well with: SSH key setup guide.
Need a server to run this on? A HostMyCode VPS gives you full control for Certbot, Nginx/Apache tuning, and predictable renewal behavior.
If you don’t want to own patching and operational work, consider managed VPS hosting for ongoing maintenance and hands-on help.
Step 2A: Deploy Let’s Encrypt on Nginx (HTTP-01)
Confirm your Nginx site is enabled and answering on port 80 for the hostname. You only need a working HTTP server block to start.
Typical paths on Ubuntu:
- Configs:
/etc/nginx/sites-available/and/etc/nginx/sites-enabled/ - Web root:
/var/www/example.com/public(yours may differ)
Run Certbot with the Nginx plugin:
sudo certbot --nginx -d example.com -d www.example.com
Certbot will ask about redirecting HTTP to HTTPS. For most sites, accept the redirect.
If you’re migrating cautiously, it’s fine to wait. Verify every hostname and upstream dependency first.
Verify:
sudo nginx -t
sudo systemctl reload nginx
# Quick check from the server
curl -I https://example.com
Common pitfall: your config answers example.com but not www.example.com.
Your certificate only covers the names you include with -d, so list them explicitly.
If you want baseline hardening after HTTPS goes live, use a known-good header set.
Test admin paths before you ship it. See: Nginx security headers configuration tutorial.
Step 2B: Deploy Let’s Encrypt on Apache (HTTP-01)
Apache is a solid choice for hosting, especially for apps that depend on .htaccess.
Install Apache (if needed), then run Certbot against your vhost.
sudo apt update
sudo apt install -y apache2
sudo certbot --apache -d example.com -d www.example.com
Verify Apache is serving the certificate you expect:
sudo apachectl -t
sudo systemctl reload apache2
# Inspect the certificate subject/issuer and SANs
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -subject -issuer -dates -ext subjectAltName
If you run Nginx as a reverse proxy in front of Apache, terminate TLS at Nginx.
Keep Apache on localhost or a private interface to reduce exposure.
Step 3: Force HTTPS safely (and avoid redirect loops)
Most sites should redirect HTTP to HTTPS. Aim for a single, predictable redirect.
Redirect loops usually come from apps, proxies, or CDN settings fighting each other.
Nginx redirect (simple and reliable)
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
Apache redirect (VirtualHost :80)
<VirtualHost *:80>
ServerName example.com
ServerAlias www.example.com
Redirect permanent / https://example.com/
</VirtualHost>
If your application already forces HTTPS (WordPress, frameworks, CDNs), you can get double redirects or outright loops.
Check quickly with:
curl -I http://example.com
curl -I https://example.com
Watch the 301 chain. Two hops is usually fine.
Five means you’ve built a latency tax and a debugging problem.
Step 4: Automate renewals and prove they work
Let’s Encrypt certificates are short-lived by design. That’s fine when renewals are automatic and easy to verify.
With snap-based Certbot, a systemd timer usually runs renewals. Confirm it:
sudo systemctl list-timers | grep -i certbot || true
sudo systemctl status snap.certbot.renew.timer
Then run a dry run. This validates the full renewal path without spending a real issuance:
sudo certbot renew --dry-run
After renewal, your web server must reload to pick up the updated files.
Certbot often installs a hook, but don’t assume. Add your own deploy hook so behavior stays consistent across servers.
Create a deploy hook:
sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-webserver.sh >/dev/null <<'EOF'
#!/bin/sh
set -e
# Reload whichever is running
if systemctl is-active --quiet nginx; then
systemctl reload nginx
fi
if systemctl is-active --quiet apache2; then
systemctl reload apache2
fi
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-webserver.sh
Reload is usually better than restart. It picks up the new cert files without dropping active connections in most common setups.
Step 5: Fix the 6 most common ACME and browser errors
This is where most “TLS broke overnight” incidents come from.
Work through these in order. You’ll usually find the cause quickly.
1) HTTP-01 validation fails (404 or wrong content)
Symptoms: Certbot reports the challenge can’t be fetched, or Let’s Encrypt reaches a different server than you expect.
- DNS A/AAAA points to the wrong IP (old VPS still live).
- Port 80 blocked by firewall/security group.
- Another vhost is catching the request (default server block).
Quick checks:
# On the server, confirm your vhost answers the hostname
curl -I http://example.com
# If you use Nginx, list enabled server blocks
sudo nginx -T | grep -n "server_name" | head
If port 80 is blocked, fix the firewall first. If you’re using UFW:
sudo ufw status verbose
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
For cPanel servers, CSF is common. Make changes carefully so you don’t lock yourself out.
Internal guide: cPanel CSF firewall setup tutorial.
2) Browser shows “certificate not valid” after renewal
Usually the certificate renewed, but the web server never reloaded.
The other common cause is renewing one lineage while your vhost points at a different one.
Check the live expiry:
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates -subject -issuer
Then confirm your vhost points at the right files, typically:
/etc/letsencrypt/live/example.com/fullchain.pem/etc/letsencrypt/live/example.com/privkey.pem
3) Wrong certificate served for one hostname (SNI mismatch)
Symptoms: example.com looks fine, but www.example.com serves a different cert.
You’ll also see this when a panel hostname “bleeds” into another site.
Test each hostname explicitly:
for h in example.com www.example.com; do
echo "== $h ==";
echo | openssl s_client -connect $h:443 -servername $h 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName | sed 's/, /,\n /g'
done
The fix is usually one of these:
- Add missing names to the certificate (
certbot --nginx -d ...again). - Correct
server_name/ServerNameand ensure vhost ordering is sane. - Remove a catch-all vhost that steals traffic for 443.
4) Chain issues (missing intermediate, “NET::ERR_CERT_AUTHORITY_INVALID”)
Point your TLS config at fullchain.pem, not cert.pem.
Most clients expect the server to send intermediates.
Nginx example:
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
If a chain issue still won’t die, follow a structured handshake check so you don’t chase symptoms.
Internal guide: TLS handshake troubleshooting tutorial.
5) Renewal fails due to rate limits
You’ll hit rate limits when scripts keep requesting new certificates instead of renewing.
You can also hit them when validation fails and retries spiral.
- Prefer
certbot renewover re-issuing withcertbot certonly. - Use
--dry-runwhile testing automation. - Fix DNS and port 80 problems before retrying.
6) Mixed content after moving to HTTPS
Symptoms: the padlock warning returns, and the console shows HTTP assets getting blocked.
- Update app base URL (WordPress Address / Site Address).
- Fix hard-coded
http://assets in themes. - Ensure your CDN origin protocol matches.
Keep the changes reversible.
If you run a hosting business, test in staging first.
Step 6: Add HSTS carefully (don’t brick yourself)
HSTS tells browsers to always use HTTPS for your domain. It’s helpful, but it has teeth.
If you break TLS later, users may be stuck until the policy expires.
Start with a short max-age and skip preload:
# Nginx (inside the TLS server block)
add_header Strict-Transport-Security "max-age=86400" always;
After a week of clean operation, increase it (for example to 30 days).
Only add includeSubDomains if every subdomain is guaranteed to stay on HTTPS.
Step 7: Production checklist for hosting-grade TLS
Run this list before you call it “done.” It reduces customer tickets and makes audits faster.
- Certificate includes all required SANs (apex, www, app, mail if applicable).
- Server presents
fullchain.pemand correct key. - HTTP redirects to HTTPS with no loops.
- Renewal timer exists and
certbot renew --dry-runsucceeds. - Deploy hook reloads Nginx/Apache after renewal.
- 443 is open; 80 is open (or you’re using DNS-01).
- Monitoring alerts on cert expiry (14 days is a practical threshold).
If you want monitoring that catches expiry plus resource spikes and service failures, fold it into your normal ops checks.
Internal guide: Server monitoring tutorial.
Step 8: Optional—DNS-01 for wildcard certificates (quick walkthrough)
Wildcard certificates (*.example.com) require DNS-01.
The flow is simple: Certbot prints a TXT record, you publish it in DNS, then you continue validation.
Example (manual DNS challenge):
sudo certbot certonly --manual --preferred-challenges dns \
-d example.com -d '*.example.com'
Certbot will ask you to create a TXT record for _acme-challenge.example.com.
Add it in your DNS provider, wait for it to propagate, then continue.
Ops note: manual DNS-01 doesn’t scale. If you issue wildcard certs regularly, use a DNS provider API plugin.
Scope the API token to the minimum zone access you need.
Summary: keep renewals boring and errors explainable
A good TLS deployment is repeatable. You know exactly where the certificate files live.
You can verify what’s live in seconds with openssl s_client, and renewals trigger a reload without human intervention. That keeps customer sites online and maintenance predictable.
If you’re building on new infrastructure, start with a HostMyCode VPS for full control of Nginx/Apache and Certbot automation.
If you’d rather avoid patch cycles and incident response work, managed VPS hosting is the straightforward option.
If your certificate renewals feel fragile, the cause is usually boring: mismatched vhosts, unclear DNS ownership, or firewall rules that drift over time. HostMyCode can help you set this up cleanly on a VPS you control, or take on the operational work with managed plans. Start with a HostMyCode VPS, or choose managed VPS hosting for hands-on help with TLS, renewals, and troubleshooting.
FAQ: TLS certificate deployment on a hosting VPS
How do I check which certificate my server is actually presenting?
Use SNI with OpenSSL: echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -subject -issuer -dates. This shows what clients receive, not what you think is configured.
Can I close port 80 after issuing the certificate?
If you rely on HTTP-01 for renewals, keep port 80 reachable. If you must close it, switch to DNS-01 and automate TXT record updates via your DNS provider API.
Why did my cert renew but browsers still show the old expiry?
Nginx/Apache probably didn’t reload, or you renewed a different lineage than the vhost uses. Confirm the live expiry with openssl s_client, then reload the web server.
Should I enable HSTS on client sites?
Yes, but roll it out in stages. Start with max-age=86400, confirm every subdomain is HTTPS-ready, then increase max-age over time. Avoid preload unless you’re sure you want that commitment.
What’s the fastest way to debug ACME failures?
Confirm DNS points to the correct IP, verify port 80 works from the public internet, and make sure the right vhost answers the hostname. Most failures come back to one of those three.