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":

  1. Missing websocket / HTTP/1.1 headers — without proxy_http_version 1.1 and the Upgrade/Connection headers, live-reload and websocket apps break.
  2. Wrong bind — the app listens on a different port, or on 0.0.0.0 when you expected localhost. Check sudo ss -tlnp | grep :3000.
  3. 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.

Generate my config — free

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.