11 min read

How to Self-Host a Blog for Almost Nothing

How to Self-Host a Blog for Almost Nothing

If you've got something you want to share online and you want it under your control rather than subject to the whims of social media TOS, it's never been easier to start publishing while keeping your home network safe. Whatever the reason, you don't need to hand over $25 a month to WordPress or Ghost's managed hosting to make it happen.

With a spare computer, a $10 domain, and some free software, you can run your own lightweight blog that looks professional, loads fast, and doesn't cost you anything beyond electricity and the domain renewal. Self hosting helps make the internet more decentralized, and I'm going to teach you how to do it.

What You'll Need Before Starting

You can easily do this with old discarded hardware.

Hardware: Any computer that can stay powered on. An old laptop, a mini PC, a Raspberry Pi 4 or 5, or a spare desktop all work. Ghost runs comfortably on 2GB of RAM with a couple of CPU cores. If you've got an old machine collecting dust, this is a perfect use for it.

Software: You'll install Ubuntu Server (or Debian) and Docker. Both are free.

A Domain Name: This is your only real recurring cost. A .com runs about $10-11 per year through Cloudflare, which sells domains at wholesale cost with zero markup.

A Cloudflare Account: Free. You'll use it for DNS, the tunnel, SSL certificates, and CDN caching. All included on their free tier.

Time: Plan for a couple of hours your first time through. Once you understand the pieces, you could repeat this setup in about 30 minutes.

Step 1: Buy Your Domain

You have two solid options for domain registration, and both work fine for this setup.

Cloudflare Registrar is the simplest choice if you're going to use Cloudflare for everything else anyway (and you are, in this guide). They sell domains at the exact wholesale price the registry charges. No markup, no hidden fees, no surprise renewal price hikes. A .com domain runs about $10.46 per year, and the renewal price is the same as the registration price. That alone makes them worth considering, because most registrars lure you in with a cheap first year and then jack up the renewal.

To register through Cloudflare, create a free account at cloudflare.com, navigate to "Domain Registration" in the sidebar, search for the domain you want, and follow the checkout process. Done.

Namecheap is the other popular option. They frequently run promotions where you can grab a .com for under $6 the first year, though renewals are higher (typically around $13-15 per year). Their interface is straightforward and they include free WHOIS privacy, which keeps your personal information out of public domain records. Cloudflare also includes WHOIS privacy for free, so that's a wash.

If you buy through Namecheap but want to use Cloudflare for DNS (which you'll need for the tunnel), you'll need to point your domain's nameservers to Cloudflare after purchase. Cloudflare walks you through this when you add a site to your account. It takes about five minutes and usually propagates within an hour, though it can take up to 24 hours.

If you want the least amount of friction, buy through Cloudflare. Everything stays under one roof.

Step 2: Set Up Your Server

You need a machine running Linux with Docker installed. If you're already running a home lab with Proxmox or another hypervisor, spin up an Ubuntu Server VM and allocate it 2GB of RAM and 2 CPU cores. If you're working with bare metal (an old laptop, mini PC, whatever), just install Ubuntu Server directly.

Install Ubuntu Server

Download Ubuntu Server from ubuntu.com and flash it to a USB drive using Rufus (Windows), Balena Etcher, or the dd command on Linux/Mac. Boot from the USB, follow the installer, and make sure you check the box to install OpenSSH server during setup. You'll want to manage this machine remotely.

Once Ubuntu is installed and you're logged in, update everything:

sudo apt update && sudo apt upgrade -y

Install Docker

Docker makes running Ghost painless. Instead of manually installing Node.js, MySQL, and managing dependencies, you just tell Docker what containers you want and it handles the rest.

sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

Add your user to the docker group so you don't need sudo for every docker command:

sudo usermod -aG docker $USER

Log out and back in for the group change to take effect.

Step 3: Deploy Ghost with Docker Compose

Create a directory for your Ghost installation and set up the Docker Compose file.

mkdir -p ~/docker/ghost
cd ~/docker/ghost

Create a file called docker-compose.yml with the following contents. Replace yourdomain.com with your actual domain and set a real password for MySQL (not the placeholder).

services:
  ghost:
    image: ghost:5
    restart: always
    ports:
      - "2368:2368"
    depends_on:
      - db
    environment:
      url: https://yourdomain.com
      database__client: mysql
      database__connection__host: db
      database__connection__user: root
      database__connection__password: your_strong_password_here
      database__connection__database: ghost
    volumes:
      - ghost-content:/var/lib/ghost/content

  db:
    image: mysql:8
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: your_strong_password_here
    volumes:
      - ghost-db:/var/lib/mysql

volumes:
  ghost-content:
  ghost-db:

A few things to note about this configuration. The url field must be set to your full domain with https:// because that's how Ghost generates links, handles redirects, and builds your sitemap. Even though Ghost itself isn't handling SSL (Cloudflare will), Ghost needs to know the public URL visitors will use.

The MySQL password needs to match in both the ghost and db service sections. Pick something strong. This database never gets exposed to the internet, but good habits are good habits.

Start everything up:

docker compose up -d

Docker will download the Ghost and MySQL images, create the containers, and start them. Give it a minute or two on the first run. You can check that everything is running with:

docker compose ps

Both containers should show a status of "Up." At this point, Ghost is running locally on port 2368. If you open a browser on the same network and go to http://your-server-ip:2368, you should see the default Ghost blog. Don't worry about setting up your admin account yet. We'll do that after the tunnel is connected, so everything works through your actual domain from the start.

Step 4: Create a Cloudflare Tunnel

This is the part that makes self-hosting from home actually viable. Traditionally, hosting a website from your house meant opening ports on your router, dealing with dynamic IP addresses, setting up SSL certificates, and hoping nobody found your home IP and used it to do something unpleasant. Cloudflare Tunnels eliminate all of that.

A Cloudflare Tunnel creates an outbound-only connection from your server to Cloudflare's network. Your server reaches out to Cloudflare, not the other way around. That means no open ports on your router, no exposing your home IP address, and Cloudflare handles SSL termination automatically. Visitors connect to Cloudflare, and Cloudflare routes the traffic through the tunnel to your server. Your home IP never appears in DNS records or HTTP headers.

We're going to create the tunnel through Cloudflare's Zero Trust dashboard and run the cloudflared connector as a Docker container. This keeps everything containerized and easy to manage.

Create the Tunnel in Cloudflare's Dashboard

Log into your Cloudflare account and navigate to Zero Trust (you can get there directly at dash.cloudflare.com). If this is your first time, Cloudflare will walk you through a quick setup. The free plan is all you need.

Once you're in the Zero Trust dashboard, go to Networks > Connectors and click Create a tunnel. Select Cloudflared as the connector type. Give your tunnel a name, something like "ghost-blog" works fine.

After you name it and save, Cloudflare will show you an install command that contains a long token string. This token is what authenticates your cloudflared container with Cloudflare's network. Copy that token and save it somewhere safe. You'll paste it into your Docker Compose file in the next step.

The token is a long base64-encoded string that looks something like eyJhIjoiYWJjMTIz.... It contains your account ID, tunnel ID, and a secret, all bundled together. That single string is the only credential cloudflared needs to connect.

Configure the Public Hostname

Before leaving the dashboard, you need to tell the tunnel where to route traffic. On the tunnel's configuration page, go to the Public Hostnames tab and add a new public hostname.

Set the Domain to your domain (leave subdomain blank for the root domain), and under Service, choose HTTP as the type and enter localhost:2368 as the URL. This tells Cloudflare that when someone visits your domain, it should route that request through the tunnel to port 2368 on the machine running cloudflared, which is where Ghost is listening.

If you want both yourdomain.com and www.yourdomain.com to work, add a second public hostname entry for the www version. Same settings, same service URL.

Cloudflare automatically creates the necessary DNS records (CNAME entries) when you add public hostnames. You don't need to set those up manually.

Save it and move on.

Step 5: Run Cloudflared with Docker

Now we set up the cloudflared connector as its own Docker Compose stack. Keeping it separate from Ghost is cleaner for management. If you ever want to tunnel other services later, you just add more public hostnames in the Cloudflare dashboard without touching your Ghost setup.

mkdir -p ~/docker/cloudflared
cd ~/docker/cloudflared

Create a docker-compose.yml file:

services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared
    restart: unless-stopped
    command: tunnel --no-autoupdate run
    network_mode: host
    environment:
      - TUNNEL_TOKEN=your_tunnel_token_here

Replace your_tunnel_token_here with the token you copied from the Cloudflare dashboard.

There are a couple of things worth explaining about this setup.

We're using network_mode: host so that the cloudflared container shares the host machine's network stack. This is important because we told the Cloudflare dashboard to route traffic to localhost:2368. With host networking, cloudflared sees the same localhost as the host machine, so it can reach Ghost on that port. Without it, the container would have its own isolated network and localhost would refer to the container itself, not the machine Ghost is running on. You'd get "connection refused" errors and spend an hour wondering what went wrong.

We're passing the token through the TUNNEL_TOKEN environment variable rather than putting it on the command line with --token. This is the preferred method because environment variables don't show up in process listings the way command-line arguments do. Anyone who can run ps aux on the machine would be able to see a token passed via --token, but not one set as an environment variable. Small security detail, but a good habit.

The --no-autoupdate flag is there because auto-updating inside a Docker container doesn't work the way you'd want it to. When you need to update cloudflared, you pull a new image instead.

Start the tunnel:

cd ~/docker/cloudflared
docker compose up -d

Check that it connected:

docker compose logs -f

You should see log lines showing cloudflared registering connections to Cloudflare's edge network. It typically establishes four connections to different data centers for redundancy. Once you see those connection messages, your tunnel is live.

Go back to the Zero Trust dashboard. Under Networks > Tunnels, your tunnel should show a status of Healthy. If it does, open a browser and navigate to your domain. You should see the default Ghost blog.

If the tunnel shows as healthy but you can't reach your site, double-check that the public hostname configuration in the dashboard has the service type set to HTTP (not HTTPS) and the URL set to localhost:2368. A common mistake is selecting HTTPS for the service type, but Ghost isn't running SSL locally, so the tunnel needs to connect to it over plain HTTP. Cloudflare handles the HTTPS part between visitors and their edge servers.

Step 6: Configure SSL and Cloudflare Settings

Log into the main Cloudflare dashboard (not Zero Trust) and navigate to your domain. There are a few settings worth adjusting.

SSL/TLS: Go to SSL/TLS and set the encryption mode to "Full." This encrypts traffic between visitors and Cloudflare, and between Cloudflare and your tunnel. Don't use "Full (Strict)" here because your origin isn't serving a publicly trusted certificate, the tunnel handles that layer.

Caching: Cloudflare automatically caches static assets like images, CSS, and JavaScript. This means repeat visitors load most of your site from Cloudflare's edge servers rather than your home connection. For a blog, this is a huge deal. Your upload bandwidth matters a lot less when Cloudflare is serving cached copies of your content from data centers all over the world.

Page Rules or Cache Rules (optional): If you want to get more aggressive with caching, you can create a cache rule that caches everything on your domain except the /ghost/ admin path. This keeps your public-facing blog fast while ensuring the admin panel always pulls fresh data.

Security: Under the Security section, Cloudflare's free plan includes basic DDoS protection and a web application firewall. For a personal blog, the defaults are solid. You're probably not a high-value target, but it's nice to have a buffer between your home network and the internet anyway.

Step 7: Set Up Your Ghost Blog

Now that everything is connected and your domain is live, head to https://yourdomain.com/ghost/ in your browser. Ghost will walk you through the initial setup, where you'll create your admin account, name your site, and optionally invite other users.

A few things worth doing right away.

Pick a theme. Ghost ships with Casper, which is clean and works well for a blog. If you want something different, Ghost's marketplace has free and paid themes. You can also build your own or modify Casper.

Set up your navigation. Ghost lets you customize the menu structure under Settings > Navigation. Keep it simple. Most blogs only need a few links: Home, About, and maybe a Contact page.

Configure your publication settings. Under Settings > General, set your site title, description, and meta data. This is what shows up in search results, so take a minute to write something useful.

Write your first post. Ghost's editor is genuinely good. It supports Markdown, embedded content, image galleries, and a clean distraction-free writing interface. Write something, publish it, and verify it loads correctly through your domain.

Keeping Things Running

Self-hosting means you're the IT department. That's the trade-off for control and cost savings. A few maintenance tasks will keep everything healthy.

Updating Ghost: With Docker, updating is straightforward. Navigate to your Ghost directory and pull the latest images:

cd ~/docker/ghost
docker compose pull
docker compose up -d

This pulls the latest Ghost and MySQL images and restarts the containers. Your content and database persist in Docker volumes, so nothing gets lost.

Updating Cloudflared: Same process, different directory:

cd ~/docker/cloudflared
docker compose pull
docker compose up -d

Backups: Don't skip this. Ghost stores your content in the MySQL database and your images on disk (in the Docker volume). A simple backup script that dumps the database and copies the content volume to an external drive or cloud storage is enough. Run it weekly, or set up a cron job. You can also export your content from Ghost's admin panel under Settings > Labs > Export, which gives you a JSON file of all your posts.

Server Updates: Keep Ubuntu patched. A quick sudo apt update && sudo apt upgrade -y once a week, or enable unattended-upgrades to handle security patches automatically.

Monitoring: If your server goes down, your blog goes down. For a personal blog, that's probably fine. If uptime matters more to you, look into a free uptime monitor like UptimeRobot that will ping your site every few minutes and alert you if it goes offline.

What This Costs

The total annual cost for this setup breaks down to roughly $10-11 for the domain and whatever electricity your server consumes. A mini PC or Raspberry Pi draws maybe 10-15 watts, which adds about $1-2 per month to your electric bill depending on your rates.

Compare that to Ghost's managed hosting at $25 per month (their cheapest plan) or WordPress hosting at $4-25+ per month, and the savings add up fast. You also own everything. Your content, your server, your data. Nobody changes the terms of service on you, nobody injects ads, nobody raises prices because they had a rough quarter.

The trade-off is your time. You're responsible for updates, backups, and troubleshooting. For most people comfortable enough to follow this guide, that amounts to maybe an hour per month.

Wrapping Up

Running your own blog isn't just about saving money, though the economics are hard to argue with. It's about having a space that's entirely yours, where you control what gets published, how it looks, and who has access to your audience's data.

Ghost is a solid platform for this. The editor is simple, it looks great, and the software is actively maintained by a team that cares about open source. Paired with a Cloudflare Tunnel, you get the security and performance benefits of a major CDN without exposing your home network or paying for a VPS.

You have expertise that others want, and this is one of the cheapest and most capable ways to start. Buy the domain this weekend, set aside a couple hours, and build the thing.