Subdomains let one domain host several sites and apps: www.example.com for the main site, blog.example.com for a blog, app.example.com for a web app, status.example.com for monitoring. With Nginx, each subdomain is just another server block, on the same VPS and IP address.
1. Create DNS records
At your DNS provider, add an A record for each subdomain, pointing to your server’s IP:
| Type | Name | Value |
|---|---|---|
| A | blog |
203.0.113.10 |
| A | app |
203.0.113.10 |
| CNAME | status |
example.com (alternative to an A record) |
Check propagation with dig +short blog.example.com. See DNS records explained and DNS propagation.
2. A server block for a static subdomain
sudo mkdir -p /var/www/blog.example.com
# /etc/nginx/sites-available/blog.example.com
server {
listen 80;
server_name blog.example.com;
root /var/www/blog.example.com;
index index.html;
location / { try_files $uri $uri/ =404; }
}
sudo ln -s /etc/nginx/sites-available/blog.example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Nginx chooses the server block by matching the request’s Host header against server_name. See Nginx server blocks.
3. A subdomain for an app (reverse proxy)
# /etc/nginx/sites-available/app.example.com
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
That’s the pattern for Node.js, Python, Docker containers, or self-hosted apps (Grafana, Uptime Kuma, Gitea). See Nginx reverse proxy and reverse proxy for multiple Docker containers.
4. HTTPS for each subdomain
sudo certbot --nginx -d blog.example.com
sudo certbot --nginx -d app.example.com
Or one certificate for several names: -d example.com -d www.example.com -d blog.example.com. See Let’s Encrypt with Certbot and multiple domains with SSL.
5. Wildcard subdomains
For many dynamic subdomains (for example customer1.example.com, customer2.example.com in a SaaS):
- DNS:
*.example.comA record → your IP. - Nginx:
server_name *.example.com;, or a regex that captures the name:
server {
listen 443 ssl;
server_name ~^(?<tenant>[a-z0-9-]+)\.example\.com$;
# ssl_certificate for *.example.com
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Tenant $tenant;
}
}
- TLS: a wildcard certificate needs the DNS-01 challenge. See Certbot wildcard certificates.
Exact names (app.example.com) take priority over wildcards, so you can mix both.
6. A default catch-all server
Requests for unknown hostnames (or your raw IP) otherwise land on the first server block, which might be a site you didn’t intend to show. Add a default that rejects them:
server {
listen 80 default_server;
listen 443 ssl default_server;
server_name _;
ssl_reject_handshake on; # Nginx 1.19.4+: no certificate needed
return 444;
}
Troubleshooting
| Problem | Check |
|---|---|
| Subdomain shows another site | server_name typo, missing symlink in sites-enabled, or no default server |
| DNS doesn’t resolve | Record missing, or not yet propagated (dig) |
| Certificate warning | Certificate doesn’t include that name; run Certbot for it |
| 502 on app subdomain | App not running on the proxied port |
Quick reference
| Task | How |
|---|---|
| DNS | A record sub → server IP |
| Site | New server block with server_name sub.example.com |
| App | proxy_pass http://127.0.0.1:PORT |
| HTTPS | certbot --nginx -d sub.example.com |
| Wildcard | *.example.com DNS + DNS-01 cert |
| Default | default_server returning 444 |
Related: redirect www to non-www and Nginx test and reload.