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.
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.
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.
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.
| 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 |
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.
# 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
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:
myapp.com and not an impersonatorA 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.
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.
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.
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.
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.
# 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
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:
# 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.
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
Cloudflare is a network of ~300 data centres globally. When you put your domain behind Cloudflare:
When you set a DNS A record to "Proxied" in Cloudflare:
Consequences:
$remote_addr is always a Cloudflare IP — you must read CF-Connecting-IP for the real client IPThis 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.
# 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
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.
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
Do these in order. Each step teaches one layer.
.xyz)1.2.3.4 (fake IP, doesn't matter yet)nslookup yourdomain.xyz 8.8.8.8 — watch it resolvewww pointing to @nslookup -type=TXT yourdomain.xyzGoal: understand that DNS is just a globally distributed key-value store. Records take time to propagate because of caching.
certbot certonly --standalone -d yourdomain.xyz/etc/letsencrypt/live/yourdomain.xyz/fullchain.pem in a text editor — it's a Base64-encoded blob. Run openssl x509 -in fullchain.pem -text -noout to decode itGoal: understand that a certificate is just a signed document. A private key is a number. TLS is math.
server {} block in nginx.conf for the subdomainlocation /api/ block that proxies to one backend, location / that proxies to anotherlimit_req_zone and limit_reqGoal: understand that nginx is a programmable router. The server_name and location directives are pattern matching rules.
nslookup yourdomain.xyz — notice the IP changed to a Cloudflare IPnginx access.log — all $remote_addr are now Cloudflare IPs$http_cf_connecting_ip instead for real client IPs/static/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.
| 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 |
# 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