State Of The Blog

Just a quick opportunity to check in with you all and say thanks!

Don't worry, nothing is changing. I just wanted to write this as a quick thank you to all of you for checking in with my little site. I also wanted to address a few questions I've gotten now that we've hit a certain threshold of traffic so I can direct people here in the future with questions. If I miss something hit me up on Mastodon here: https://c.im/@matdevdug

20k Average Weekly Visitors!

One of the milestones I never thought I'd hit was 10,000 average weekly visitors but we have blown past that without me noticing. Here are the last 30 days stats for those interested in such things. This obviously has a giant spike throwing the data off but if you look at a typical 30 day period we're just at 20,000 a week average.

I'm glad so many of you have found my little corner of the internet. It's been a pleasure to interact with (almost) all of you and for the vanishingly small percentage that have been unpleasant, we've had words.

Cost

Thanks to Cloudflare, running this site has not been expensive. We're still at about $6 a month to run. I'm running on the Hetzner ARM CAX11 instance class and have been really impressed with performance. Typically folks go with ARM-class instances for cost, but this thing has been a beast in terms of workload with zero ARM-specific issues I can point to. This mirrors my experience with AWS ARM instances, but in case you were considering doing the same thing, you can easily scale with even the cheapest instance.

Monetization

I've gotten a few offers to monetize the site, mostly either running ads (which for this audience would be a giant waste of time and ruin the visual of the site) or by running "promoted posts". After thinking it over I decided I don't want to earn any money on the site. It's fun to write, hopefully people enjoy reading it and I'm lucky enough to be at a point in my life where $10 a month is not a sum of money I miss.

If that ever changes in the future, I'll be sure to mark the posts as endorsed or paid for in some way so that nobody feels duped. But I haven't been interested so far.

Software

This is a Debian server that's initialized with cloud-init to set up a non-root user, install Docker and Docker compose, pull the Ghost images along with Nginx and then attach the volume. I also pull the Cloudflare certificate and insert that inside the Nginx container so I can have a long-running SSL certificate and let them handle the edge certificate rotation.

Previously I used Caddy in front of Ghost but did run into a few times when under load it seemed to struggle and required a restart. In general I had more problems than I expected with Caddy, which doesn't make it a bad webserver, but it is difficult to compete with the completely idiot-proof nature of Nginx. Plus since I'm not handling user-facing SSL certificates, the built-in SSL certificate functionality ended up not being much of a perk.

Ghost

As a platform Ghost has been pretty good with one exception, which is email newsletters. I'll touch on those later. It's held up well under load, with frequent updates. I don't use most of the admin panel which is really geared towards building an email newsletter business. I'm not interested in doing that, so a lot of the functionality is wasted on me.

However it is quite good at the basic stuff, which is you write text in their editor, hit publish and it goes out to the world. Most of the Dashboard stuff is pointless to me, with links to YouTubers and optimizing for SEO which I haven't done at all. Most of the new features they add has nothing to do with me and in retrospect I might have been better off with a static site.

In general though if you are interested in starting a blog with a focus on building a newsletter-based business, Ghost is great. For this it works well enough with some optimizations.

Email Newsletter

You may have noticed that the Subscribe button has disappeared. While I appreciate that people liked getting an email with the posts in it, the cost of sending emails exceeded the cost of hosting the rest of the platform by a lot. Ghost relies on Mailgun for sending newsletter and while it can use SMTP for transaction emails, the cost of Mailgun exceeds the value of what I get out of sending posts as newsletters. (If I post multiple times a month, we'd be looking at $90 a month for emails alone which is too rich for me). I also don't love having a database full of peoples names and email addresses in it, since the best way to prevent a data leak is to not have the data to begin with.

If anyone in the future complains I'll likely set this up: https://github.com/ItzNotABug/ghosler so I can use the much cheaper SMTP options. But so far the response to removing it has been small. For those reading this I would probably disable it from the get-go if started a new site OR set it up with SMTP from launch. Mailgun is too expensive for what it provides which was a pretty underwhelming user experience full of nested menus. (Insert my rant that transactional email API services are a scam business based on the false assertion that the reputation scores of senders are impossible to set up from scratch despite me having seen it done multiple times with IPv6 addresses).

However folks seem to be using RSS successfully, which is great. Some homegrown clients aren't intelligently checking whether there are new feeds or not, simply grabbing the entire RSS feed with every check. It's not a crisis by any means, but if that is you, maybe add a check for "is pubDate not todays date for the latest entry and if not, then maybe don't pull down the entire feed". But in general I strongly prefer RSS because the cost per user is extremely small and there are no personal data concerns around it. You are in control of your own feeds.

It does suck that less technical people still seem to struggle to find a functional RSS reader. It's still an unsolved problem as far as I can tell. I have many I like and recommend, but I constantly hear how hard it is to get "set up". If that's you, maybe check out Feedrabbit: https://feedrabbit.com/ to get RSS to emails.

Downsides

  • I don't love the Ghost team dumping sqlite for MySQL 8. Especially because there is no real scaling options here. So I'm not sure what the perk is for moving away from sqlite towards MySQL 8 if we're never going to be able to support multiple instances hosting the same site.
  • A lot of the technical work lately seems more in the direction of the headless CMS route, which is fine but does nothing for me.
  • Editor bugs. I get a lot of them. Markdown will stop working then resume working with a new update. Sometimes commands like /image will trigger the behavior I expect and sometimes it won't. The whole thing is a bit mysterious.

Nginx

Nothing to report here, just the official Nginx docker image with the following config. I have Authenticated Origin Pull set up so that I know all my traffic is coming from Cloudflare.

There's a couple of things happening here.

  • We have the proxy_cache setup so that Nginx can assist with any massive spikes in traffic.
  • This config attempts to both force SSL connections with the Strict-Transport-Security "max-age=63072000; includeSubdomains"; and also cache the SSL session parameters.
  • This is effectively my all-purpose Nginx configuration that I use for a lot of different things. There are a few sub-optimal things here (I don't think you need to do ssl_ciphers if you remove TLSv1) but in general this has been a pretty battle-tested config.
map $sent_http_content_type $expires {
    default                    off;
    text/css                   max;
    application/javascript     max;
    ~image/                    max;
}

server {
    listen 80;
    listen [::]:80;
    server_name matduggan.com www.matduggan.com;
    return 302 https://$server_name$request_uri;
}
proxy_cache_path /tmp/cache  levels=1:2    keys_zone=STATIC:10m inactive=24h  max_size=1g;
client_max_body_size 100M;

server {

    # SSL configuration

    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    charset UTF-8;
    gzip on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 5m;
    ssl_prefer_server_ciphers on;
    ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_buffer_size 4k;
    add_header Strict-Transport-Security "max-age=63072000; includeSubdomains";
    ssl_certificate         /etc/ssl/cert.pem;
    ssl_certificate_key     /etc/ssl/key.pem;
    ssl_client_certificate /etc/ssl/cloudflare.crt;
    ssl_verify_client on;
    expires $expires;


    server_name matduggan.com www.matduggan.com;

    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        add_header X-Cache-Status $upstream_cache_status;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
        proxy_buffering        on;
        proxy_cache            STATIC;
        proxy_cache_valid      200  1d;
        proxy_cache_use_stale  error timeout invalid_header updating
                               http_500 http_502 http_503 http_504;
        proxy_pass http://127.0.0.1:8080;
        proxy_redirect off;
    }

    location ~ ^/(ghost/|p/)/ {
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_pass http://127.0.0.1:8080;
    }

}

Considering Starting a Site?

You should! If you run across this and need some help, let me know at https://c.im/@matdevdug. Glad to provide pointers.

I think that addresses all the questions I typically get. If I missed something let me know.