← Back to all docs

Learn the Entire Stack: DNS, TLS, nginx, Cloudflare

For someone technical but new to web infrastructure. This covers the actual mechanics — not just what each tool does, but why it exists and how the pieces fit together.


Mental Model: What Actually Happens When You Visit a Website

Before diving into each layer, trace what happens when a browser visits https://myapp.com:

1. Browser asks: "What IP address is myapp.com?"
        ↓ DNS resolution
2. DNS answers: "It's 104.21.80.1" (a Cloudflare IP)
        ↓ TCP connection
3. Browser opens a TCP connection to 104.21.80.1 on port 443
        ↓ TLS handshake
4. Browser and Cloudflare negotiate encryption, exchange certificates
        ↓ HTTPS request
5. Browser sends: GET / HTTP/1.1 Host: myapp.com (encrypted)
        ↓ Cloudflare proxies the request
6. Cloudflare forwards the request to your server (another TLS connection)
        ↓ nginx receives it
7. nginx on your server receives the request on port 443
        ↓ reverse proxy
8. nginx forwards it to your app: http://127.0.0.1:3000
        ↓ app responds
9. Node.js generates HTML, sends it back
        ↓ response travels back up the chain
10. Browser renders the page

Every section below is one of those numbered steps in detail.


1. DNS — The Internet's Phone Book

What problem does DNS solve?

Computers talk to each other using IP addresses (e.g. 104.21.80.1). Humans remember names (e.g. myapp.com). DNS is the system that translates one to the other.

How DNS resolution works (step by step)

When your browser needs the IP for myapp.com:

Browser → checks its own cache → found? done.
       → asks your OS resolver (127.0.0.53 on Linux, set by DHCP)
       → OS checks /etc/hosts → found? done.
       → OS asks your configured DNS server (e.g. 8.8.8.8, your router)
       → Recursive resolver asks the Root nameservers: "who handles .com?"
       → Root says: "Verisign handles .com — ask 192.5.6.30"
       → Recursive resolver asks Verisign: "who handles myapp.com?"
       → Verisign says: "Cloudflare's nameservers — ask aria.ns.cloudflare.com"
       → Recursive resolver asks Cloudflare: "what's the IP for myapp.com?"
       → Cloudflare answers: "104.21.80.1"
       → Recursive resolver caches the answer for TTL seconds
       → Returns IP to your browser

This whole chain completes in ~20–100ms the first time, then is cached.

DNS record types you need to know

Record Purpose Example
A Maps a name to an IPv4 address myapp.com → 1.2.3.4
AAAA Maps a name to an IPv6 address myapp.com → 2001:db8::1
CNAME Alias — points a name to another name www.myapp.com → myapp.com
MX Mail server for the domain myapp.com mail → mail.myapp.com
TXT Arbitrary text — used for domain ownership verification "v=spf1 include:..."
NS Nameservers — who answers DNS for this domain myapp.com NS → aria.ns.cloudflare.com

TTL (Time To Live)

Every DNS record has a TTL in seconds. This tells caches how long to hold the answer before asking again.

When you're setting up or migrating, lower your TTL to 60 before making changes. Raise it back to 3600+ after things are stable.

Practical exercise

# Look up an A record
nslookup myapp.com

# Ask a specific DNS server (bypass your cache)
nslookup myapp.com 8.8.8.8

# See all records for a domain
nslookup -type=ANY myapp.com 8.8.8.8

# Trace the full DNS resolution chain
nslookup -debug myapp.com

# On Linux/Mac: dig is more powerful
dig myapp.com
dig myapp.com +trace        # shows every step of the resolution chain
dig myapp.com @1.1.1.1      # ask Cloudflare's resolver specifically

2. TLS and Certificates — How HTTPS Works

What problem does TLS solve?

Without TLS, HTTP sends everything as plain text. Anyone on the network between you and the server (your ISP, a coffee shop router, a government) can read your requests and responses, and can modify them in transit.

TLS solves two things:

  1. Encryption — data is unreadable to anyone except the two endpoints
  2. Authentication — you can verify you're actually talking to myapp.com and not an impersonator

What is a certificate?

A TLS certificate is a document that says: "This public key belongs to the owner of myapp.com, and I (the Certificate Authority) vouch for it."

It contains:

The CA's signature is what makes it trustworthy. Your browser ships with a list of ~150 trusted root CAs. If a cert is signed by one of them (or a chain leading to one), the browser trusts it.

The chain of trust

Certificates are not signed directly by root CAs — they use intermediate CAs:

Root CA (e.g. "ISRG Root X1") — built into your OS/browser
    └── Intermediate CA (e.g. "Let's Encrypt R10") — signed by root
            └── Your certificate (myapp.com) — signed by intermediate

Why intermediates? Root CA private keys are kept offline in vaults. Intermediates are online and can be revoked if compromised, without touching the root.

When your browser visits your site, nginx sends the full chain (your cert + intermediate). The browser verifies each signature up to a trusted root.

The TLS handshake (simplified)

Browser → Server: "Hello. I support TLS 1.3. Here are cipher suites I know."
Server → Browser: "Hello. Use this cipher suite. Here's my certificate."
Browser:          Verifies certificate chain up to a trusted root CA.
Browser:          Checks the domain in the cert matches the URL.
Browser → Server: "Here's a session key, encrypted with your public key."
Server:           Decrypts the session key with its private key.
Both:             Now use the session key for symmetric encryption.
                  All further communication is encrypted.

Only the server's public key is exchanged openly. The private key never leaves the server. Even if someone captures the entire handshake, they can't decrypt the session.

What Let's Encrypt does

Let's Encrypt is a free, automated Certificate Authority. Before it existed (pre-2016), you had to pay $50–200/year for a certificate from a commercial CA.

To get a cert, you prove you control the domain via a "challenge":

HTTP-01 challenge:

Let's Encrypt: "Put this token at http://myapp.com/.well-known/acme-challenge/abc123"
Certbot:       Creates the file.
Let's Encrypt: Fetches it. Token matches → domain ownership proven.
               Issues certificate.

DNS-01 challenge (used when port 80 isn't open):

Let's Encrypt: "Add a TXT record: _acme-challenge.myapp.com = xyz789"
Certbot:       Adds the DNS record (via Cloudflare API).
Let's Encrypt: Looks up the DNS record. Matches → proven. Issues cert.

Certificate files

After Certbot runs, you get:

/etc/letsencrypt/live/myapp.com/
    fullchain.pem   ← your cert + intermediate cert (send this to clients)
    privkey.pem     ← your private key (KEEP SECRET, never share)
    cert.pem        ← just your cert (rarely used directly)
    chain.pem       ← just the intermediate (rarely used directly)

nginx uses fullchain.pem and privkey.pem. That's it.

Practical exercises

# Inspect a live certificate
openssl s_client -connect myapp.com:443 -servername myapp.com

# See the certificate details
openssl s_client -connect myapp.com:443 | openssl x509 -text -noout

# Check expiry date
openssl s_client -connect myapp.com:443 2>/dev/null | openssl x509 -noout -dates

# Verify a cert file locally
openssl x509 -in /etc/letsencrypt/live/myapp.com/cert.pem -text -noout

3. nginx — The Reverse Proxy

What problem does nginx solve?

Your Node.js app listens on port 3000 and does one thing: run your application code. nginx sits in front of it and handles everything else:

Forward proxy vs reverse proxy

nginx config structure

# Global settings
worker_processes auto;

events {
    worker_connections 1024;    # max simultaneous connections per worker
}

http {
    # Shared HTTP settings

    server {
        # One virtual host (one domain)
        listen 443 ssl;
        server_name myapp.com;

        # TLS
        ssl_certificate     /etc/letsencrypt/live/myapp.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.pem;

        location / {
            # What to do with requests matching this path
            proxy_pass http://127.0.0.1:3000;
        }

        location /static/ {
            # Serve files directly from disk
            root /var/www/myapp;
        }
    }

    server {
        # A second virtual host on the same machine
        listen 80;
        server_name myapp.com;
        return 301 https://$host$request_uri;  # redirect to HTTPS
    }
}

You can have multiple server {} blocks — nginx picks the right one based on the Host header in the request. This is how one server can host multiple domains.

Key nginx commands

nginx -t                    # test config syntax (do this before every reload)
nginx -s reload             # reload config without dropping connections
nginx -s stop               # stop immediately
systemctl status nginx      # check if running (Linux with systemd)
tail -f /var/log/nginx/access.log   # watch live traffic
tail -f /var/log/nginx/error.log    # watch errors

4. Cloudflare — CDN, DNS, and Protection

What Cloudflare actually is

Cloudflare is a network of ~300 data centres globally. When you put your domain behind Cloudflare:

  1. Your DNS is hosted on Cloudflare's nameservers (fast, globally distributed)
  2. All traffic to your domain hits Cloudflare's nearest data centre first
  3. Cloudflare decides what to do with it: cache it, block it, or forward it to your server

What the "orange cloud" proxy does

When you set a DNS A record to "Proxied" in Cloudflare:

Consequences:

Cloudflare SSL/TLS modes

This is the most confusing part for beginners. There are two TLS connections:

Browser ←──TLS──→ Cloudflare ←──TLS?──→ Your Server

Cloudflare gives you four modes for the Cloudflare → Server leg:

Mode Browser→CF CF→Server Notes
Off HTTP HTTP Never use this
Flexible HTTPS HTTP Your server has no cert; CF encrypts for the browser but not the backend. Bad.
Full HTTPS HTTPS Your server has a cert, but CF doesn't validate it (self-signed OK)
Full (strict) HTTPS HTTPS Your server must have a valid, trusted cert. Use this.

Always use Full (strict). Flexible is deceptive — it shows a padlock to the user but the Cloudflare→server leg is unencrypted.

Cloudflare's free plan includes

Practical exercise: see Cloudflare in action

# Check what IP a Cloudflare-proxied domain returns
nslookup myapp.com
# Returns a Cloudflare IP like 104.21.x.x, not your server IP

# Check the certificate issuer (should be Cloudflare's cert for the browser)
openssl s_client -connect myapp.com:443 | openssl x509 -noout -issuer

# Check response headers — Cloudflare adds its own
curl -I https://myapp.com
# Look for: CF-RAY, CF-Cache-Status, Server: cloudflare

5. Ports — Why 80 and 443?

HTTP is conventionally served on port 80; HTTPS on port 443. These are just agreed-upon defaults — nothing forces it technically. When you visit https://myapp.com, your browser automatically tries port 443 because that's the convention.

If your app runs on a non-standard port (like 3000), users have to type myapp.com:3000. nginx exists in part to eliminate this — nginx listens on 443, proxies to 3000, and users never know port 3000 exists.

On Linux, binding to ports below 1024 requires root (or the CAP_NET_BIND_SERVICE capability). This is another reason nginx runs as root but your app doesn't.


6. How It All Fits Together

Full production request lifecycle:

User types: https://myapp.com

① DNS: myapp.com → 104.21.80.1 (Cloudflare IP)
② TCP: browser connects to 104.21.80.1:443
③ TLS: Cloudflare presents its certificate for myapp.com
        Browser verifies: cert is valid, domain matches, not expired
        Session key exchanged → connection encrypted
④ HTTP: GET / HTTP/1.1  Host: myapp.com  CF-Connecting-IP: user's real IP
        (all encrypted inside TLS)
⑤ Cloudflare checks: is this cached? → No
        Is this blocked by WAF? → No
        Forward to origin server
⑥ TCP: Cloudflare connects to your server IP:443
⑦ TLS: your nginx presents its Let's Encrypt certificate
        Cloudflare verifies it's valid (Full Strict mode)
        Encrypted session established
⑧ HTTP: nginx receives the request
         Checks server_name: matches myapp.com block
         Checks location: matches /
         proxy_pass → http://127.0.0.1:3000
⑨ TCP: nginx connects to localhost:3000 (plain HTTP, internal only)
⑩ Node.js receives the request, generates HTML, returns it
⑪ Response travels back: Node → nginx → Cloudflare → Browser

7. Hands-On Learning Path

Do these in order. Each step teaches one layer.

Week 1: DNS

  1. Buy a cheap domain (Cloudflare Registrar, ~$1/year for .xyz)
  2. Add it to Cloudflare, point nameservers
  3. Create an A record pointing to 1.2.3.4 (fake IP, doesn't matter yet)
  4. Run nslookup yourdomain.xyz 8.8.8.8 — watch it resolve
  5. Change the A record, change the TTL, observe propagation time
  6. Add a CNAME record for www pointing to @
  7. Add a TXT record, verify it with nslookup -type=TXT yourdomain.xyz

Goal: understand that DNS is just a globally distributed key-value store. Records take time to propagate because of caching.

Week 2: TLS Certificates

  1. Spin up the cheapest VPS you can find (Hetzner CX11, €3.49/month or DigitalOcean $4/month)
  2. Point your domain's A record at the VPS IP
  3. Install nginx, install Certbot
  4. Run certbot certonly --standalone -d yourdomain.xyz
  5. Look at the files in /etc/letsencrypt/live/yourdomain.xyz/
  6. Open fullchain.pem in a text editor — it's a Base64-encoded blob. Run openssl x509 -in fullchain.pem -text -noout to decode it
  7. Find the expiry date, issuer, Subject Alternative Name
  8. Configure nginx to use the cert manually (don't use certbot --nginx)
  9. Visit your domain in a browser, click the padlock, inspect the cert

Goal: understand that a certificate is just a signed document. A private key is a number. TLS is math.

Week 3: nginx

  1. Add a second domain (or subdomain) to the same server
  2. Write a second server {} block in nginx.conf for the subdomain
  3. Point it to a different port (run a second Node app on 3001)
  4. Test that nginx routes each domain to the right app
  5. Add a location /api/ block that proxies to one backend, location / that proxies to another
  6. Add rate limiting: limit_req_zone and limit_req
  7. Serve a static file directly from nginx without hitting Node

Goal: understand that nginx is a programmable router. The server_name and location directives are pattern matching rules.

Week 4: Cloudflare

  1. Enable the orange cloud (proxy) on your A record
  2. Run nslookup yourdomain.xyz — notice the IP changed to a Cloudflare IP
  3. SSH into your VPS and check nginx access.log — all $remote_addr are now Cloudflare IPs
  4. Fix it: log $http_cf_connecting_ip instead for real client IPs
  5. Try setting SSL mode to Flexible — notice what breaks (or use Wireshark to see the unencrypted backend connection)
  6. Set back to Full (strict)
  7. Create a Page Rule to cache everything under /static/
  8. Check the CF-Cache-Status response header (HIT vs MISS vs BYPASS)

Goal: understand that Cloudflare is a man-in-the-middle you trust. Know what it does to your traffic.


8. Recommended Resources

Topic Resource
DNS fundamentals DNS and BIND by Cricket Liu (book) or Julia Evans' DNS zine at wizardzines.com
TLS/certificates "The Illustrated TLS 1.3 Connection" at tls13.xargs.org — animated, shows every byte
nginx Official docs at nginx.org/en/docs — dense but authoritative
HTTP fundamentals MDN Web Docs → HTTP section
Cloudflare Cloudflare's own Learning Centre at cloudflare.com/learning — genuinely good free content
General networking Computer Networking: A Top-Down Approach by Kurose & Ross
Hands-on practice sadservers.com — broken Linux server puzzles, many involve nginx and networking

Quick Reference Cheat Sheet

# DNS
nslookup domain.com                     # basic lookup
nslookup -type=A domain.com 8.8.8.8     # A record via Google DNS
dig domain.com +trace                   # full resolution chain
dig domain.com TXT                      # TXT records

# TLS / Certificates
openssl s_client -connect domain.com:443 -servername domain.com
openssl x509 -in cert.pem -text -noout  # inspect a cert file
openssl x509 -in cert.pem -noout -dates # check expiry
certbot renew --dry-run                 # test cert renewal

# nginx
nginx -t                                # test config
nginx -s reload                         # reload without downtime
tail -f /var/log/nginx/error.log        # live error log

# Cloudflare
curl -I https://domain.com              # check CF-RAY header (confirms CF is proxying)
curl https://cloudflare.com/cdn-cgi/trace  # your IP as Cloudflare sees it