--- # Putting Obsidian Publish on your own domain with Cloudflare ## ELI5 first Think of a small restaurant. **The kitchen** is Obsidian. It cooks your notes into a website. It has one shared service entrance for every customer's kitchen order: `publish-main.obsidian.md`. **The front counter** is Cloudflare. Guests never walk into the kitchen. They talk to the counter, the counter walks to the back, collects the order, and brings it out. **Your domain** is the sign above the door. `digital-armory.org` is just a name that tells people which counter to walk up to. Three things have to be true for a guest to get fed: 1. The counter has to know which kitchen to walk to. That's the DNS record. 2. The counter has to actually stand between the guest and the kitchen. That's the orange cloud, the proxy. 3. The counter and the kitchen have to agree on how they hand things over. That's the TLS mode. The kitchen only accepts a sealed hatch. If the counter tries to shout across the open room, the kitchen refuses and the guest waits forever. Miss any one of the three and the site is down, but each one fails with a different symptom. Most of the pain in this setup comes from not knowing which of the three broke. --- ## The working configuration Two DNS records at Cloudflare, and one setting. |Type|Name|Target|Proxy|TTL| |---|---|---|---|---| |CNAME|`@`|`publish-main.obsidian.md`|Proxied|Auto| |CNAME|`www`|`publish-main.obsidian.md`|Proxied|Auto| **SSL/TLS → Overview → encryption mode: Full.** **Obsidian → Publish changes → Change site options → Custom domain → Custom URL:** `digital-armory.org`, with no `https://` and no `www`. That's the entire thing. No A records. No IP addresses. Nothing else by hand. ```mermaid flowchart LR V["Visitor"] -->|"HTTPS to digital-armory.org"| E["Cloudflare edge<br>terminates TLS"] E -->|"resolves the CNAME target"| O["publish-main.obsidian.md"] O -->|"matches Custom URL"| S["Your published vault"] ``` --- ## Why each piece is there ### The CNAME target is a hostname, never a path A CNAME says "this name is another name for that name." DNS only knows names. It has no idea what a slash is, what a page is, or what a URL is. DNS is the address book that gets you to the building. It has no way to say which drawer in the kitchen. So `publish-main.obsidian.md` is valid. `publish.obsidian.md/digital-armory` is not a hostname at all, and Cloudflare will flag it with a warning triangle and then fail to resolve it. Your site's slug is not needed anywhere in DNS. Every Obsidian Publish user on a custom domain points at the exact same generic host. Obsidian works out which vault to serve from the **Custom URL** you set inside the app, by looking at the domain the request arrived for. The routing happens at the kitchen, not in the address book. > [!note] Two different hostnames `publish.obsidian.md` is the public site you browse. `publish-main.obsidian.md` is the origin the proxy is meant to fetch from. They are not interchangeable. ### A CNAME on the root of a domain should be illegal Strictly, DNS forbids a CNAME at the zone apex, because the apex has to carry other record types (SOA, NS, later MX) and a CNAME says "nothing else exists here." Cloudflare gets around this with **CNAME flattening**. You store a CNAME on `@`, and when someone asks, Cloudflare resolves it behind the scenes and answers with plain A records instead. This is why a lookup of the apex returns Cloudflare addresses like `104.21.x.x` and `172.67.x.x` rather than anything mentioning Obsidian. Useful side effect: because a proxied record answers with Cloudflare's own addresses, **you cannot see your origin target from outside**. If you need to verify what you actually saved, either read it in the dashboard, or flip the record to DNS-only for a minute and resolve it again. ### The proxy is not optional here Obsidian does not issue a TLS certificate for your domain. It has no way to prove to a browser that it is allowed to serve `digital-armory.org`. Cloudflare does that instead. With the orange cloud on, Cloudflare presents its own certificate for your domain to the visitor, then makes its own separate connection to the origin. Grey the cloud out and the visitor talks straight to Obsidian's origin, which has no certificate for your name, and HTTPS breaks. ### Full, and specifically not Flexible Cloudflare's encryption mode governs only the second hop, counter to kitchen. - **Off** or unset: no edge certificate, nothing to serve HTTPS with. - **Flexible:** Cloudflare fetches the origin over plain HTTP. Obsidian's origin answers with a redirect to HTTPS. Cloudflare passes that redirect to the browser, the browser comes back, and the same thing happens again. Infinite redirect loop. - **Full:** Cloudflare fetches over HTTPS but does not hard-validate the origin certificate. This is what Obsidian documents and expects. - **Full (strict):** validates the origin certificate too. Stricter than the documented setup, so treat it as unsupported here unless you have tested it. > [!warning] Unset is a failure mode, not a neutral state A fresh zone can sit with SSL/TLS never configured. Everything else can be perfect and the site still won't load. Opening the SSL/TLS panel and choosing Full is a step, not a formality. --- ## Reading the failures The error code tells you which of the three layers broke. This is the fastest debugging tool in the whole setup. |Symptom|Layer|What it means| |---|---|---| |**Error 1016**, Origin DNS error|DNS|The edge accepted the request, then failed to resolve the CNAME target. Your target string is wrong. Obsidian was never contacted.| |**Error 522**, connection timed out|Network|The origin is a real, resolvable address that isn't answering. Means you used an A record with a dead IP, not a bad hostname.| |**Error 526**, invalid certificate|TLS|Origin certificate failed validation. Usually Full (strict) where Full was needed.| |**ERR_TOO_MANY_REDIRECTS**|TLS|Flexible mode against an origin that insists on HTTPS.| |Cloudflare error page at all|Everything before it|Delegation works, the zone is live, the record exists and is proxied. The fault is downstream of DNS delegation.| The distinction between 1016 and 522 is worth internalising. **1016 means the name did not resolve. 522 means it resolved and didn't answer.** One is a typo, the other is a reachability problem, and they get confused constantly. ### Debugging hygiene Turn **off** Obsidian's "redirect to your custom domain" option while you are still fixing things. With it on, the original `publish.obsidian.md/your-slug` URL bounces you into the broken custom domain, so it looks like you broke two things instead of one. Turn it back on once the domain answers. After a fix, wait about five minutes. Cloudflare caches the failed origin lookup briefly. Test in a private window so your browser isn't replaying its own cached redirect. --- ## Choosing where to buy the domain Worth writing down, because the usual advice misses the point here. Since DNS has to live at Cloudflare for the certificate to work, the registrar's own DNS panel, their premium DNS upsell, their hosting and their mail are all irrelevant. That collapses the decision to four things: - **Renewal price, not the first-year price.** The cheap first year is marketing. Look up year two before paying. - **Free nameserver changes** and a **self-service auth code**. If getting your own transfer code needs a phone call or a fee, that tells you about the exit. - **2FA**, and a contact address you will still control in five years. Domains are usually lost to a dead mailbox and a missed renewal, not to an attacker. - **No bundling** of the price with a hosting package you don't want. For a gTLD, Cloudflare Registrar sells at cost with no upsells and puts DNS where it needs to be anyway. For `.pl` and various other ccTLDs it isn't an option, so use a local registrar and point the nameservers at Cloudflare. The registrar then becomes a billing relationship and nothing more. --- ## Email on the domain without running a mail server Separate problem, same zone. Splits cleanly into two halves that people wrongly treat as one. ### Inbound is free and takes five minutes Cloudflare **Email Routing**. Add your real mailbox as a destination, click the verification link it sends there, create a rule, done. Cloudflare writes the MX records and the SPF include itself, so don't hand-write those and don't add a null MX record. Leave catch-all off unless you enjoy spam. Forwarding normally breaks SPF at the receiving hop, because the forwarder isn't an authorised sender for the original domain. Cloudflare rewrites the envelope sender (SRS) so it survives. ### Outbound is the real problem Email Routing forwards. It does not send. Gmail's "send mail as" on a free account needs actual SMTP credentials, and without them your replies leave as your gmail.com address and the whole arrangement is decorative. Three routes: - **Cloudflare Email Service** now exposes SMTP submission at `smtp.mx.cloudflare.net` on port 465 with implicit TLS, username `api_token`, password an API token. Public beta, and sending to arbitrary recipients requires the Workers Paid plan. - **A real mailbox provider** (Migadu, Purelymail, mailbox.org, Fastmail). Roughly 10 to 25 USD a year, genuine IMAP and SMTP, no beta. Point MX at them and skip Email Routing entirely. - **A bulk relay's free tier** (Brevo, SMTP2GO, Mailjet). Works mechanically, but it's personal one-to-one mail down a marketing pipe, with deliverability that depends on whoever else is on those IPs. ### Authentication records One SPF record at the apex, never two. If you add a sender, merge its `include:` into the existing record rather than creating a second TXT. Add whatever DKIM selector your sender gives you. Start DMARC at `p=none` with a `rua=` address while you wire it up, confirm alignment in the reports, then move to `p=reject`. Going straight to reject before DKIM aligns means the first message you bounce is your own test. --- ## Lessons worth keeping - Three independent layers: name resolution, proxy, TLS. Always ask which one the symptom belongs to before touching anything. - DNS stores names. Never paths, never URLs, never protocols. - A warning triangle on a record is the system telling you the value is malformed. Read it before assuming propagation. - An unset setting is not a safe default. It is a broken state that hasn't announced itself yet. - When two changes go in together and something breaks, disable the cosmetic one (the redirect) so you're debugging one variable.