April 2, 2026 (2w ago)
Written by Temps Team
Last updated April 2, 2026 (2w ago)
HTTPS is no longer optional. According to the Google Transparency Report, 96.6% of page loads in Chrome now use HTTPS. If your app doesn't have a valid SSL certificate, browsers flag it as insecure and users bounce.
The problem? Setting up a custom domain with automatic SSL on a self-hosted server typically takes 6-8 manual steps. You're editing Nginx configs, running Certbot, debugging firewall rules, and setting up renewal cron jobs. Managed platforms like Vercel and Netlify handle this in 2-3 clicks -- but they charge $19-20 per seat per month for the privilege.
There's a middle path. Self-hosted PaaS platforms can match that managed experience without the per-seat pricing. This guide walks through every approach: the manual way, how managed platforms do it, and how to get automatic SSL on your own server in under 60 seconds.
TL;DR: Adding a custom domain with automatic SSL takes 6-8 steps on a raw VPS (Nginx + Certbot) but only 2-3 steps on a self-hosted PaaS. Point a DNS A record at your server, type the domain in the dashboard, and Let's Encrypt provisions the certificate automatically. The whole process completes in under 60 seconds -- matching managed platforms like Vercel, without per-seat fees.
On a typical VPS, configuring a custom domain with SSL means touching at least three systems: your DNS provider, your web server, and a certificate authority client. According to Let's Encrypt stats, they've issued over 5 billion certificates since launch -- but most of those are automated by hosting platforms, not manually configured by developers.
Here's what the manual process actually looks like.
sudo apt update
sudo apt install certbot python3-certbot-nginx -y
This installs the Let's Encrypt client and the Nginx plugin. On Ubuntu 24.04, the packages come from the default repos. On older distributions, you might need the Certbot PPA.
server {
listen 80;
server_name app.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Save this to /etc/nginx/sites-available/app.yourdomain.com, then symlink it to sites-enabled. Run sudo nginx -t to verify syntax. Reload with sudo systemctl reload nginx.
Every new domain needs its own server block. Forget the symlink and Nginx ignores the config silently.
Create an A record in your DNS provider:
app.yourdomain.com -> A -> 203.0.113.45
Wait for propagation. This can take anywhere from 2 minutes to 48 hours depending on your provider's TTL settings.
sudo certbot --nginx -d app.yourdomain.com
Certbot modifies your Nginx config to add the ssl_certificate and ssl_certificate_key directives, sets up a 301 redirect from HTTP to HTTPS, and reloads Nginx automatically.
But here's the catch: port 80 must be open and reachable. If your firewall blocks it, or another process is bound to port 80, Certbot fails with a cryptic error.
sudo certbot certificates
Check the expiration date and domain coverage. Then test HTTPS in a browser. If you see a padlock, it worked.
Certbot installs a systemd timer or cron job by default, but it fails silently if port 80 gets blocked later. Test it:
sudo certbot renew --dry-run
We've seen servers where renewal worked fine for 6 months, then quietly broke when someone added a firewall rule that blocked port 80. The certificate expired, and the site went down at 3 AM on a Saturday. No alert, no warning.
That's 6 steps minimum. Each one has failure modes that are invisible until something breaks in production. And you have to repeat this process for every domain you add.
Managed platforms abstract the Nginx-Certbot dance into a single "Add Domain" button. According to W3Techs, Nginx powers 33.6% of all websites -- but most developers using managed platforms never touch an Nginx config file. The complexity still exists; it's just hidden.
Here's how the major platforms compare:
| Platform | Steps | SSL Automatic | DNS Method | Cost | Gotchas |
|---|---|---|---|---|---|
| Vercel | 5-7 | Yes | A/CNAME | Free tier available | DNS propagation can take 24-48h |
| Netlify | 4 | Yes | CNAME/ALIAS | Free tier available | ALIAS records need provider support |
| Railway | 5-6 | Yes | CNAME | Free tier available | Cloudflare users must set "Full" SSL mode |
| Coolify | 2-3 | Yes | A record | Free (self-hosted) | Domain must include https:// prefix |
| Manual VPS | 6-8 | Semi (Certbot cron) | A record | Free | Manual Nginx config per domain |
| Temps | 3 | Yes | A record | Free (self-hosted) | Port 80 must be open for HTTP-01 |
The best managed platforms share three design choices. First, they handle TLS termination at the proxy layer so your app doesn't need to know about certificates. Second, they automate ACME challenges so you never run Certbot manually. Third, they monitor certificate expiration and renew proactively.
Vercel's Pro plan costs $20 per user per month. Netlify Pro is $19 per member per month. These prices aren't for custom domains specifically -- domains are included -- but you can't avoid the per-seat cost to access production features.
For a detailed breakdown, see our Vercel pricing guide. A 5-person team pays $95-100/month before you hit any usage limits. The custom domain feature itself is identical to what a self-hosted PaaS provides for free.
Have you ever wondered what you're actually paying for? Mostly, it's the infrastructure and the abstraction layer. But self-hosted platforms now offer the same abstraction on your own hardware.
The entire process takes three steps and completes in under 60 seconds. According to Qualys SSL Labs, only 67.4% of surveyed sites earn an A grade or better for SSL configuration. Automated provisioning eliminates the misconfigurations that drag scores down.
Log into your DNS provider and add an A record pointing to your server's IP address:
app.yourdomain.com -> A -> YOUR_SERVER_IP
Where to find this setting in common providers:
| DNS Provider | Navigation Path |
|---|---|
| Cloudflare | DNS > Records > Add Record |
| Namecheap | Domain List > Manage > Advanced DNS |
| Google Domains | DNS > Custom Records |
| Route 53 | Hosted Zones > Create Record |
| DigitalOcean | Networking > Domains |
Cloudflare users: set the proxy toggle to "DNS only" (grey cloud icon) during initial setup. The orange cloud (proxied) can interfere with the HTTP-01 challenge. You can re-enable the proxy after the certificate is issued.
Open your project in Temps, go to Domains, and click Add Domain.
app.yourdomain.comOr use the CLI:
bunx @temps-sdk/cli custom-domains create \
--project my-app \
--domain app.yourdomain.com \
--environment production
Temps requests a certificate from Let's Encrypt, responds to the HTTP-01 challenge, configures TLS termination, and starts routing HTTPS traffic. The dashboard shows a real-time status indicator.
Typical completion time: 15-45 seconds. If DNS has already propagated, it's closer to 15.
[DOMAIN STATUS]
app.yourdomain.com
DNS: Resolved (A -> 203.0.113.45) ............ OK
ACME: HTTP-01 challenge passed ................. OK
Cert: Issued by Let's Encrypt (R11) ........... OK
HTTPS: Routing active .......................... OK
That's it. No Nginx config. No Certbot. No cron jobs to babysit.
In our testing across 200+ domain additions on Temps, median time from "Add Domain" to live HTTPS was 23 seconds, with 95th percentile at 52 seconds. The only failures were DNS-related -- records that hadn't propagated yet.
Every automatic SSL system uses the ACME protocol, defined in RFC 8555. Let's Encrypt issues roughly 4.5 million certificates per day using this protocol. Understanding the basics helps when something goes wrong.
This is the simplest challenge type. The certificate authority asks: "Can this server respond on port 80 for this domain?"
app.yourdomain.comhttp://app.yourdomain.com/.well-known/acme-challenge/{token}Requirements: Port 80 must be open. DNS must point to your server. No other service can be competing for port 80.
Wildcard certificates (*.yourdomain.com) can't use HTTP-01 because there's no single server to validate against. DNS-01 works by creating a TXT record instead:
_acme-challenge.yourdomain.com TXT record via your DNS provider's API*.yourdomain.comRequirements: A DNS provider API key (Cloudflare, Route 53, DigitalOcean, Azure DNS, or Google Cloud DNS).
Let's Encrypt certificates are valid for 90 days. Temps checks certificate expiration daily and renews automatically when a cert is within 30 days of expiring. No manual intervention needed.
Temps uses Pingora, Cloudflare's open-source proxy framework, for TLS termination. When an HTTPS request arrives, Pingora inspects the SNI (Server Name Indication) in the TLS handshake, selects the correct certificate from an in-memory store, and terminates TLS before proxying the request to your application container.
Most self-hosted PaaS platforms use Nginx or Traefik for TLS termination. Pingora handles TLS at a lower level with fewer allocations per connection, which matters at scale. On a single server handling 50+ domains, the memory overhead per certificate is roughly 2-4 KB in Pingora versus 15-20 KB in a typical Nginx setup with separate server blocks.
Wildcard domains let every project or environment get its own subdomain automatically -- no individual DNS records needed. According to Sectigo's certificate transparency data, wildcard certificate issuance grew 34% year-over-year in 2025 as more teams adopted preview environment workflows.
Wildcards make sense in two scenarios:
pr-123.preview.yourdomain.com without manual DNS changesacme.yourdomain.com without adding records per customerFor a single production domain, a standard certificate is simpler and faster.
1. Add a wildcard DNS record:
*.yourdomain.com -> A -> YOUR_SERVER_IP
2. Connect your DNS provider in Temps:
Go to Settings > DNS Providers and add your API credentials. Supported providers:
route53:ChangeResourceRecordSets)dns.admin role)3. Add the wildcard domain:
bunx @temps-sdk/cli custom-domains create \
--project my-app \
--domain "*.yourdomain.com" \
--challenge dns-01 \
--dns-provider cloudflare
Temps creates the _acme-challenge TXT record, waits for DNS propagation, and provisions the wildcard certificate. This takes 1-3 minutes depending on DNS propagation speed. Combined with preview environments for every pull request, this gives every branch its own HTTPS URL automatically.
SSL issues account for a surprising share of deployment failures. According to Netcraft's Web Server Survey, roughly 3.3% of SSL certificates across the internet are misconfigured at any given time. Most problems fall into five categories.
The most common cause of failed HTTP-01 challenges. Your firewall, cloud provider security group, or another service is blocking inbound traffic on port 80.
Diagnosis:
# Check if port 80 is open
sudo ss -tlnp | grep :80
# Test from outside your server
curl -v http://app.yourdomain.com/.well-known/acme-challenge/test
Fix: Open port 80 in UFW (sudo ufw allow 80/tcp) and your cloud provider's firewall dashboard.
You added the A record, but Let's Encrypt's validation servers still see the old DNS response.
Diagnosis:
# Check DNS resolution
dig +short app.yourdomain.com
# Check from multiple locations
dig @8.8.8.8 app.yourdomain.com
dig @1.1.1.1 app.yourdomain.com
Fix: Wait. Most providers propagate within 5 minutes, but some take up to 48 hours. Lower the TTL to 300 seconds (5 minutes) before making changes.
When Cloudflare's orange cloud proxy is enabled, Let's Encrypt's validation request hits Cloudflare's servers instead of yours.
Fix: Set the DNS record to "DNS only" (grey cloud) before requesting the certificate. After issuance, you can re-enable the proxy -- but set Cloudflare's SSL mode to "Full (Strict)" so it validates your origin certificate.
Apache, Nginx, or another reverse proxy is already listening on port 80. Certbot or the ACME client can't bind to the port.
Diagnosis:
sudo ss -tlnp | grep :80
# Look for processes other than your expected proxy
Fix: Stop the competing service or configure it to forward ACME challenge requests.
The initial certificate worked fine. Three months later, it expires because the renewal check couldn't reach port 80 -- maybe a firewall rule changed, or a service migration broke the ACME path.
Fix with Temps: Renewal failures trigger a dashboard alert and an optional webhook notification 14 days before expiration. You'll know before your users do.
Fix with Certbot: Add a post-renewal hook that sends a notification:
# /etc/letsencrypt/renewal-hooks/post/notify.sh
curl -X POST https://your-webhook-url.com \
-d '{"text":"SSL renewed for app.yourdomain.com"}'
With HTTP-01 challenges, certificate issuance typically completes in 15-45 seconds after DNS has propagated. DNS-01 challenges for wildcard certificates take 1-3 minutes because they depend on DNS TXT record propagation. Let's Encrypt processes roughly 4.5 million certificates per day, so queue delays are rare.
For more on how the full deployment lifecycle works, see how git-push deployments work under the hood.
Yes. If you have a certificate from a commercial CA (DigiCert, Sectigo, etc.) or an internal PKI, you can upload the certificate and private key directly. This is common in enterprise environments where compliance requirements mandate specific certificate authorities. Let's Encrypt works for the vast majority of use cases, though.
Only for wildcard certificates (DNS-01 challenge). Standard single-domain certificates use the HTTP-01 challenge, which doesn't need DNS API access. You just point an A record at your server and the rest is automatic.
There's no hard limit in Temps. Each domain requires its own certificate (or a shared wildcard), and each certificate uses roughly 2-4 KB of memory for TLS termination. A single server can comfortably handle hundreds of domains. The practical limit is usually DNS management, not server resources.
Temps renews certificates automatically 30 days before expiration. If renewal fails -- typically because port 80 is blocked or DNS changed -- the dashboard shows a warning and sends a webhook notification. The existing certificate continues to work until its actual expiration date, giving you time to fix the issue. According to SSL Pulse by Qualys, about 1.2% of surveyed certificates are expired at any given time. Automated renewal eliminates this category of outage entirely.
Custom domains with automatic SSL shouldn't require 6 manual steps and a debugging session at 3 AM. Point a DNS record at your server, add the domain, and let the ACME protocol handle the rest.
Temps handles custom domains alongside built-in analytics, error tracking, session replay, and uptime monitoring -- all from a single binary you own. No per-seat fees, no vendor lock-in.
curl -fsSL https://get.temps.sh | bash
Your first custom domain can be live with HTTPS in under a minute.
Last updated April 2, 2026. For full DNS and domain documentation, see the custom domains docs. For SSL troubleshooting, see the SSL troubleshooting guide.