Buy a domain and point DNS at your VPS
Choose a registrar, buy a domain for ~€10-15/yr, and point the right DNS records — an apex A record and a wildcard — at your VPS, then verify resolution with dig. The home IP never enters DNS.
In this series
Expose your homelab to the internetlevel:Intermediate
In part 1 we settled the shape of the whole thing: a public VPS holds the edge, a private WireGuard tunnel reaches back into your homelab, and your home IP never appears anywhere public. This part is the first concrete step — getting a name and pointing it at that VPS. It is hands-on, but with one honest caveat: you do not create the VPS until part 3, so you do not yet have its real public IP. We will set up everything around a clearly-marked placeholder IP, and you will swap in the real one the moment part 3 hands it to you.
This is also a natural stopping point to do something slow: domain registration and DNS changes both involve waiting, so doing this early means the wait happens in the background while you build the rest.
This post is part 2 of the Expose your homelab to the internet series.
What a domain is, and where to get one
What. A domain is a human-readable name — example.dev — that the internet's naming system (DNS) can translate into an IP address. You do not buy a domain outright; you register it for a period (usually a year at a time) through a registrar, which is a company accredited to sell names under a given top-level domain (TLD).
Why. Your VPS will have a raw IP address like 203.0.113.10. That works, but nobody wants to type it, it changes if you rebuild the server, and you cannot get a trustworthy HTTPS certificate for a bare IP from a normal certificate authority. A domain fixes all three: it is memorable, it is stable even if the IP changes, and it is what Caddy uses to obtain a Let's Encrypt certificate later in the series.
How. Pick a registrar and a name. Any of these three are reasonable, vendor-neutral choices:
- Namecheap — straightforward DNS panel, good for a first domain. We use it as the worked example below.
- Cloudflare Registrar — sells domains at wholesale cost (no markup), but only for TLDs Cloudflare supports, and the name must use Cloudflare's DNS. Cheapest over time.
- Porkbun — low prices, wide TLD selection, clean interface.
The TLD is the bit after the last dot — .com, .dev, .net, .io, and so on. Choice is mostly aesthetic and budget-driven. A common, dependable TLD like .com or .dev costs roughly €10-15 per year; some novelty TLDs are cheaper to register but renew far more expensively, so check the renewal price, not just the first-year promo.
💡 Tip —.dev(and.app,.page) are on the HSTS preload list, which means browsers require HTTPS for them. That is a feature for this series — your sites will be HTTPS-only anyway — but it does mean a plainhttp://test will not work on those TLDs.
ℹ️ Note — Throughout the series the example domain isexample.dev, the public host isapp.example.dev, and the VPS public IP is the placeholder203.0.113.10(a documentation address from RFC 5737 — never a real server). Mentally replaceexample.devwith your name, and replace203.0.113.10with your VPS IP once part 3 gives it to you.
What DNS records are, and the ones that matter
What. Once you own a domain, the registrar (or another DNS provider) hosts a small table of records for it. Each record maps a name to a value. A DNS resolver reads this table to answer the question "what is the address for app.example.dev?"
Why. Pointing your domain at the VPS is editing this table. You will add two records and verify they resolve — that is the entire job of this post. Understanding the record types means you will know exactly which one to reach for, here and for every future service.
How. These are the record types that matter for this series:
- A record — maps a hostname to an IPv4 address.
app.example.dev → 203.0.113.10. This is the workhorse; it is what actually points a name at your VPS. - AAAA record — the same idea, but for an IPv6 address. Optional. If your VPS has an IPv6 address (Hetzner gives you one), you can add an AAAA record alongside the A record so IPv6-capable clients reach it directly.
- CNAME record — an alias that points one name at another name rather than an IP (e.g.
www.example.dev → example.dev). Useful for aliasing, but a CNAME cannot coexist with other records on the same name, and by spec the apex (root) cannot be a CNAME. We will not need one for the core setup.
A few concepts that come up constantly:
- Apex (root) vs subdomains. The apex is the bare domain itself,
example.dev, with nothing in front of it (your panel may show this as@). A subdomain is anything in front:app.example.dev,grafana.example.dev. You can point the apex and any subdomain at the same IP independently. - TTL (time to live). How long, in seconds, resolvers are allowed to cache a record before re-checking. A low TTL like
300(5 minutes) means changes take effect fast; a high TTL like3600(1 hour) means less DNS traffic but slower changes. Set a low TTL while you are setting things up. - Propagation. When you add or change a record, it is not instant everywhere — caches around the world expire at their own pace. New records usually appear within minutes; changes to an existing record can take up to its old TTL to clear. First-time delegation of a brand-new domain can occasionally take a few hours.
⚠️ Warning — Your home IP address must never go into any of these records. That is the whole point of the VPS pattern from part 1: only the VPS IP (203.0.113.10 here) ever appears in public DNS. The path from the VPS to your home runs through the private WireGuard tunnel, which has no DNS presence at all.The wildcard record
What. A wildcard record uses * as the name — *.example.dev — and matches any subdomain that does not have its own explicit record. Point *.example.dev at the VPS IP and app.example.dev, grafana.example.dev, whatever.example.dev all resolve to the VPS, with no per-name records.
Why. This is what makes the edge pattern pleasant to live with. The end goal of this series is that adding a new self-hosted service costs one line of Caddy config and zero DNS changes — because the wildcard already resolves every possible hostname to the VPS. The VPS's Caddy then looks at the hostname in each request and decides which internal service to forward it to. One wildcard record up front saves you from editing DNS for the rest of the homelab's life.
How. You add it exactly like an A record, but with * as the host. We will create it together in the next section, alongside the apex record.
ℹ️ Note — A wildcard only covers one label.*.example.devmatchesapp.example.devbut nota.b.example.dev. For this series, single-level subdomains are all you need.
Hands-on: create the records
What. In your registrar's DNS panel you will add two records: an A record for the apex (example.dev) and a wildcard A record (*.example.dev), both pointing at the VPS public IP. In Namecheap this lives under Domain List → Manage → Advanced DNS → Host Records; other registrars put it under a similarly-named "DNS" or "Records" tab.
Why. The apex record means example.dev itself reaches the VPS; the wildcard means every subdomain does too. Together they cover both https://example.dev and https://app.example.dev — and anything else you invent later — with no further DNS work.
How. Read the target table first, then enter the rows in your panel. In Namecheap's Host Records grid, the columns map like this: Type → the record type, Host → the name (@ for the apex, * for the wildcard), Value → the IP, TTL → set to a low value while building.
DNS records — example.dev (Namecheap Advanced DNS)
Type Host Value TTL
A Record @ 203.0.113.10 5 min # apex: example.dev
A Record * 203.0.113.10 5 min # wildcard: any subdomain
⚠️ Warning — 203.0.113.10 is the placeholder. You will not have your VPS's real IP until part 3. It is fine to enter the placeholder now to learn the panel, but the records only become useful once you edit both rows to your real Hetzner IP. Come back and do that as soon as part 3 hands you the address.If your VPS also has an IPv6 address (Hetzner provides one), you can optionally add matching AAAA records so IPv6 clients reach it directly:
Optional IPv6 records — example.dev
Type Host Value TTL
AAAA Record @ 2001:db8::10 5 min # apex over IPv6
AAAA Record * 2001:db8::10 5 min # wildcard over IPv6
Save, and give it a minute or two to propagate before verifying.
Verify with dig
What. dig (domain information groper) is the standard command-line tool for querying DNS directly. dig +short strips the output down to just the answer — exactly the IP a record resolves to.
Why. You want to confirm two things before moving on: that the apex/app record returns the VPS IP, and that the wildcard genuinely catches an arbitrary subdomain you never explicitly created. Verifying now means that when something later "can't connect," you can rule DNS out instantly.
How. Query the explicit hostname first. (On Debian/Ubuntu, dig comes from the dnsutils package; on macOS it is built in.)
Verify the apex / app hostname
dig +short app.example.dev
# expected: 203.0.113.10 (your real VPS IP once part 3 is done)
Now query a hostname you never created. If the wildcard is working, an arbitrary name still resolves to the VPS:
Verify the wildcard catches any subdomain
dig +short test.example.dev
# expected: 203.0.113.10 — proves *.example.dev is doing its job
If both return your VPS IP, DNS is done. If you get nothing back, it is almost always one of: the record has not propagated yet (wait, or query a public resolver explicitly with dig +short app.example.dev @1.1.1.1), a typo in the Host field, or you are still pointing at the placeholder and the placeholder IP simply has nothing running on it (that is expected until later parts).
Here is what that lookup actually does under the hood — the chain of questions a resolver asks to turn a hostname into your VPS IP:
flowchart TD
C["Your machine<br/><i>dig app.example.dev</i>"] --> R["Recursive resolver<br/><i>e.g. 1.1.1.1</i>"]
R --> Root["Root servers<br/><i>who handles .dev?</i>"]
Root --> TLD[".dev TLD servers<br/><i>who handles example.dev?</i>"]
TLD --> NS["Your registrar's<br/>authoritative nameservers"]
NS -->|"A example.dev → 203.0.113.10<br/>* matches app, grafana, …"| R
R -->|"203.0.113.10"| C
C -->|"https:// to the IP"| V["Hetzner VPS<br/><i>Caddy on :80 / :443</i>"]
The resolver walks down from the root to your authoritative nameservers, which return the VPS IP; the wildcard answers for any subdomain that has no explicit record. The home network appears nowhere in this chain.
Optional: using Cloudflare as your DNS provider
What. Many people move their domain's DNS to Cloudflare (free tier), even when the domain was registered elsewhere, for its fast panel and analytics. Cloudflare adds one wrinkle the other providers do not: each record has an orange-cloud (proxied) or grey-cloud (DNS-only) toggle.
Why it matters here. Grey-cloud / DNS-only means Cloudflare just answers the DNS query with your VPS IP — traffic goes straight to your VPS, exactly like Namecheap or Porkbun. Orange-cloud / proxied means Cloudflare publishes its own IPs and proxies traffic through its network, hiding your origin IP and terminating TLS at Cloudflare's edge.
How — what to choose. For this series' VPS + Caddy + WireGuard setup, use DNS-only (grey cloud) on the records that point at your VPS. Caddy obtains its own Let's Encrypt certificate via the HTTP-01 challenge, which needs ports 80 and 443 reachable directly on the VPS — and the design deliberately lets Caddy own TLS end to end. Grey-cloud preserves that direct, unmediated path. (Cloudflare's proxy does hide your origin IP, which sounds attractive — but it conflicts with how this stack issues certificates and handles traffic, as the warning below explains.)
⚠️ Warning — Orange-cloud (proxied) mode changes the picture in ways that fight this setup. It only proxies HTTP/HTTPS on a fixed set of ports, so anything non-HTTP or on an arbitrary port breaks. It can interfere with Caddy's HTTP-01 certificate issuance (Cloudflare answers:80/:443, not your VPS), so Caddy may fail to get or renew a Let's Encrypt cert unless you switch it to the DNS-01 challenge with Cloudflare API credentials. And it inserts Cloudflare between your visitors and your origin, which terminates and re-encrypts TLS at their edge. None of that is wrong in general — it is genuinely useful for hiding an origin from DDoS — but it is extra complexity this series does not assume. Keep records grey-cloud unless you have a specific reason and are ready to reconfigure Caddy's certificate challenge.
A short close
You now own a name and have two records — an apex A record and a wildcard — staged at your registrar, plus the dig checks to prove they resolve. The only thing missing is the real value to point them at: your VPS's public IP. That is exactly what comes next.
In part 3, Provision a Hetzner VPS, you stand up and harden the public host that holds the edge — and the first thing it hands you is the real IP. Swap it into both records, re-run dig, and your domain is genuinely pointing at your server. From there the tunnel and Caddy turn that pointer into a live, HTTPS-secured front door for your homelab.