Hard fork

I'll start out by stating the obvious: I am not a web developer. Hell, I don't even play one on T.V.! At one point in my career, though, I could have been had I decided to embrace the world of front-end / UI Engineer, but my career took me down another path (User Experience, Customer Research, and Product Design).

I wrote a rather large post about rebooting my digital footprint in January of 2022. I had ditched WordPress for Ghost, which I chose to self-host at Digital Ocean on a 1-click droplet. It was easy enough, right? Push a button, pick a server, and let it rip!

But, me being me, I wanted to find something more complicated. I got curious about spinning up my own Ubuntu VPS with a cloud provider - mostly to see if I could get max performance out of a like $2/mo box - but also to see if I had the technical and analytical skills to actually do it.

You're reading this post on my new CloudFanatic (affiliate link), $6.99/mo VPS based out of Chicago, IL - just 300 miles north of St. Louis. I've got 4GB RAM, 2vCPU cores, and a 50GB SSD to play with for all the things I want to run.

Initial Setup

Remember how I'm not a web developer? I do have some familiarity with command line and I'm getting increasingly comfortable with the idea of moving around Linux without a GUI. This old designer is learning new tricks!

User Management

The first thing I had to do was ssh in to the VPS as the root user:

ssh root@IP Address

Now that I'm in the box, I had to create a new user and give it appropriate permissions:

# adduser ghost-mgr
# usermod -aG sudo ghost-mgr

Next, I needed to log in as that new user:

# su - ghost-mgr

Upgrades galore

Now that I'm logged in as ghost-mgr, it's time to upgrade my install of Ubuntu 22.04:

$ sudo apt update && sudo apt upgrade -y

Installing nginx

Next, I needed to install nginx:

$ sudo apt install nginx

Just for good measure, I wanted to verify that everything was ok with nginx by running the following command:

$ sudo systemctl status nginx

The output:

nginx.service - A high performance web server and a reverse proxy server
     Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
     Active: active (running)
       Docs: man:nginx(8)
    Process: 66019 ExecStartPre=/usr/sbin/nginx -t -q -g daemon on; master_process on; (code=exited, status=0/SUCCESS)
    Process: 66020 ExecStart=/usr/sbin/nginx -g daemon on; master_process on; (code=exited, status=0/SUCCESS)
   Main PID: 66112 (nginx)
      Tasks: 2 (limit: 2196)
     Memory: 2.6M
        CPU: 148ms
     CGroup: /system.slice/nginx.service
             ├─66112 "nginx: master process /usr/sbin/nginx -g daemon on; master_process on;"
             └─66113 "nginx: worker process"

Looks good enough to me (but again, I am not a developer).

MySQL Install

Now's where things got a little complicated for me. It was time to install MySQL. I am - again - barely comfortable with node.js and certainly not as comfortable with SQL as I'd like to admit, but I'm following the guide that was published by Ghost on how to self-install, so I went in with gusto. Worst-case scenario, it doesn't work and I have to go back and try again... and again... and again.

(Deep breath)

$ sudo apt install mysql-server

Success. Now, I needed to start it up and make sure that if I rebooted the VPS, it'd also automatically start (enable, in the parlance of MySQL):

$ sudo systemctl start mysql
$ sudo systemctl enable mysql

Just for shits 'n giggles, I wanted to test to see if it was working by running the following command:

$ sudo systemctl status mysql

And the output?

mysql.service - MySQL Community Server
     Loaded: loaded (/lib/systemd/system/mysql.service; enabled; vendor preset: enabled)
     Active: active (running)
   Main PID: 1083 (mysqld)
     Status: "Server is operational"
      Tasks: 41 (limit: 2797)
     Memory: 434.0M
        CPU: 1min 57.932s
     CGroup: /system.slice/mysql.service
             └─1083 /usr/sbin/mysqld

Again, looks good to me. It actually output something!

A lot of web searching tells me that MySQL is actually not hardened or secure out of the box. So, I ran the following command:

$ sudo mysql_secure_installation

And configured as follows:

- Set root password? [Y/n] Y
- Remove anonymous users? [Y/n] Y
- Disallow root login remotely? [Y/n] Y
- Remove test database and access to it? [Y/n] Y
- Reload privilege tables now? [Y/n] Y

At this point, I was just hoping that I didn't mess it up. It was now time to create the MySQL database, add a user, and give that user permissions:

$ sudo mysql -u root -p

Success, baby! I'm now seeing a mysql> prompt instead of my usual $ prompt. I am truly hacking the planet. Time to get after it:

mysql> CREATE DATABASE ghostdb;
mysql> CREATE USER 'ghostuser'@'localhost' IDENTIFIED BY 'PASSWORD_HERE';
mysql> GRANT ALL PRIVILEGES ON ghostdb. * TO 'ghostuser'@'localhost';
mysql> exit;

Time to remember some things, because the MySQL database that makes my Ghost install run is not the same as the root user, with a different password, or my ghost-mgr user with yet another password. It's really easy to get lost in the sauce, so write some of this stuff down so you don't regret it!

Database User Password
ghostdb ghostuser PASSWORD_HERE


Now that we've exited the database set-up portion of the install, we need to toss Node.js on the VPS. We can do that pretty easily by banging out the following:

$ sudo curl -sL https://deb.nodesource.com/setup_18.x | sudo -E bash

You'll get some errors that whine about "oh, no! this is outdated, blah blah blah" - just blow through those. Keep on truckin' - we're nearly there.

After your screen looks like it's glitching in The Matrix, you'll need to actually install Node.js:

$ sudo apt-get install -y nodejs

And, much like we've done before, you can verify the installed version with a simple command:

node --version

With an output of:


Now, we need to install Node Package Manager (npm), which is the engine that drives a lot of our Ghost install:

$ sudo npm install npm@latest -g

And, again, to verify:

npm --version
npm: '8.19.4'

Installing Ghost

It's finally time to do the thing. You've made it this far...

Now we need to install the Ghost command-line interface (CLI) tool. This will allow us to actually install Ghost. Can you feel the excitement? I sure could:

$ sudo npm install -g ghost-cli@latest

And, once again, we can check to see what version we have:

$ ghost -v
Ghost-CLI version: 1.25.3

Smells like success.

Now we get to finally - finally! - install Ghost and see all of this hard work pay off.

Installing Ghost

First, we make a new directory, grant our ghost-mgr user permission to the directory, set up permissions, and move inside the directory we created:

$ sudo mkdir /var/www/ghost
$ sudo chown ghost-mgr:ghost-mgr /var/www/ghost
$ sudo chmod 775 /var/www/ghost
cd /var/www/ghost/

Drumroll, please. It's now time to press the button and do the thing!

$ ghost install

When done successfully, you'll see the following output:

✔ Checking system Node.js version - found v16.18.1
✔ Checking logged in user
✔ Checking current folder permissions
✔ Checking system compatibility
✔ Checking for a MySQL installation
✔ Checking memory availability
✔ Checking free space
✔ Checking for latest Ghost version
✔ Setting up install directory
✔ Downloading and installing Ghost v5.24.0
✔ Finishing install process

And then, you'll get to actually configure the install!

? Enter your blog URL: http://yourdomain.com
? Enter your MySQL hostname: localhost
? Enter your MySQL username: ghostuser
? Enter your MySQL password: [hidden]
? Enter your Ghost database name: ghostdb
✔ Configuring Ghost
✔ Setting up instance
+ sudo useradd --system --user-group ghost
? Sudo Password [hidden]
+ sudo chown -R ghost:ghost /var/www/ghost/content
✔ Setting up "ghost" system user
ℹ Setting up "ghost" mysql user [skipped]
? Do you wish to set up Nginx? Yes
+ sudo nginx -s reload
✔ Setting up Nginx

You'll also get to do a few other things with the install – mainly, set up your free Let's Encrypt SSL certificate for https all over the place, and set up systemd to keep ghost running smoothly. I promise you it's worth it.

And once you've got all that done, you can finally - at long last! - log-in via the web in the URL provided.

Pour yourself a drink, partner. You've earned it!

You've successfully subscribed to Stephen Bolen
Great! Next, complete checkout to get full access to all premium content.
Error! Could not sign up. invalid link.
Welcome back! You've successfully signed in.
Error! Could not sign in. Please try again.
Success! Your account is fully activated, you now have access to all content.
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.