A subdomain works when four things are true for that exact hostname: DNS returns the destination, the destination accepts the hostname, a trusted certificate names it, and the application answers the way you intended. The steps below follow that dependency order, and the verification section shows the checks we ran on our own hostnames, and.guide and www.and.guide.

Decide these before touching DNS

  • The exact hostname and its owner. A name that describes the service (docs, status, api) outlives one that describes a moment (new-prod, site2), and every name needs someone responsible for it.
  • What the destination documents. Either addresses you run or a hostname from a platform, plus any verification record the platform asks for.
  • Who issues the certificate. The platform, your own ACME client, or a CDN in front of the origin.
  • How to undo it. The record’s current value and TTL, or a note that the name didn’t exist before.

1. Register the hostname at the destination first

DNS only decides where a request goes. The destination still has to recognize the hostname, and it should know about it before traffic arrives.

On a managed platform this is the “custom domain” step, and some platforms depend on the order. Cloudflare Pages, for example, warns that adding the CNAME by hand without first adding the domain to the Pages project leaves the domain failing with a 522 error.

On your own server, bind the name explicitly. In NGINX:

server {
  listen 443 ssl;
  http2 on;  # NGINX 1.25.1 or later
  server_name docs.example.com;

  ssl_certificate     /etc/letsencrypt/live/docs.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/docs.example.com/privkey.pem;

  location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_set_header Host $host;
  }
}

If no server_name matches a request’s Host header, NGINX hands the request to the default server for that port, which is the first server block unless another one is marked default_server. A missing binding therefore tends to show up as the wrong site rather than as an error, so check the response body during verification, not only the status code.

2. Publish the record the destination documents

Use a CNAME for a platform hostname and A plus AAAA records for addresses you run. The A record versus CNAME guide explains the difference and the apex exception.

docs.example.com.   300  IN  CNAME  tenant.hosting.example.
api.example.com.    300  IN  A      203.0.113.42
api.example.com.    300  IN  AAAA   2001:db8::42

A short TTL such as 300 seconds keeps early mistakes cheap to correct. On Cloudflare the proxy setting matters as much as the record:

Cloudflare proxy status What resolvers see Who terminates TLS TTL
Proxied Cloudflare’s anycast addresses Cloudflare’s edge Fixed at 300 seconds
DNS only Your origin’s real address Your origin 60 seconds (30 on Enterprise) to 1 day; Auto is 300

Avoid looking the name up before the record exists. A “does not exist” answer is cached too, for as long as the zone’s SOA record allows (30 minutes on and.guide), so an early check can keep failing after the record is live. The propagation guide shows how to tell cached answers from live ones.

3. Let the certificate issue

How the certificate arrives depends on who terminates TLS:

Who terminates TLS How the certificate is issued What it needs from you
A hosting platform The platform requests it after the custom-domain step Correct DNS, sometimes a verification record
Cloudflare’s proxy Cloudflare’s edge certificate; Universal SSL covers the apex and first-level subdomains A proxied record; deeper names such as dev.www.example.com need another certificate
Your server, ACME HTTP-01 The CA fetches a token over port 80 at the hostname DNS already pointing at the server, and port 80 reachable
Your server, ACME DNS-01 The CA checks a TXT record at _acme-challenge.<hostname> DNS API access; the only way to get a wildcard certificate from Let’s Encrypt

Let’s Encrypt performs HTTP-01 only on port 80, so a firewall that closes port 80 breaks issuance and renewal even when the site itself serves only HTTPS.

4. Verify from outside, layer by layer

Here is the full sequence as we ran it against our own hostnames. and.guide is attached to a Cloudflare Pages project, and www.and.guide exists only to redirect to it.

DNS: where clients will connect

Live capture, 26 September 2026. Both hostnames through our ISP’s default resolver in South Korea (DiG 9.10.6, 23:55 KST):

$ dig +nocmd and.guide A +noall +answer
and.guide.		300	IN	A	172.66.44.244
and.guide.		300	IN	A	172.66.47.12
$ dig +nocmd and.guide AAAA +noall +answer
and.guide.		300	IN	AAAA	2606:4700:310c::ac42:2f0c
and.guide.		300	IN	AAAA	2606:4700:310c::ac42:2cf4
$ dig +nocmd www.and.guide A +noall +answer
www.and.guide.		300	IN	A	104.21.70.196
www.and.guide.		300	IN	A	172.67.138.236
$ dig +nocmd www.and.guide AAAA +noall +answer
www.and.guide.		300	IN	AAAA	2606:4700:3035::ac43:8aec
www.and.guide.		300	IN	AAAA	2606:4700:3031::6815:46c4

Two different sets of Cloudflare addresses, one for the Pages-hosted apex and one for the proxied www name. This check proves where clients will connect, and nothing more.

HTTP: status, Location, and the redirect chain

Live capture, 26 September 2026. Response headers filtered to the status line, Location, and Server, then the full chain for a deep link (23:56 KST):

$ curl -sI https://www.and.guide/ | grep -iE "^(HTTP|location|server)"
HTTP/2 301 
location: https://and.guide/
server: cloudflare
$ curl -sI http://www.and.guide/ | grep -iE "^(HTTP|location|server)"
HTTP/1.1 301 Moved Permanently
Location: https://and.guide/
Server: cloudflare
$ curl -sI http://and.guide/ | grep -iE "^(HTTP|location|server)"
HTTP/1.1 301 Moved Permanently
Location: https://and.guide/
Server: cloudflare
$ curl -sI https://and.guide/ | grep -iE "^(HTTP|location|server)"
HTTP/2 200 
server: cloudflare
$ curl -sS -o /dev/null -L -w "%{http_code} %{num_redirects} %{url_effective}\n" http://www.and.guide/guides/
200 1 https://and.guide/guides/

Three results to check:

  • www answers 301 with location: https://and.guide/ over both HTTPS and HTTP. That is one hop to the canonical URL, not an HTTP-to-HTTPS hop followed by a second hop to the apex.
  • Plain HTTP on the apex also answers 301 to HTTPS.
  • The deep link keeps its path: /guides/ on www lands on /guides/ at the apex after exactly one redirect, and the final status is 200.

TLS: which certificate each name gets

Live capture, 26 September 2026. The certificate each hostname presents, read with OpenSSL 3.6.3 at 23:41 KST (the LibreSSL build that macOS ships as /usr/bin/openssl rejects -ext as an unknown option):

$ openssl s_client -connect and.guide:443 -servername and.guide </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates -ext subjectAltName
subject=CN=and.guide
issuer=C=US, O=Google Trust Services, CN=WE1
notBefore=Aug  8 05:34:39 2026 GMT
notAfter=Nov  6 06:34:36 2026 GMT
X509v3 Subject Alternative Name: 
    DNS:and.guide
$ openssl s_client -connect www.and.guide:443 -servername www.and.guide </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates -ext subjectAltName
subject=CN=and.guide
issuer=C=US, O=Let's Encrypt, CN=YE2
notBefore=Sep  6 14:25:26 2026 GMT
notAfter=Dec  5 14:25:25 2026 GMT
X509v3 Subject Alternative Name: 
    DNS:*.and.guide, DNS:and.guide

Live capture, 26 September 2026. Chain validation against the local trust store (23:54 KST):

$ openssl s_client -connect and.guide:443 -servername and.guide -verify_return_error </dev/null 2>&1 | grep -E "Verify return code|Protocol|Cipher is"
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Protocol: TLSv1.3
Verify return code: 0 (ok)
$ openssl s_client -connect www.and.guide:443 -servername www.and.guide -verify_return_error </dev/null 2>&1 | grep -E "Verify return code|Protocol|Cipher is"
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Protocol: TLSv1.3
Verify return code: 0 (ok)

What the TLS captures show:

  • Two hostnames, two certificates. The apex certificate names only and.guide and comes from Google Trust Services. The www certificate names *.and.guide and and.guide and comes from Let’s Encrypt; that apex-plus-one-level shape matches Cloudflare’s Universal SSL coverage, served because www is proxied.
  • Both are 90-day certificates (8 August to 6 November, and 6 September to 5 December), and both validate (Verify return code: 0 (ok)) over TLS 1.3. Nobody renews these by hand. Renewal has to be automatic, and it is worth monitoring per hostname.
  • Always pass -servername. It sends the hostname in the TLS handshake (SNI), and a shared edge chooses the certificate from it.

For more ways to read certificates, see inspecting a TLS certificate from the command line.

What each check proves

Check Proves Does not prove
dig answer Where clients will connect That the destination knows the hostname
curl -sI status and Location The destination routes the hostname, and where the redirect points That other paths and methods work
curl -L -w Where the redirect chain ends, in how many hops, with the path intact That the final page has the right content
openssl s_client piped to openssl x509 Which certificate this name gets: names, issuer, validity dates That the next renewal will succeed
Verify return code: 0 (ok) The chain validates against this machine’s trust store That every older client trusts it

Reading failures by layer

What you see Layer Where to look
No record, or the wrong target, from dig Authoritative DNS The record itself; query your nameserver with +norec
Authoritative answer right, resolvers differ Resolver caches Remaining TTLs
Right DNS, but a platform’s default page, a 404, or a 522 Host binding The custom-domain step or server_name
curl: (60) SSL: no alternative certificate subject name matches target host name Certificate names The certificate doesn’t list this hostname, for example a wildcard one level too shallow
curl: (60) SSL certificate problem: unable to get local issuer certificate Certificate trust The server presented a certificate that doesn’t chain to a trusted CA, often a default certificate for a hostname it isn’t configured to serve
TLS works, but login or callbacks fail Application Base URL, redirect URIs, trusted-host settings

Both curl errors in that table are copied from our own tests on the same day: the first from a name two labels below a *.github.io certificate, the second from a server that has no publicly trusted certificate for the requested hostname. The wildcard subdomains guide shows both captures in full.

After launch: renewals and ownership

  • Monitor certificate expiry per hostname. Our two hostnames renew on different schedules with different CAs, and either renewal can fail on its own.
  • Record the owner, the record, the target, and the TLS mechanism for every public hostname, and review them in your subdomain inventory.
  • When a service is retired, remove its DNS record together with the destination binding, so the name can’t keep pointing at something you no longer control.