2026 Guide
How to self-host any app with a subdomain and SSL
The complete, copy-paste path from a blank server to a custom subdomain serving HTTPS. Works for any web app — Node, Python, Go, a static SPA, or a Docker container. Covers both nginx and Caddy.
The short version: point a subdomain at your server, run your app on a local port, put a reverse proxy in front of it, and issue a Let's Encrypt certificate. Five steps. Below is exactly what to run — or skip the typing and generate the configs for your domain.
Step 1 — Point a subdomain at your server
In your DNS provider, add an A record: host app (the subdomain prefix), type A, value your server's public IP, TTL 1 minute. Wait 1–5 minutes, then confirm it resolves:
getent hosts app.example.com # must print your server IP
If you use a wildcard record (*.example.com), any new subdomain already resolves — no edit needed.
Step 2 — Run your app on a local port
Start your app listening on 127.0.0.1 and a port like 3000. Binding to localhost (not 0.0.0.0) means only the reverse proxy — not the public internet — can reach it directly. Keep it alive with a process manager:
pm2 start your-app --name app -- --port 3000
pm2 save
curl -fsS http://127.0.0.1:3000/ # confirm it answers locally
Step 3 — Put a reverse proxy in front of it
The reverse proxy maps https://app.example.com to your local port. This is where most guides go wrong — they forget the websocket upgrade headers or the body-size limit.
nginx
Create /etc/nginx/sites-available/app:
server {
listen 80;
server_name app.example.com;
client_max_body_size 25M;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
}
}
sudo ln -sf /etc/nginx/sites-available/app /etc/nginx/sites-enabled/app
sudo nginx -t && sudo systemctl reload nginx
Caddy
If you'd rather not run certbot at all, Caddy handles HTTPS automatically. Add to /etc/caddy/Caddyfile:
app.example.com {
encode gzip
reverse_proxy 127.0.0.1:3000
}
sudo systemctl reload caddy
Step 4 — Issue an HTTPS certificate
With nginx, use certbot. It rewrites your vhost to add the 443 block and an HTTP→HTTPS redirect, and sets up auto-renewal:
sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d app.example.com --agree-tos --redirect --non-interactive
With Caddy, you already have HTTPS — it requested the certificate the moment you reloaded. Nothing to do.
Step 5 — Verify it's actually live
Don't trust "it should work." Prove it:
curl -I https://app.example.com # expect HTTP/2 200
curl -sI http://app.example.com | grep -i location # expect a 301 to https
echo | openssl s_client -servername app.example.com -connect app.example.com:443 2>/dev/null | openssl x509 -noout -dates
Why apps return 502 after deploy (and how to avoid it)
Three causes account for nearly every "it worked locally but 502s in production":
- Missing websocket / HTTP/1.1 headers — without
proxy_http_version 1.1and theUpgrade/Connectionheaders, live-reload and websocket apps break. - Wrong bind — the app listens on a different port, or on
0.0.0.0when you expected localhost. Checksudo ss -tlnp | grep :3000. - HTTPS before the cert exists — you hit
https://before certbot ran, so nginx fell back to its default server. Run certbot, then retry.
Skip the typing
SelfHost Buddy generates all of the above — the exact vhost or Caddyfile for your domain, the certbot command for your domains, a Docker snippet, an ordered runbook, and this verification checklist — in about 30 seconds. It runs entirely in your browser; your domains and ports are never uploaded.
FAQ
Do I need nginx or Caddy?
Either. Caddy = automatic HTTPS, zero commands. nginx = ubiquitous, more tutorials, certbot for certs. SelfHost Buddy generates both so you can compare.
Is Let's Encrypt really free?
Yes — free, automated, and trusted by all browsers. Certificates last 90 days and renew automatically via certbot's timer or Caddy.
Can I host multiple apps on one server?
Yes. Give each a subdomain, a local port, and its own vhost/Caddy block. The Fleet plan generates these in bulk and monitors every cert's expiry.