Your app is running on the server. curl localhost:3000 works. Now you want https://app.example.com with a padlock, and that last mile (DNS, Nginx, certificates, renewals) is where a surprising number of deploys go wrong.
This guide covers custom domains and free HTTPS on a Linux VPS end to end, plus how ShipOwn automates it safely.
How the pieces fit
-
DNS maps
app.example.comto your server's IP. -
Nginx listens on ports 80/443 and decides which app gets each request, based on the
server_name. - Certbot proves to Let's Encrypt that you control the domain and installs a certificate into Nginx.
- Renewal replaces the certificate before it expires.
Step 1: DNS records
At your domain registrar or DNS provider:
| Type | Name | Value |
|---|---|---|
| A | app |
your server's IPv4 |
| AAAA | app |
your server's IPv6 (only if Nginx listens on IPv6) |
| A | @ |
server IP (for the root domain, if needed) |
| CNAME | www |
example.com |
Check propagation before going further:
dig +short app.example.com
If it doesn't return your IP yet, Certbot will fail. Wait.
If you use Cloudflare's proxy (orange cloud), set it to DNS-only while you issue the certificate, or use Cloudflare's own certificates with "Full (strict)".
Step 2: Open the firewall
sudo ufw allow 'Nginx Full'
Also check your cloud provider's firewall or security group: ports 80 and 443 must be open.
Step 3: An Nginx server block
# /etc/nginx/sites-available/app.example.com
server {
listen 80;
listen [::]:80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
For a static site, use root /var/www/site; and try_files instead of proxy_pass.
Step 4: Test before you reload. Always.
sudo ln -s /etc/nginx/sites-available/app.example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Nginx serves every site on the server from one process. If you reload a broken config, you can take down all of them. nginx -t is the cheapest insurance in DevOps.
Step 5: Issue the certificate
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d app.example.com
Certbot answers the HTTP challenge, edits your server block to add listen 443 ssl, certificate paths and (optionally) an HTTP→HTTPS redirect.
Multiple names on one certificate:
sudo certbot --nginx -d example.com -d www.example.com
Step 6: Renewals
Let's Encrypt certificates are short-lived, so renewal must be automatic. The Certbot package usually installs a systemd timer. Verify it:
systemctl list-timers | grep certbot
sudo certbot renew --dry-run
Bonus: www vs non-www and HTTP to HTTPS
Pick one canonical host and redirect the other. Search engines treat example.com and www.example.com as different URLs, so serving the same content on both splits your signals.
server {
listen 443 ssl;
server_name www.example.com;
# ssl_certificate lines added by Certbot
return 301 https://example.com$request_uri;
}
server {
listen 80;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}
Also check that your app generates absolute URLs (sitemaps, Open Graph tags, OAuth callbacks) with the final HTTPS host. Many "redirect loop" and "OAuth mismatch" bugs after a domain change come from an old BASE_URL or NEXTAUTH_URL environment variable, not from Nginx.
Quick verification checklist
-
curl -I http://app.example.comreturns a 301 to HTTPS. -
curl -I https://app.example.comreturns 200 from your app. -
sudo certbot certificateslists the domain with a future expiry date. -
sudo nginx -tis clean.
Common mistakes
-
DNS not propagated yet → challenge fails. Check with
dig. - Port 80 blocked → the HTTP challenge can't reach you.
-
Two server blocks with the same
server_name→ Nginx warns and picks one; requests go to the wrong app. -
Forgot
X-Forwarded-Proto→ the app thinks it's on HTTP and creates redirect loops. - Renewal silently failing → you find out when browsers show a warning. Check expiry regularly.
Domains and HTTPS with ShipOwn
ShipOwn is a desktop app (Windows, macOS, Linux) that deploys apps to your Linux server over SSH, with no deployment agent installed on the server. Domains and HTTPS are built into the deploy flow:
- When you point a domain at an app or static site, ShipOwn writes the Nginx virtual host for it.
- It tests the configuration before reloading Nginx.
- If Nginx rejects the config, it's put back the way it was, so one app's domain can't break the others.
- It asks Certbot for a certificate when you want HTTPS.
- Certificate expiry is shown in the app, so a failing renewal doesn't stay hidden.
The domain is part of the deploy plan you review before anything runs, next to the framework, commands, port and target. Every value is editable.
ShipOwn also helps when the server moves: if a server's IP changes, it traces the server through its own domains and confirms it by its SSH host key.
FAQ
Do I need a paid SSL certificate? No. Let's Encrypt certificates are free and trusted by all major browsers.
Can I host several domains on one VPS? Yes. Each gets its own server block; Nginx routes by server_name.
Does ShipOwn change my existing Nginx sites? It writes virtual hosts for the apps you deploy with it and tests every change before reloading.
Conclusion
DNS, a clean server block, nginx -t, Certbot and working renewals: that's all custom-domain HTTPS really is. Do it carefully once, and let ShipOwn repeat it for every app with the safety checks built in. Free 30-day trial, no card needed.
Written by Vishvajeet Shukla, full stack developer and creator of ShipOwn.
Top comments (0)