How to install Ghost alongside CyberPanel on Ubuntu 20.04 without Docker

Close up of Terminal screen showing sudo command.
Elevating the world's consciousness. WeInstall.

If you have a Ubuntu server with CyberPanel installed, and want to install a Ghost blog on that same server, here are step by step directions on how to do that.

The latest version of Ghost (5.x) encourages (and soon requires) MySQL 8. If your CyberPanel server has MariaDB installed, you have a three options:

  1. Use remote database server with MySQL 8.
    Pro: Meets requirements.
    Con: Need to set up remote database.
  2. Configure SQLite if you don’t want to bother with MySQL 8.
    Pro: No need for remote database.
    Con: Data stored on file system. Not scalable.
  3. Install MySQL 8 on your CyberPanel in parallel to MariaDB.
    Pro: Meets requirements.
    Con: New database system just for Ghost.

I’ll be using a remote MySQL 8 database, simply because I already have a separate server with that set up, but I’ll include a note on how to set up SQLite if you just want to take Ghost for a spin.

Install Node, NPM, and Ghost CLI

As of this writing, Ghost recommends using Node 16.x to run Ghost, but it’s likely that the Node version that would automatically be installed on your server is lower (and perhaps even unsupported) compared the recommended version.

In order to see which version would be installed, SSH into your server and run:

apt-cache policy nodejs

If it displays something lower than Node 16.x, Ghost recommends adding the Node 16.x distribution from Node Source to your server:

curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -

Next, install Node (which includes NPM):

apt-get install nodejs

Last, install the Ghost CLI via NPM:

npm install --location=global ghost-cli

Create website in CyberPanel

Login to CyberPanel and create a website as you normally would. This is not where the Ghost blog will live, but we get a few things for free by using this process: a Linux user for the Ghost blog, a SSL certificate, and a virtual host file to configure.

Here is what I used, but replace any values as needed, such as domain name and email:

  • Select Package: Default
  • Select Owner: admin
  • Domain Name: blog.example.org
  • Email: [email protected]
  • Select PHP: PHP 8.0
  • Additional Features: Check SSL

Make sure you already pointed the DNS of the domain name to your server, otherwise CyberPanel won’t be able to set up a SSL cert from Let’s Encrypt, but rather fall back on configuring a self-signed certificate.

If it fails, don’t worry, you can always attempt it again later after the DNS has propagated. If you’re using something like Cloudflare in front of your site, Cloudflare’s SSL cert will be sufficient and connect to your site using the self-signed cert.

Prepare for Ghost installation

During the Ghost installation, the Ghost CLI checks whether it has read and write permission to the directory, and all of its parent directories, in which the Ghost blog will be installed.

But everything in the /home directory, where your sites normally live, is locked down to prevent websites from interfering with one another. This means Ghost CLI's check, and therefore the installation, will fail.

Because of this, we’re going create a new directory outside of the /home directory for the Ghost blog to live. I’m choosing /var/www out of convention, and will match the directory name to the one CyberPanel created, so that it remains clear to which CyberPanel website the Ghost blog belongs to:

mkdir /var/www/blog.example.org

Now list the directory contents of the CyberPanel site:

ls -al /home/blog.example.org

And look up the user CyberPanel created for the Ghost blog (e.g. bloge1234), which will be referred to as the Ghost user from here on out:

drwx--x--x  4 bloge1234 bloge1234 4096 Jul 26 02:17 .
drwx--x--x 21 root      root      4096 Jul 26 02:17 ..
drwxr-x---  2 root      nogroup   4096 Jul 26 02:17 logs
drwxr-x---  2 bloge1234 nogroup   4096 Jul 26 02:17 public_html

Let’s give the Ghost user access to read and write to the directory we just created:

chown bloge1234:bloge1234 /var/www/blog.example.org
chmod 775 /var/www/blog.example.org

Now set a password for the Ghost user:

passwd bloge1234

And add the Ghost user to the sudo group (required for Ghost to work):

adduser bloge1234 sudo

Next, switch over to the Ghost user:

su - bloge1234

And last, move into the directory where we’ll install the Ghost blog. Note that we’re moving into /var/www and not /home:

cd /var/www/blog.example.org

Install and configure Ghost

Install Ghost with the Ghost CLI:

ghost install --no-stack --no-setup-nginx --no-setup-ssl
  • –no-stack: Avoid checking if server meets requirements.
  • –no-setup-nginx: Don’t set up nginx; we’re using OpenLiteSpeed.
  • –no-setup-ssl: Don’t set up SSL; we’re using CyberPanel.

You can learn more about the Ghost CLI and its options on Ghost's website.

💡
If you already followed this guide before and want to install another Ghost blog, simply append --port 2369 up above (or a port other than the 2368 default) to run this new instance on a different port.

Ghost will complain that it can’t find MySQL, which is fine, because we’re using a remote MySQL 8 database. Press y and ENTER:

Local MySQL install was not found or is stopped. You can ignore this if you are using a remote MySQL host.
Alternatively you could:
a) install/start MySQL locally
b) run `ghost install --db=sqlite3` to use sqlite
c) run `ghost install local` to get a development install using sqlite3.

Now enter your blog URL and database credentials:

? Enter your blog URL: https://blog.example.org
? Enter your MySQL hostname: 0.0.0.0
? Enter your MySQL username: blog_example_org
? Enter your MySQL password: [hidden]
? Enter your Ghost database name: blog_example_org

Enter the password you set for the Ghost user earlier:

? Sudo Password [hidden]

To automatically start Ghost after a server reboot, choose Y to set up systemd:

? Do you wish to set up Systemd? Yes

And last, choose Y to start the Ghost blog now:

? Do you want to start Ghost? Yes

SQLite

If you want to install Ghost using SQLite, include the --db=sqlite3 with the other flags. You can start with SQLite and later edit config.production.json within /var/www/blog.example.org to configure MySQL 8.

Simply swap out the SQLite configuration:

"database": {
  "client": "sqlite3",
  "connection": {
    "filename": "/var/www/blog.sechu.us/content/data/ghost.db"
  }
},

With the MySQL 8 configuration:

"database": {
  "client": "mysql",
  "connection": {
    "host": "0.0.0.0",
    "port": 3306,
    "user": "blog_example_org",
    "password": "[hidden]",
    "database": "blog_example_org"
  }
},

And be sure to replace host, user, password, and database accordingly.

Configure virtual host for Ghost

CyberPanel configures websites with the intent of running a PHP-based website, which is not the case for our Ghost blog. In light of this, all of the PHP references can be removed.

Go to Websites > List Websites > blog.example.org > Manage and click on the vHost Conf button.

Here is what’s left after cleaning it up. Note that the adminEmails, keyFile, and certFile directives are unique to you, so don’t copy everything verbatim.

docRoot                   $VH_ROOT/public_html
vhDomain                  $VH_NAME
adminEmails               [email protected]
enableGzip                1
enableIpGeo               1

errorlog $VH_ROOT/logs/$VH_NAME.error_log {
  useServer               0
  logLevel                WARN
  rollingSize             10M
}

accesslog $VH_ROOT/logs/$VH_NAME.access_log {
  useServer               0
  logFormat               "%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-Agent}i""
  logHeaders              5
  rollingSize             10M
  keepDays                10
  compressArchive         1
}

extprocessor ghost {
  type                    proxy
  address                 localhost:2368
  maxConns                100
  pcKeepAliveTimeout      60
  initTimeout             60
  retryTimeout            0
  respBuffer              0
}

context /.well-known/acme-challenge {
  location                /usr/local/lsws/Example/html/.well-known/acme-challenge
  allowBrowse             1
}

context / {
  type                    proxy
  handler                 ghost
  addDefaultCharset       off
}

vhssl  {
  keyFile                 /etc/letsencrypt/live/blog.sechu.us/privkey.pem
  certFile                /etc/letsencrypt/live/blog.sechu.us/fullchain.pem
  certChain               1
  enableECDHE             1
  renegProtection         1
  sslSessionCache         1
  enableSpdy              15
  enableStapling          1
  ocspRespMaxAge          86400
}

module cache {
  storagePath /usr/local/lsws/cachedata/$VH_NAME
}

Additional Ghost blogs

If this isn’t your first Ghost installation on the server, there are three additional things you need to change in the vHost configuration above.

(1) Set a unique extprocessor name (previously ghost) so it’s different from your first installation. For example, add a keyword that defines your particular site: ghost-example.

(2) Update the port number (previously 2368) to whatever you chose during the installation with the --port flag. I used 2369 in my example.

extprocessor ghost-example {
  type                    proxy
  address                 localhost:2369
  maxConns                100
  pcKeepAliveTimeout      60
  initTimeout             60
  retryTimeout            0
  respBuffer              0
}

(3) Update the context handler (previously ghost) to match the extprocessor name from step 1. This would be ghost-example in our case.

context / {
  type                    proxy
  handler                 ghost-example
  addDefaultCharset       off
}

Visit Ghost blog

Your Ghost blog should now be up and running. Finish setting up Ghost by visiting the Ghost admin by appending /ghost to the URL: https://blog.example.org/ghost.

Troubleshooting

If you don’t see your Ghost blog, a great place to start is the Ghost error log, which can be found at:

/var/www/blog.example.org/content/logs/https___blog_example_org_production.error

Be sure to replace blog.example.org with the domain you chose in both the directory (blog.example.org) and the file name (blog_example_org).

That's a wrap. If you have any questions or comments, leave them below.

Featured image by Gabriel Heinzer.