Skip to content
prod e051e98
Browse

2 · Subdomains + SSL

Objective — give staging and production each a reachable hostname and a valid certificate before the first deploy (Deployer flips a symlink, it doesn’t issue certs), so the deploy lands on a working HTTPS origin.

Staging and production each need a reachable hostname and a valid certificate before the first deploy — Deployer flips a symlink, it doesn’t issue certs. This page gets DNS and TLS green so the deploy lands on a working HTTPS origin.

Decide the hostnames up front so DNS, certs, and your app’s APP_URL all agree.

EnvironmentExample hostPoints to
Stagingstaging.example.comStaging server IP (or Cloudflare proxy)
Productionapp.example.com / example.comProduction server IP
Apex redirectexample.com → www (or vice-versa)Pick one canonical, 301 the other

Keep apex vs www decisions consistent with APP_URL in 3 · Production .env. A mismatch breaks absolute URLs, signed routes, and cookie domains.

Create one record per host at your DNS provider (or Cloudflare — see 6 · Cloudflare CDN).

TypeNameValueNotes
Astaging<server IPv4>Apex/subdomain → IP
AAAAstaging<server IPv6>Only if the server has IPv6
CNAMEwwwexample.comAlias one name to the canonical

Use your host’s automated Let’s Encrypt integration (cPanel AutoSSL, Forge, Ploi, or certbot). One cert per hostname, or a wildcard if your panel supports DNS-01.

  • Issue for every hostname that serves traffic, including www.
  • Enable auto-renewal — Let’s Encrypt certs expire every 90 days.
  • Force HTTP → HTTPS at the web server (or via Cloudflare “Always Use HTTPS”).

Don’t trust the browser — run these and read the output.

  1. Run the five-check verification pass.

    Terminal window
    # 1) DNS resolves to the expected IP
    dig +short staging.example.com
    # 2) HTTPS responds (expect HTTP/2 200 or 301/302 to the canonical host)
    curl -sI https://staging.example.com | head -1
    # 3) HTTP redirects to HTTPS (expect 301/308 with a https:// Location)
    curl -sI http://staging.example.com | grep -iE 'HTTP/|location'
    # 4) Behind Cloudflare? Confirm the proxy is in front
    curl -sI https://staging.example.com | grep -i 'cf-ray' && echo "proxied" || echo "direct"
    # 5) Certificate is valid and covers the host
    echo | openssl s_client -connect staging.example.com:443 -servername staging.example.com 2>/dev/null \
    | openssl x509 -noout -subject -issuer -dates
    # Expected: dig returns your IP, HEAD is 200/301, HTTP redirects to HTTPS, and the cert notAfter is in the future with subject matching the host
    • dig returns your IP, the HEAD request is 200/301, HTTP redirects to HTTPS, and the cert’s notAfter date is in the future and subject matches the host.
SymptomCauseFix
dig returns nothing / wrong IPRecord missing or not propagatedRe-check the record; wait for TTL
curl cert errorCert not issued / wrong hostRe-run AutoSSL; ensure host is in the cert SAN
Redirect loopCloudflare SSL mode Flexible + origin forcing HTTPSSet Cloudflare SSL to Full (Strict) (see page 6)
NET::ERR_CERT_COMMON_NAME_INVALIDCert doesn’t list this hostReissue covering all hostnames

Do not mark this step done until every box below is checked.

  • 🤖 Hostnames resolve — every environment hostname resolves via dig to the correct IP.
  • 🤖 HTTPS + redirect — HTTPS returns 200/301 and HTTP force-redirects to HTTPS.
  • 👤 Cert valid + auto-renew — certificate is valid, covers the host, and auto-renewal is on.
  • 🔀 Canonical matches APP_URL — apex vs www decision matches the planned APP_URL.