Category: Self-Hosting Build Guide

  • No Open Ports — Reaching My Server from Anywhere with Tailscale

    No Open Ports — Reaching My Server from Anywhere with Tailscale

    No Open Ports — Reaching My Server from Anywhere with Tailscale

    Self-hosting build guide part 16. If part 9 was the road for visitors to enter the blog (Cloudflare Tunnel), this is the road for me alone to get inside my server.

    The companion part: Part 9 — Invisible Bridge (Cloudflare Tunnel)


    TL;DR

    • Problem: I want to reach my home server by remote desktop or SSH — from outside, or from a laptop in another room. But opening a port is dangerous.
    • Old method: Open a remote-desktop port on the router → bots brute-force the login 24/7.
    • New method: Tailscale — bundle my devices (server, laptop, phone) into one encrypted private network. Zero holes in the router.
    • Why it’s safe: Nothing is exposed to the internet + end-to-end encryption between devices + only my logged-in devices may enter.
    • Tool: Install the Tailscale app on each device and sign in with the same account. 5 minutes.
    • Cost: $0 (free for up to 100 devices on a personal plan).

    1. The Problem — How Do I Get Into My Server?

    Once you run a small server at home, this need shows up fast: you want to reach it by remote desktop (see the screen as-is) or SSH (a terminal) — from outside, or from another room in the same house.

    The classic old way is to open a port on the router (port forwarding). And that’s where the trouble starts.

    Trouble Why
    Brute-force attacks Open a remote-desktop port to the internet and bots start guessing passwords the moment it’s up
    One thin password If a bot breaks it, the whole server is gone. The login is the only line of defense
    Location exposure The public IP alone hints at your rough neighborhood
    Renewal hassle When the ISP rotates your public IP, the connection address changes too

    Exposing remote desktop directly to the internet is close to security suicide. (Part 9 handled visitor traffic; this is a channel only I, the admin, use, so it’s even more sensitive.)


    2. A Different Idea — Don’t Open Ports, Network the Devices

    Here’s the shift in thinking: Don’t open a door for the outside to come in. Instead, build a separate encrypted private network that only my own devices share.

    Tailscale does exactly this. Its engine is WireGuard (a modern, fast encrypted-tunnel technology), and Tailscale lays down those WireGuard tunnels automatically, with no configuration.

    Once installed, my devices join one private network (a “tailnet”), and each device gets a private address of the form 100.x.x.x. That address is invisible to the internet and reachable only by my own devices. Even far apart, they connect as if on the same LAN cable.


    3. Why It’s Safe — A Matchmaker and a Phone Call

    Tailscale’s cleverest design is separating coordination (control) from data. A matchmaker analogy makes it click.

    • Coordination server = the matchmaker: Tailscale’s servers authenticate devices and introduce them to each other. “These two belong to the same owner, so they may connect” — and they hand over public keys. But they never listen to the actual conversation.
    • Data = the actual call: Real traffic, like your remote-desktop screen, flows directly between devices (peer-to-peer), end-to-end encrypted by WireGuard. It does not pass through the company’s servers.

    That yields three layers of security:

    Safeguard What it means
    Zero exposed ports The server’s remote-desktop port doesn’t even appear to the internet. There’s no target for scanners
    End-to-end encryption WireGuard encrypts every link between devices. Intercept it and you still can’t read it
    Identity-based entry Not a shared password — only devices signed into my account join the network. A lost device can be cut off instantly from the console

    Each device’s private key never leaves that device. Only public keys go to the coordination server, so even if that server were breached, your traffic stays unreadable.


    4. Traffic Flow — Old vs New

    

    The old way punches a hole in the router that anyone can knock on. The new way wires an encrypted tunnel directly between my own devices and opens no door at all to outsiders.


    5. How Is This Different from Cloudflare Tunnel?

    It’s easy to confuse this with Cloudflare Tunnel from part 9. They aren’t competitors — they’re complementary tools with different jobs.

    Cloudflare Tunnel (part 9) Tailscale (this part)
    Goal Publish the blog to visitors Private access for me
    Audience Anyone, worldwide (https) Only my signed-in devices
    Used for Exposing a blog/website Remote desktop, SSH, admin
    Analogy Hotel front desk (guides guests) A staff-only back hallway

    Publish the blog to the world with Cloudflare Tunnel, and get into that same server to manage it with Tailscale. Running both on one server doesn’t conflict at all.


    6. How to Use It — 5 Minutes

    The idea is simple: install the app on every device you want to reach, sign in with the same account, done.

    1. Install Tailscale on the server (e.g., a Linux mini-PC) → sign in
    2. Install the Tailscale app on your laptop and phone → sign in with the same account
    3. Now every device joins one private network and gets a 100.x.x.x address
    4. For remote desktop or SSH, connect to the server’s Tailscale address

    Turn on MagicDNS and you can connect by device name instead of the address (e.g., myserver → that device). The private address, once assigned, doesn’t change, so you can keep it like a bookmark.


    7. Pitfalls — What We Actually Hit

    Pitfall 1. Connecting via the LAN IP gets blocked

    A device on the same home network has two addresses — the LAN IP the router gave it (192.168.x.x) and the private address Tailscale gave it (100.x.x.x). If you “safely” lock the server’s firewall to allow only the Tailscale path, then connecting over the LAN IP gets dropped by the firewall.

    The symptom is nasty: ping works and SSH (22) works, but remote desktop alone fails — because the firewall blocks only that port. Even inside the same house, you must connect via the Tailscale address. Not knowing this, we burned a while wondering “why doesn’t it work even at home?” (The saved connection address was set to the LAN IP — that was the culprit.)

    Pitfall 2. Both ends must be running

    Tailscale must be up on both devices to connect. If Tailscale is logged out on the laptop, it isn’t on the network, so of course it can’t connect. It usually auto-starts at boot, but when something’s off, suspect this first.

    Pitfall 3. Long-idle devices drop offline

    A device you haven’t powered on for a while (a phone, say) shows as “offline” in the console. Turn it on and sign in to bring it back. Tidying the device list now and then helps.


    8. Verification

    The fastest way to confirm the connection (with Tailscale on at both ends):

    ① Is the server’s private address alive?ping <server's Tailscale address> and watch for replies.

    ② Does the target port actually reach? — on Windows, in PowerShell:

    Test-NetConnection -ComputerName <server Tailscale address> -Port <port>
    

    If TcpTestSucceeded : True shows up, the private tunnel is healthy. Now point remote desktop or SSH at that address.


    FAQ

    Q. Can I use it together with Cloudflare Tunnel?
    Yes. Different jobs. Publish the blog via Cloudflare Tunnel, and manage the server (remote desktop, SSH) via Tailscale. No conflict.

    Q. What’s the free limit?
    The personal plan is free for up to 100 devices and 3 users. For bundling a few home servers, that’s effectively free forever.

    Q. Does Tailscale’s company see my traffic?
    No. The company’s servers only introduce; the actual data flows directly between devices, encrypted. (On tricky networks where a direct link fails, it goes through an encrypted relay — but even then it’s decrypted only at the two ends, so the relay can’t read it.)

    Q. What if power or internet drops?
    If the server goes down, it leaves the network. When it comes back, it rejoins automatically. The network itself is tied to your account and stays intact.

    Q. Is this used at companies too?
    Yes. It’s widely used in companies as a “zero trust” (trust nothing by default) way to reach internal networks. A home lab just applies the same principle for free.


    One-Line Summary

    To reach a home server from anywhere, instead of the old way of opening a router port and inviting bot attacks, build an encrypted private network of just your own devices with Tailscale. WireGuard encrypts end to end, and not a single port is opened to the internet. Setup is installing the app on each device and signing in with the same account — 5 minutes.


    References

  • The Link That Won’t Tap — Opening Obsidian Notes From Telegram

    The Link That Won’t Tap — Opening Obsidian Notes From Telegram

    Self-Hosting Build Guide, Part 15. Finishing the build guide isn’t the end — once you actually use it, follow-on problems surface. The note we synced in Part 13: its link wouldn’t tap on the phone.

    ← Previous: Part 13 — My Notes on My Server (Obsidian LiveSync)

    TL;DR

    • An Obsidian note carries its own link, shaped like obsidian://open?vault=...&file=...
    • Send that link over Telegram and you get gray dead text, not a blue link — it won’t tap
    • Why: chat apps auto-linkify only http/https. Custom schemes like obsidian:// are ignored
    • Fix: insert one https hop — a “shuttle” page that receives over https and bounces the browser to obsidian://
    • We put that shuttle on a Cloudflare Worker (serverless) instead of the home server — works even when the home machine is off, $0

    1. Symptom — A Link Gone Gray and Dead

    Obsidian gives every note its own link. Right-click a note and hit “Copy Obsidian URL” and you get something like obsidian://open?vault=MyVault&file=folder/note. I expected it to turn blue in Telegram, but it came up as gray plain text. Tapping does nothing. It was never a link — just letters.

    2. Cause — Chat Apps Only Linkify http/https

    A chat app scans the text of a message and, when it spots a URL, turns it into a tappable link on its own. But that auto-conversion applies only to http:///https:// (and Telegram’s own tg://). App-specific custom schemes like obsidian://, notion://, slack:// aren’t converted — they stay as plain text. Telegram’s iOS repo has the same issue on file: links with a custom URI scheme aren’t clickable.

    Can’t you route around it with a button or a markdown link? No. The inline buttons and links a bot attaches to a message also allow only http/https/tg. Custom schemes are rejected.

    3. The Standard Pattern — Insert One https “Shuttle”

    Since the custom scheme can’t go directly, route it through https once in the middle.

    1. Send Telegram an https link → it becomes a blue link
    2. Tap it and the browser opens that https page
    3. The page’s JavaScript bounces to obsidian:// → Obsidian opens

    This pattern isn’t new. A free public service called obsid.net does exactly this one thing — a static page that takes vault and file and hands the browser off to obsidian://. You could just use it.

    4. So Why Build My Own

    The reason I built my own anyway, despite obsid.net, is the same one that runs through this series.

    • No third party in the path — my vault name and note paths don’t pass through someone else’s server (even one that claims no logging)
    • My own domain — one line, o.mydomain, and the link is mine
    • I control the page — I can add the button to tap when the auto-redirect is blocked, and the filename that shows which note it is (this matters because of the traps in section 7)

    At first I dropped the page into the static folder of another app I already had running. It worked, but it meant wedging a notification redirect inside an unrelated app — the boundaries got dirty. A redirect belongs to no single app; it’s a generic utility. So I pulled it out and let it stand alone.

    5. Where to Put It — Home Server vs the Edge

    If it’s going to stand alone, it’s one of two places.

    A small web server on the home box Cloudflare Worker (edge)
    Runs when only if the home machine is on even when the home machine is off
    Certificate issue/renew yourself automatic
    Cost electricity free (100k requests/day)
    Dependency none Cloudflare

    A redirect is a one-second static response, so there’s no reason to keep the home server awake for it. I picked the Cloudflare Worker — put a snippet of code on the cloud edge and it answers without you running a server (this is called serverless). The domain I bought in Part 6 is already on Cloudflare, so attaching one subdomain is all it takes.

    6. How It Works — https Receives, Bounces to obsidian://

    What the Worker does is short. It takes ?f=note-path and returns a single HTML page that turns it into an obsidian:// link.

    export default {
      async fetch(request) {
        const url = new URL(request.url);
        const file = url.searchParams.get('f') || '';
        const vault = url.searchParams.get('v') || 'MyVault';
        const uri = 'obsidian://open?vault=' +
          encodeURIComponent(vault) + '&file=' + encodeURIComponent(file);
        const html = '<!doctype html><meta charset=utf-8>'
          + '<p>' + file + '</p>'
          + '<a href="' + uri + '">Open note</a>'
          + '<script>setTimeout(function(){location.href=' + JSON.stringify(uri) + '},350)</' + 'script>';
        return new Response(html, {
          headers: { 'content-type': 'text/html; charset=utf-8' }
        });
      }
    }
    

    To Telegram you send only https://o.mydomain/?f=note-path. That’s https, so it becomes a blue link; tap it and the page above comes up and hands off to Obsidian.

    7. Three Traps

    ① The auto-redirect gets blocked. Even if you set location.href = obsidian://... for an automatic jump, mobile browsers (especially Android Chrome and the in-app browsers inside chat apps) sometimes block, for security, a custom-scheme navigation the user didn’t tap themselves (“Navigation is blocked”). So you can’t lean on the auto-redirect alone — always include an “Open note” button for the user to tap. That’s why an auto-only page fails now and then.

    ② The vault name differs per device. The X in obsidian://open?vault=X must exactly match the vault’s display name registered on that device. If it opens on your PC but the phone says “note not found,” nine times out of ten the vault name differs. The Obsidian forum has the same case: the app opens on Android but the note doesn’t. Match it with &v=PhoneVaultName in the link.

    ③ Sync comes first. A link opens a note; it doesn’t create one. The note has to be synced to the phone (Part 13, LiveSync) for it to open.

    8. Cost

    • Cloudflare Worker: $0 (free plan, 100k requests/day)
    • Domain: already owned (Part 6), no extra charge for a subdomain
    • Home server load: none (the edge answers)

    In One Line

    Chat apps turn only http/https into links. An app link like obsidian:// needs one https shuttle inserted to become tappable. Put that shuttle on a Cloudflare Worker and it runs free, independent of your home server. Just include a button, since the auto-redirect can be blocked, and match the vault name to the device.

  • A Hands-Off Server — The Small cron Automations That Keep It Running

    A Hands-Off Server — The Small cron Automations That Keep It Running

    A Hands-Off Server — The Small cron Automations That Keep It Running

    Self-Hosting Build Guide, Part 14 (final). Backups (Part 12) and sync (Part 13) all have to run “at a set time, on their own.” That engine is cron.

    ← Previous: Part 13 — My Notes on My Server (Obsidian LiveSync)

    TL;DR

    • cron is the built-in Linux scheduler that runs commands at set times — nothing to install
    • What our server does on its own every day: backup · DB dump · cache cleanup · health check · summary
    • One crontab line = minute, hour, day, month, weekday + the command to run
    • Trap 1: cron has almost no PATH → without absolute paths you get “command not found”
    • Trap 2: silent failure — if you don’t log, you find out days later that it never ran

    1. What cron Is — an Alarm Clock With Nothing to Install

    cron ships with Linux by default. Register a time and a command — “run this script at 4:30 every morning” — and as long as the machine is on, it runs on its own. In self-hosting, almost every repetitive chore can be handed to cron.

    2. Reading One crontab Line

    The list of jobs is called the crontab. One line is five time fields + a command.

    Field Meaning Range
    1 minute 0–59
    2 hour 0–23
    3 day 1–31
    4 month 1–12
    5 weekday 0–6 (0=Sun)

    A * means “every.” So 30 4 * * * means 4:30 every day.

    30 4 * * *   /home/user/bin/db-dump.sh    >> /var/log/db-dump.log 2>&1
    0  5 * * *   /home/user/bin/backup.sh      >> /var/log/backup.log 2>&1
    

    3. What Our Server Does Every Day

    Small cron jobs stack up into a “hands-off server.”

    • DB dump — the database to a safe file (before the backup)
    • File backup — shipped to a NAS (Part 12)
    • Cache cleanup — clear piled-up temp files and expired cache
    • Health check — is the service alive? alert if it died
    • Summary — yesterday’s visitors and errors on one page

    4. The Three Traps Beginners Always Hit

    ① No PATH. A command that worked fine in your terminal dies under cron with “command not found.” cron runs in a minimal environment with a nearly empty PATH. Fix: use absolute paths (/usr/bin/python3), or set PATH at the top of your script.

    ② Silent failure. cron won’t show errors on screen. By default it only mails them, and nobody reads that. So, as in the example above, redirect with >> logfile 2>&1 to capture output and errors to a file — that’s how you trace a job that didn’t run.

    ③ Time zone. Check whether the server clock is UTC or your local time. Aim 30 4 * * * at “4:30 a.m.” and, if the server is on UTC, it may actually run in the early afternoon your time.

    5. The Compounding of Small Automations

    Each cron job is trivial — one line for backup, one for cache cleanup. But they stack, and the server reaches a state where it takes care of itself without you touching it daily. That’s the whole goal of self-hosting: build it, and let it keep running even after you forget about it.

    This series comes full circle here too. We started with an empty computer (Part 1), put up a blog, attached a domain, locked it down, backed it up, and now made it run on its own. What’s left is to stand one up yourself.


    In One Line

    cron is the engine behind “at a set time, on its own.” One line: minute, hour, day, month, weekday + command. Watch just three things — use absolute paths, keep logs, and check the time zone. Stack up small automations and the server stops needing your hands.

  • My Notes on My Server — Self-Hosting Obsidian LiveSync + CouchDB

    My Notes on My Server — Self-Hosting Obsidian LiveSync + CouchDB

    My Notes on My Server — Self-Hosting Obsidian LiveSync + CouchDB

    Self-Hosting Build Guide, Part 13. Once the blog lives on your server, your notes can too.

    ← Previous: Part 12 — Backing Up to a NAS Every Night (tar+ssh)
    → Next: Part 14 — A Hands-Off Server (cron)

    TL;DR

    • Obsidian’s official Sync is a monthly subscription — sync your notes to a database on your own server for $0 plus data ownership
    • How: the Self-hosted LiveSync plugin ↔ CouchDB (a Docker container), real-time and bidirectional
    • Always bind CouchDB to 127.0.0.1 only — beware Docker’s habit of opening it on 0.0.0.0
    • Reach it only from your own devices: inside a private network (a VPN mesh), never on the open internet
    • War story: the culprit behind ERR_SSL_PROTOCOL_ERROR wasn’t the certificate — it was “no server running behind it”

    1. Why Self-Host Instead of Official Sync

    Obsidian is a note app, and to see the same notes on multiple devices you need sync. Official Sync works well but it’s a monthly subscription. And your notes pass through someone else’s server.

    You already have a server running for the blog, so you can put note sync on top of it.

    Item Official Sync Self-hosted LiveSync
    Cost monthly fee $0 (your server)
    Data location company server your server
    Setup effort flip a switch configure yourself
    Sync fast real-time, bidirectional

    The price is “configure yourself.” Set it up once and it’s as smooth as the official option after that.

    2. The Shape — Plugin ↔ CouchDB

    CouchDB is a database good at exchanging changes in real time. The LiveSync plugin pushes to CouchDB whenever a note changes, and other devices pull from it.

    flowchart LR
        PC[Laptop Obsidian] -->|changes| DB[(CouchDB / your server, Docker)]
        Phone[Phone Obsidian] -->|changes| DB
        DB -->|sync| PC
        DB -->|sync| Phone
    

    3. Never Open It on 0.0.0.0

    This is the most important part. Spin up CouchDB with plain Docker and its port opens on 0.0.0.0 (every network interface). Now another device on the same Wi-Fi — even something past a misconfigured router — can reach your database.

    ports:
      - "5984:5984"             # dangerous: 0.0.0.0, exposed on every interface
      - "127.0.0.1:5984:5984"   # safe: only the computer itself
    

    Bind to 127.0.0.1 only and it’s invisible from outside the server. So how does your phone connect? Only from inside a private network (a VPN mesh). Build a virtual LAN that links just your own devices, and expose CouchDB over https only within it. To the whole internet it stays private.

    4. War Story — ERR_SSL_PROTOCOL_ERROR

    After finishing the setup I opened the https address and the browser spat out ERR_SSL_PROTOCOL_ERROR. Naturally I assumed it was a certificate problem and stared at certificates for a long time. It wasn’t.

    The real cause was simple: the CouchDB container itself wasn’t running. The https front end was set up, but with no server behind it to answer, the TLS connection never completed and the browser reported an “SSL error.”

    Lesson: an “SSL error” isn’t always about the certificate. Split the front end (address, cert) from the back end (the actual server), and check the back end is alive first. A quick curl against the backend port settles it in five seconds.


    In One Line

    You can bring note sync onto your own server too — the LiveSync plugin + CouchDB means $0 in subscription. Just bind CouchDB to 127.0.0.1 and reach it only over a private network. And when you hit ERR_SSL_PROTOCOL_ERROR, suspect a dead back end before the certificate.

  • Backing Up to a NAS Every Night — tar+ssh (and Why Not rsync)

    Backing Up to a NAS Every Night — tar+ssh (and Why Not rsync)

    Backing Up to a NAS Every Night — tar+ssh (and Why Not rsync)

    Self-Hosting Build Guide, Part 12. We secured the server (Part 11). Now: how to save the data even when the server dies.

    ← Previous: Part 11 — Free WordPress Defense (Wordfence)
    → Next: Part 13 — My Notes on My Server (Obsidian LiveSync)

    TL;DR

    • Self-hosting without backups is a time bomb — one dead disk and your posts, photos, and database vanish at once
    • Every night a cron job tars the important folders and ships them to a NAS over ssh
    • We deliberately skipped rsync (which needs its own daemon on port 873) — ssh is already open, so zero extra ports
    • The DB dump must finish before the file backup runs, so that dump is included in the same bundle — order creates consistency
    • “The backup ran” and “the backup succeeded” are different — a 0-byte file is a failure

    1. No Backups = a Time Bomb

    Putting a blog on your own computer means the backups the cloud used to handle are now your job. One SSD reaches end of life, or one mistyped command, and months of posts, photos, and databases disappear in an instant.

    The rule is simple: back up to a different physical device, automatically, every day. Manual backups always get forgotten.

    2. Why tar + ssh Instead of rsync

    For backups, most people reach for rsync — fast, with incremental sync. But to use rsync properly with a NAS (here, a Synology) you have to turn on its rsync daemon (port 873). That’s one more open port.

    Item rsync daemon tar + ssh
    Extra ports to open needs 873 0 (reuses existing ssh)
    Incremental transfer yes full each time
    Setup complexity service + permissions one line
    Attack surface grows unchanged

    Our data is small, so bundling it whole every day costs nothing. In that case there’s no reason to pay the price of another open port. Shrinking attack surface is almost always the right call in self-hosting.

    3. A One-Line Backup

    tar czf - /important/data/folder | ssh -p <port> [email protected] "cat > /backup/site-$(date +%F).tgz"
    

    tar czf - bundles the folder and streams it to stdout, and ssh catches that stream and drops it as a file on the NAS. No temp file in the middle, so it uses less disk too. The date ($(date +%F)) in the filename means each day stacks under a different name.

    4. Order Creates Consistency — DB First

    You can’t just copy a database’s folder. Copy it mid-write and you back up a half-written state. So first dump the DB safely to a file, and the file backup must run after that dump finishes.

    flowchart LR
        A[04:30 DB dump] --> B[dump file created]
        B --> C[05:00 file backup
    bundles the dump] C --> D[ship to NAS]

    That’s why the two are staggered. The order of the two cron jobs is the consistency of the backup.

    5. Verify “Success”

    The most common illusion: if cron fired on schedule, you assume the backup happened. But running is not succeeding. If the disk was full or the NAS briefly went down, you’re left with an empty 0-byte file.

    • After the backup, check the size of the file that was created
    • If it’s below a threshold, treat it as failed and send an alert
    • Auto-delete old backups (rotate) so the disk doesn’t fill up

    Add those three lines and your backup escapes the classic trap of “I thought it was there, but it was empty.”


    In One Line

    Back up to another device, automatically, every day. tar czf - … | ssh ships to a NAS without opening one more port. DB dump first, then the file backup. And check the size to verify “success” — an empty backup is not a backup.

  • Free WordPress Defense — Wordfence’s Free Tier and the Premium Trap

    Free WordPress Defense — Wordfence’s Free Tier and the Premium Trap

    Free WordPress Defense — Wordfence’s Free Tier and the Premium Trap

    Self-Hosting Build Guide, Part 11. The bots already arrived back in Part 8. Now we post a guard at the door.

    ← Previous: Part 10 — What Not to Publish (Privacy Review)
    → Next: Part 12 — Backing Up to a NAS (tar+ssh)

    TL;DR

    • WordPress runs over 40% of the web — which makes it the No.1 target for automated attack bots
    • Wordfence’s free tier gives you a firewall (WAF), two-factor auth (2FA), and a malware scanner — cost $0
    • The trap: after install, the screen hides the free option and pushes the paid plan ($149/yr) — the free key hides in a small underlined link
    • The only real free-vs-paid difference is “firewall rules arrive 30 days later” — irrelevant for a personal blog
    • The extended firewall must sit in front of PHP to truly block — one line in .htaccess, and back it up first

    1. Why WordPress is a Target

    WordPress powers more than 40% of all websites. High market share means that, from an attacker’s view, a single vulnerability can hit tens of millions of sites.

    As we saw in Part 8, the moment you publish a domain, brute-force attempts hit the login page within 24 hours — not humans, but automated bots. They also constantly probe known holes in plugins and themes. So WordPress is a system where “if it’s up, a guard goes up immediately.”

    2. What the Free Tier Gives You

    Feature Free tier Notes
    Firewall (WAF) Yes Blocks known attack patterns at the gate
    Two-factor auth (2FA) Yes One more layer if a password leaks — first defense against brute force
    Malware scanner Yes Detects tampered files and planted backdoors
    Real-time bad-IP list 30-day delay Paid is instant; free arrives 30 days later

    What a personal blog actually needs is the first three (firewall, 2FA, scan) — all free. The real-time IP list only matters for large, high-value targets.

    3. Finding the Free Key — the Store’s Paid Trap

    After installing, a “get your license” screen sends you to the Wordfence store. This is where it gets confusing.

    • The big blue button = “real-time protection” = paid ($149/yr). Sometimes two items are pre-added to the cart so it looks even pricier.
    • The free key hides below in a small underlined link: I'm OK waiting 30 days for protection updates.
    • Click that and a free license key arrives by email. Paste it into the plugin. Done. Cost $0.

    It looks like a checkout page, but you never have to pay. The whole task is finding that one sentence about waiting 30 days.

    4. Real Defense Sits in Front of PHP — the Extended Firewall

    In its default state the firewall runs inside WordPress (PHP). That means an attack request has already reached PHP before it gets inspected. One step too late.

    Turning on “Optimize the Firewall” adds one line to the web server config (.htaccess) so requests are checked before PHP runs:

    php_value auto_prepend_file '/var/www/html/wordfence-waf.php'
    
    flowchart LR
        V[Visitor/Bot] --> W[Extended firewall
    before PHP] W -->|clean| P[PHP / WordPress] W -->|attack| X[Blocked]

    ⚠️ Back up .htaccess before enabling this. Depending on your server, that one line can throw a 500 error — and the fix is to restore the backed-up .htaccess. Also, start in “learning mode” for a day or two so it learns your normal traffic, then it switches to protection automatically.

    5. Hide the Login Itself (Bonus)

    /wp-login.php is an address the whole world knows. A separate plugin lets you move the login to a different path, so the door bots keep knocking on simply disappears. Where you moved it stays secret, of course — that’s the whole point.

    Add 2FA on top, with a “remember this device for 7 days” option, and you keep the security without typing a code every time.


    In One Line

    The free Wordfence tier is enough to defend a personal blog. On the paid-nudge screen, just find the small “OK waiting 30 days” link and it’s $0. Back up .htaccess before the extended firewall, hide the login path, turn on 2FA. It’s not about money — it’s a few clicks.

  • What Not to Publish — Privacy Review for Self-Hosting Posts

    What Not to Publish — Privacy Review for Self-Hosting Posts

    What Not to Publish — Privacy Review for Self-Hosting Posts

    Self-hosting build series, Part 10. Locking down the server matters; so does locking down the story you write about it.

    ← Previous: Part 9 — Invisible Bridge: Cloudflare Tunnel
    → Next: Part 11 — Free WordPress Defense (Wordfence)

    TL;DR

    • A self-hosting post inherently publishes the shape of your server — a tightrope between tutorial value and reconnaissance data.
    • What I actually leaked: the Linux account name, the mini-PC internal IP, and a personal email (passwords and tokens, luckily, did not leak).
    • Five things to redact: secrets · personal info · internal network · file paths · frontmatter.
    • A leaked secret can only be rotated, never un-leaked — search engines have already cached it.
    • One line of grep before publishing lets the machine sweep for you.

    1. What I actually leaked

    After happily writing nine parts, I looked again — and found these scattered through the text.

    • /home/my-account/... — the Linux account name, sitting right in the path. A target for SSH brute-force attempts.
    • http://192.168.x.x:3001 — the mini-PC internal IP, effectively a sketch of my home network.
    • A personal Gmail address that slipped into a line about the domain.

    Passwords and tokens I had, luckily, written as example values like ${VAR} or <openssl rand -hex 24>, so they were safe. But the three above were real.

    The problem is that a public blog is permanent. Search engines index it; caches keep it. Deleting does not undo it. So if a secret has leaked, you don’t delete it — you rotate it immediately.


    2. The five things to redact

    Class Example Why it’s dangerous
    ① Secrets API keys, tokens, passwords, private keys Account / server takeover
    ② Personal info account name, email, real name Identity, targeting, spam
    ③ Internal network private IPs, internal hostnames, NAS address Lateral movement after a breach
    ④ File paths /home/my-account/… Exposes account name and layout
    ⑤ Frontmatter the metadata block at the top Easy to leak whole on publish

    ⑤ is the surprising one. In Markdown, the top of a file has metadata fenced by --- (frontmatter: author, date, filename, and so on). If the publishing conversion slips just once, this gets dumped into the body as a code block. One of my own posts was doing exactly that — exposing its entire frontmatter.


    3. Don’t delete — “generalize”

    The key is substitution, not deletion. Keep the tutorial value; hide only the parts that are really mine.

    Real Published
    /home/my-account /home/user
    internal IP 192.168.1.50 192.168.0.x or <server-ip>
    personal email [email protected]
    password / token ${PASSWORD}, <your-token>

    Spelling it out as <openssl rand -hex 24>“generate your own value here” — actually helps the reader follow along. Generalizing turns out to be the friendlier tutorial.


    4. An automatic pre-publish check (one line of grep)

    Catch it by eye every time and it will eventually slip. Before publishing, let the machine sweep.

    grep -rEn '/home/[a-z]+|[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+|@(gmail|naver)\.com|_TOKEN=|_KEY=|PASSWORD=' my-posts-folder/

    This catches account paths, IPs, personal email, and secret traces in one pass. The result must be zero lines before you publish. If only placeholders like ${VAR} or <...> remain, you pass.

    After publishing, look at the actual public page once more — a cache may still be serving the old version.

    curl -s https://your-domain/post-slug/ | grep -E '/home/|192\.168\.|@gmail'

    5. The bigger question — should this be public at all?

    Even after redacting every detail, one problem remains: the post itself is a map of my infrastructure.

    I run private operational tools that I never write about. No matter how well I hide keys and IPs, the mere fact that “a management tool with this shape lives here” is reconnaissance.

    So order matters. Decide “should this series be public?” first, and run the redaction above only on what you’ve chosen to publish. What can be hidden, and what should never be written at all, are two different layers of the problem.


    One-line takeaway

    If there’s a P0 for locking down the server, there’s a P0 for locking down the writing too. Generalize the five — account name, internal IP, personal email, secrets, frontmatter — into placeholders, and check with one line of grep before publishing. A leaked secret can only be rotated, so the cheapest move is never to write it down.

  • Invisible Bridge — Exposing Our Home Blog Externally with Cloudflare Tunnel

    Invisible Bridge — Exposing Our Home Blog Externally with Cloudflare Tunnel

    Invisible Bridge — Exposing Our Home Blog Externally with Cloudflare Tunnel

    Self-hosting build guide, Part 9. After buying a domain, how do we pull traffic all the way to our home computer?

    ← Previous: Part 8 — Bots Arrive in 24 Hours (Security P0)
    → Next: Part 10 — What Not to Publish (Privacy Review)

    TL;DR

    • Problem: WordPress runs on our home mini-PC, but how do we connect the purchased domain (sticknstone.org) to it from outside?
    • Old way: Open a router port → register public-IP DNS → renew SSL. Five headaches.
    • New way: Cloudflare Tunnel — the mini-PC pre-opens an outbound bridge to Cloudflare. No router hole, no IP exposure.
    • Tool: Run the cloudflared daemon with Docker. Five minutes.
    • Free bonus: WAF, DDoS protection, CDN, SSL, traffic stats — all free.
    • Cost: $0 (domain renewal ~$10/yr separate).

    1. The Problem to Solve

    The blog (WordPress) is hosted on our home mini-PC. The domain (sticknstone.org) is bought. So how do the two meet?

    The ordinary path (port forwarding): five headaches

    Headache Why
    Router hole Opening port 443 invites bot attacks forever
    Shared IP The ISP keeps changing it; needs an auto-renew script
    Location exposure The IP alone hints at your rough neighborhood
    SSL renewal Let’s Encrypt by hand every 90 days
    Traffic surge A DDoS can paralyze your home internet

    2. Another Path: Build a Bridge

    A shift in thinking: don’t accept connections coming in from outside. Instead, pre-create an outgoing connection from home to somewhere external.

    flowchart LR
        subgraph oldway["Old way — Port Forwarding"]
            A1[Visitor] --> A2[Open router port 443]
            A2 --> A3[Mini-PC public IP]
            A3 --> A4[WordPress]
            A5[Hacker] -.attack.-> A2
        end
        subgraph newway["New way — Cloudflare Tunnel"]
            B1[Visitor] --> B2[Cloudflare Seoul DC]
            B2 -. pre-opened outbound channel .-> B3[Mini-PC cloudflared]
            B3 --> B4[WordPress]
            B5[Hacker] -.blocked.-> B2
        end
        style A5 fill:#fdd,stroke:#a00
        style B5 fill:#dfd,stroke:#0a0
        style A3 fill:#fdd
        style B3 fill:#dfd

    The key: the mini-PC reaches out first. Instead of outside → home, keep an always-open outbound channel home → Cloudflare. Visitor requests are forwarded by Cloudflare through that channel to the mini-PC.

    Hotel analogy

    A hotel guest (the mini-PC) wants to meet a friend.

    • Ordinary path: Post your room number at the entrance. The friend walks in by that number — and so does the thief.
    • Bridge path: Leave a note with the front desk (Cloudflare): connect anyone asking for my friend to my room. The friend just gives a name; the desk routes them. The room number is never exposed, and the desk screens suspicious visitors first.

    3. Traffic Flow — When a Visitor Types the Domain

    sequenceDiagram
        participant V as Visitor browser
        participant CF as Cloudflare Seoul DC
        participant CD as cloudflared (mini-PC)
        participant WP as WordPress container
        V->>CF: GET https://sticknstone.org
        Note over CF: WAF/DDoS first filter
        CF->>CD: Forward via pre-opened outbound channel
        CD->>WP: http://localhost:8090
        WP-->>CD: HTML response
        CD-->>CF: Reply via outbound channel
        CF-->>V: SSL termination + CDN cache + response

    Four steps: Visitor → Cloudflare (filter) → bridge → mini-PC → reverse. The mini-PC IP never appears anywhere.


    4. The Bridge Keeper: cloudflared

    The small daemon that holds this outbound channel open from home is cloudflared — Cloudflare’s official open source.

    Why run it with Docker

    Docker is already running on the mini-PC. It’s natural for cloudflared to stay in the same hotel as the other containers.

    flowchart TB
        subgraph minipc["Mini-PC (Ubuntu 24.04)"]
            subgraph docker["Docker host"]
                CD[cloudflared / network_mode host]
                WP[wordpress-wordpress-1 :8090]
                DB[(wordpress-db-1 MariaDB)]
                RD[(wordpress-redis-1 Object Cache)]
                UM[umami_umami-1 :3001]
                UDB[(umami_db-1 Postgres)]
            end
            WP --> DB
            WP --> RD
            UM --> UDB
        end
        CF[Cloudflare network] -. outbound .-> CD
        CD --> WP
        CD --> UM
        style CD fill:#ffe4b5,stroke:#d97706
        style CF fill:#dbeafe,stroke:#1d4ed8

    Only cloudflared runs with network_mode: host so it can reach localhost:8090 (WP) and localhost:3001 (Umami) on the host.

    5-minute install guide

    Folder + config files:

    /home/user/cloudflared/
    ├── .env                # TUNNEL_TOKEN=eyJh... (token from the Cloudflare dashboard)
    └── docker-compose.yml

    docker-compose.yml:

    services:
      cloudflared:
        image: cloudflare/cloudflared:latest
        container_name: cloudflared
        restart: unless-stopped
        network_mode: host
        command: tunnel --no-autoupdate run --token ${TUNNEL_TOKEN}
        env_file: .env

    Start + verify logs:

    cd /home/user/cloudflared
    sudo docker compose up -d
    sudo docker logs cloudflared --tail 20

    A successful log shows four lines (simultaneous connections to four Cloudflare Seoul data centers):

    Registered tunnel connection ... location=icn06 protocol=quic
    Registered tunnel connection ... location=icn01 protocol=quic
    Registered tunnel connection ... location=icn05 protocol=quic
    Registered tunnel connection ... location=icn06 protocol=quic

    The mini-PC connects to Cloudflare over four backup channels. If one dies, the other three stay alive.


    5. Tokens Are Risky — Where to Keep Them

    The token cloudflared uses to authenticate with Cloudflare is shown only once at issuance.

    Principles for handling the token

    • Store it in plaintext in .env, with chmod 600 (owner read/write only).
    • Never paste it in plaintext into chats, notes, or git commits.
    • If you suspect exposure, revoke and reissue from the Cloudflare dashboard immediately.

    The exact commands

    sudo chown user:user /home/user/cloudflared/.env
    sudo chmod 600 /home/user/cloudflared/.env

    6. What Comes Along for Free

    Just by laying this bridge on Cloudflare’s free plan, these bonuses come along:

    Bonus What it does Typical price
    WAF Auto-blocks known attacks like SQL injection and XSS $20/mo~
    DDoS protection Absorbs traffic surges automatically, unlimited $200/mo~
    CDN Caches static files worldwide; fast for foreign visitors $10/mo~
    Auto SSL Zero worry about Let’s Encrypt renewals $5/mo~
    Traffic stats Graphs in the Cloudflare dashboard (analytics tools)

    Total cost: $0. Only ~$10/yr for domain renewal.


    7. Pitfalls — What We Actually Hit

    Pitfall 1: Confusing the Operating System choice

    The Cloudflare dashboard’s cloudflared install screen asks you to pick an OS. At first I picked Windows, thinking of my own Windows PC. Wrong. cloudflared installs on the mini-PC (Ubuntu), not my Windows machine. The right choice was Docker.

    Pitfall 2: Pasting the token straight into LLM chat

    If you send the copied token into an LLM chat, that content is stored on an external server. The token is as sensitive as a card PIN. Revoke it from the Cloudflare dashboard as soon as you’re done.

    Pitfall 3: How does cloudflared forward to other services?

    Just starting the cloudflared container connects it to Cloudflare, but where incoming requests go is a separate setting — the Public Hostname config. Covered in the next part (Part 10).


    8. Validation

    Right after startup, check Connection Status at the bottom of the Tunnel page in the Cloudflare dashboard.

    Before:  No connection detected yet
    After:   Connected (check)

    When that one line flips, the bridge is up.


    FAQ

    Q. How is this different from Tailscale Funnel?
    Both bypass NAT and hide the mini-PC IP. Cloudflare Tunnel also bundles WAF, DDoS, CDN, and use of your own domain for free. Tailscale Funnel forces a dedicated domain and offers no shield. For a blog, Cloudflare wins.

    Q. What if the mini-PC shuts down?
    The bridge drops. Visitors see a 521 (Web server is down) from Cloudflare. When the mini-PC boots again, cloudflared reconnects automatically (Docker restart: unless-stopped).

    Q. Does Cloudflare see the traffic? Privacy concern?
    Cloudflare terminates SSL, so it can see plaintext traffic. The trust model assumes Cloudflare is on your side. If you don’t trust it, the Tunnel isn’t a fit (alternative: your own reverse proxy + Let’s Encrypt + dynamic DNS).

    Q. Limits of the free plan?
    Unlimited bandwidth, unlimited sites, unlimited tunnels. Extras like Workers and R2 have separate limits. For running one blog, the free plan is enough for life.

    Q. Does the domain have to be .org?
    No. .com / .net / .io / .dev all work the same. Using Cloudflare DNS for the domain is just the easiest (automatic DNS registration).


    Next Part Preview

    Part 10 sets the Public Hostname to map sticknstone.org → WordPress, and covers what you must not write in a post (privacy review).


    One-Line Summary

    To run a blog on a home computer, skip the old method of punching a router hole and exposing your IP. Instead, pre-open an outbound channel from the computer to Cloudflare — that’s Cloudflare Tunnel. The little daemon that keeps the channel open is cloudflared. Five minutes with Docker.


    References

  • Star Whale Analytics Lab — Visitor Statistics with Umami

    Star Whale Analytics Lab — Visitor Statistics with Umami

    Star Whale Analytics Lab — Visitor Statistics with Umami

    Self-hosting build series part 5. Taking data sovereignty to the mini-PC.


    TL;DR

    • Google Analytics free = I give visitor data to Google servers + forced cookie consent pop-ups
    • Umami self-hosted = own analysis on mini-PC. No cookies, data owned by me, automatic exemption from GDPR
    • Docker Compose in 5 minutes. Automatic injection of one line tracker into WP = done
    • Display: visitor count, popular posts, traffic sources, devices, countries, dwell time

    1. The Hidden Costs of Google Analytics

    Item Google Analytics 4 Umami self-hosted
    Cost Free (personal use) Free (open source)
    Data Ownership Google Own mini-PC
    Cookie Consent Popup Required (GDPR/CCPA) Unnecessary (0 cookies)
    Page Load Load ~50KB JS ~2KB JS
    User Tracking Cross-site (linked to Google ads) Same site only
    South Korea PIPA Compliance Complicated Automatic exemption
    Setup Complexity 1 hour+ 5 minutes

    Google Analytics externalizes the real costs to the user (the visitor). Is it normal for Google to take all that data just because a visitor entered my site? For a personal blog, it is right to operate it ourselves.


    2. Structure

    

    When the visitor browser receives the Star Whale page, the script in the head <script defer src="https://analytics.sticknstone.org/script.js" data-website-id="..."> executes → asynchronous request to Umami → Umami logs to Postgres. The page display is not interrupted.


    3. Docker Compose

    /home/user/umami/docker-compose.yml:

    services:
      umami:
        image: ghcr.io/umami-software/umami:postgresql-latest
        restart: unless-stopped
        depends_on:
          db:
            condition: service_healthy
        environment:
          DATABASE_URL: postgresql://umami:${DB_PASSWORD}@db:5432/umami
          DATABASE_TYPE: postgresql
          APP_SECRET: ${APP_SECRET}
        ports:
          - "3001:3000"
    
      db:
        image: postgres:16-alpine
        restart: unless-stopped
        environment:
          POSTGRES_DB: umami
          POSTGRES_USER: umami
          POSTGRES_PASSWORD: ${DB_PASSWORD}
        volumes:
          - umami_db:/var/lib/postgresql/data
        healthcheck:
          test: ["CMD-SHELL", "pg_isready -U umami"]
          interval: 5s
          timeout: 5s
          retries: 6
    
    volumes:
      umami_db:
    

    .env:

    APP_SECRET=$(openssl rand -hex 32)
    DB_PASSWORD=$(openssl rand -hex 24)
    

    Run:

    cd /home/user/umami
    docker compose up -d
    curl -I http://localhost:3001  # HTTP/1.1 200
    

    Default login: admin / umami. Change the password immediately.


    4. Registering Star Whale + Receiving Tracker

    Umami dashboard → Websites → Add Website:

    Field Value
    Name Star Whale Logbook
    Domain sticknstone.org

    Save → Click site → Edit → Tracking Code:

    <script defer src="https://analytics.sticknstone.org/script.js" data-website-id="1892c870-dd4d-4ce7-9fd6-2635f8ef6220"></script>
    

    5. Automatic Injection into WP — mu-plugin

    The code is automatically placed, not manually by me for every post.

    /var/www/html/wp-content/mu-plugins/umami-tracker.php:

    <?php
    /**
     * Plugin Name: Umami Analytics Tracker
     * Description: Injects Umami tracker into front-end pages. Excludes logged-in users.
     */
    if (!defined('ABSPATH')) exit;
    
    add_action('wp_head', function () {
        if (is_admin()) return;
        if (is_user_logged_in()) return;  // Exclude my own visits
        ?>
        <script defer src="https://analytics.sticknstone.org/script.js" data-website-id="1892c870-dd4d-4ce7-9fd6-2635f8ef6220"></script>
        <?php
    }, 100);
    

    Checking is_user_logged_in() = the number of times I’ve previewed my own posts will not be counted in the statistics. Surprisingly important.


    6. What is Visible on the Dashboard

    Screen Data
    Overview Daily, weekly, monthly visitors, page views, sessions, dwell time
    Pages Ranking of popular posts
    Referrers Traffic sources (Google, Twitter, other sites)
    Browsers Chrome, Safari, Firefox ratio
    OS Windows, Mac, iOS, Android
    Devices Desktop, mobile, tablet
    Countries Visitors by country
    Languages Browser languages
    Events Clicks and downloads defined by me

    Automatically updates every 5 seconds. Realtime menu = see who is currently viewing which page.


    7. Reason to Handle Domain Linking Together

    When the tracker is first received, the URL is http://192.168.0.x:3001/script.js (LAN IP). This causes:
    – HTTP → Browser Mixed Content block
    – LAN IP → Uninterpretable from external visitor PCs

    Umami must be activated after connecting the domain (part 9). It needs to be exposed externally as analytics.sticknstone.org to function.


    FAQ

    Q. Is there a free cloud plan for Umami?
    Umami Cloud starts at $9 per month. Self-hosted is unlimited and free. If you have a mini-PC, self-hosted is the answer.

    Q. Can I migrate from Google Analytics?
    No past data import. It’s a fresh start. However, both tools can be operational concurrently (both trackers can be placed).

    Q. Do I need to worry about South Korea’s PIPA (Personal Information Protection Act)?
    Umami does not store IP addresses (only hashes), and there are no cookies. It qualifies for PIPA exemption.

    Q. Is there a mobile app?
    No official app. The mobile web is responsive, so it displays well on phones.

    Q. Can I export data as CSV?
    CSV and JSON export are available on the dashboard. API is also provided.


    Next Part Preview

    Part 6 — Domain Fraud: Cloudflare Registrar. Reasons for purchasing from Cloudflare with Gabia and WhoIs (price, management integration, future Tunnel connection).


    Summary in One Line

    The real cost of Google Analytics is the externalization of user data. Umami self-hosted = 5 minutes of Docker on the mini-PC + automatic WP mu-plugin injection = data sovereignty + no cookies + the same information.

  • Domain Purchase — Cloudflare Registrar

    Domain Purchase — Cloudflare Registrar

    Self-hosting deployment story Part 6. A 30-minute decision that lasts a lifetime.


    TL;DR

    • Comparison of five domain registrars: Gabia, Whois, GoDaddy, Namecheap, Cloudflare
    • Star Whale picks Cloudflare Registrar — cost + DNS + Tunnel + Email in one account
    • .org is about $10/year, 30–50% cheaper than Korean registrars
    • Caveat: 60-day transfer lock after registration; WHOIS privacy protection is free

    1. Choosing a Domain Name

    It took three days to settle the name. Here is the candidate comparison.

    Candidate Meaning Korean SEO English SEO Trademark
    byeolgorae.com Romanization of Star Whale Weak Hard to pronounce “byeolgorae” None
    starwhale.com English translation Weak starwhale.ai is already an ML company ⚠ Conflict
    firewhale.com Fire + Whale (intensity of fire + depth) Weak firewhale.io app and coin dominate ⚠ Conflict
    sticknstone.org Stick & Stone (an ancient navigator’s stellar tool) Clean Clean None

    Prioritizing the metaphor: separate the brand name (Star Whale) from the domain (sticknstone). It reads naturally once explained on the About page — “Stick & Stone is an ancient navigator’s tool that pointed at the stars with small stones and sticks; Star Whale sets out on a deep voyage toward that star.”


    2. Comparison of 5 Registrars

    flowchart LR
        A[Domain Purchase] --> B[Gabia·Whois
    Domestic] A --> C[GoDaddy
    International] A --> D[Namecheap
    International] A --> E[Porkbun
    International Startup] A --> F[Cloudflare
    Registrar] B -. Expensive/Renewal Pressure .-> X[Rejected] C -. Aggressive Marketing .-> X D -. Average .-> Y[2nd Place] E -. Good Price .-> Y F -. Cost+Integration .-> Z[Selected] style F fill:#dfd,stroke:#0a0 style Z fill:#dfd,stroke:#0a0

    Price comparison for .org domains for 1 year (as of May 2026):

    Registrar Price Renewal Remarks
    Gabia 22,000 KRW 22,000 KRW UI is Korean-friendly
    Whois 19,000 KRW 19,000 KRW Card charge system
    GoDaddy $20 → $35 for renewal Increases annually Increased marketing push
    Namecheap $13 $15 WHOIS protection is free
    Porkbun $9 $11 Startup, stability unknown
    Cloudflare $10.44 $10.44 Cost (0 resale margin)

    Cloudflare sells domains at cost price. Its revenue source comes from other services (Workers, Stream, Enterprise). For one-person operators like I, the cheapest + renewal cost is stable.


    3. Why Choose Cloudflare — Integrated Value

    Feature Other Registrars Cloudflare
    DNS Management Separate service (Route 53, etc.) Integrated free of charge
    WHOIS Protection Typically paid ($10/year) Free automatic
    Cloudflare Tunnel External routing separately One-click in the same account
    Email Routing (Receiving) Separate G Suite ($6/month) Free unlimited
    SSL Certificate Automatic (Let’s Encrypt directly) Automatic (Cloudflare wildcard)
    WAF·DDoS·CDN Separate ($20+) Automatically applied to the same domain

    Star Whale would ultimately utilize Cloudflare Tunnel + DNS + WAF + (later) Email Routing. If the domain is purchased elsewhere, an additional task of transferring nameservers to Cloudflare is needed. Purchasing from the same place automates that step.


    4. Purchase Process — 5 Minutes

    1. dash.cloudflare.com → Domains → Buy domain
    2. Search sticknstone → Select .org
    3. Enter registrant information (name, email, address)
      – WHOIS is automatically masked (anonymously like Contact Privacy Inc.)
    4. Card payment ($10.44)
    5. Immediately activated. Nameserver automatically points to Cloudflare

    If purchased from another registrar, changing the nameserver to Cloudflare takes an additional step (24-48 hours for propagation). Purchasing from Cloudflare negates that time.


    5. Caveats

    Caveat 1. 60-day Transfer Lock

    For 60 days after domain registration, transfer to another registrar is not permitted (ICANN rule). If there’s no immediate need to transfer it to somewhere other than Cloudflare, it’s irrelevant.

    Caveat 2. WHOIS Privacy

    Domain registrant information is usually public. Name, email, address, and phone are registered on the WHOIS DB. Cloudflare automatically masks this (other registrars charge around $5-$10/year for this option). Email is displayed as an anonymous address like [email protected].

    Caveat 3. Changing Payment Card

    Automatic renewal fails if the card expires/changes → domain expiration → 30-day grace period → Recovery. Anyone can take it back. Updating Cloudflare payment information is essential when changing the card.

    Caveat 4. Domain Price Fluctuation

    While Cloudflare’s pricing has little variability due to cost price, new TLDs (.io, .dev, .app) increase slightly each year. .org, .com, and .net remain stable.

    Caveat 5. If the Site Goes Down?

    Even if the mini-PC fails or cloudflared terminates, the domain itself remains owned by I. The DNS records remain alive in the Cloudflare dashboard. If the mini-PC is restored, it will function again.


    FAQ

    Q. Is .org better? What about .com?
    .com is the most common, but good names are mostly taken. .org feels natural for non-profits and personal notes. There is almost no SEO difference (per Google’s official statement).

    Q. What about Korean domains (별고래.한국)?
    Possible. However, it is difficult for foreigners to input + some compatibility issues with certain tools + lacks trust. A combination of English domain and Korean content is standard.

    Q. What about bundled products (Gabia HostingNW, etc.)?
    For one-person self-hosting mini-PC, it’s irrelevant. Hosting in bundled products usually means shared hosting (sharing the server with others). Less control.

    Q. How does Cloudflare Email Routing work?
    [email protected] → Cloudflare receives → forwards to my personal email ([email protected]). Free unlimited. Separate SMTP is needed for sending (Resend·Mailgun free tier).

    Q. Can I trust domain valuation sites (Estibot, etc.)?
    For a one-person operation, it’s irrelevant. The use value of a domain (is it good for me?) is more crucial than market value (who would buy it).


    Next Episode Preview

    Part 7 — It’s Complicated: Naming the Site. Three days’ worth of contemplation and decisions on the English subtitle after changing “Jam Jam’s Voyage Diary” to “Star Whale.”


    One-Line Summary

    Purchasing a domain is a lifelong decision. Cloudflare Registrar = cost price + DNS + Tunnel + Email + WHOIS protection integrated in one account. .org costs $10.44 per year. Half the price compared to Gabia.
    “`