Ghost is an open-source blogging platform that helps you create a professional-looking blog. It was launched in 2013 as an alternative to WordPress. It is written in JavaScript and is powered by the Node.js library.

In this tutorial, we will explore how to install Ghost CMS using Nginx and MySQL on a server powered by Debian 12. We will use the Let’s Encrypt SSL certificate to secure our installation.

Prerequisites

  • A server running Debian 12 with a minimum of 2GB of RAM.

  • A non-root user with sudo privileges.

  • A Fully Qualified Domain Name (FQDN) like example.com pointing to your server.

  • Make sure everything is updated.

    $ sudo apt update 
    $ sudo apt upgrade
    
  • Few packages that your system needs.

    $ sudo apt install wget curl nano ufw software-properties-common dirmngr apt-transport-https gnupg2 ca-certificates lsb-release debian-archive-keyring unzip -y
    

    Some of these packages may already be installed on your system.

Step 1 – Configure UFW Firewall

The first step is to configure the firewall. Debian comes with ufw (Uncomplicated Firewall) by default.

Check if the firewall is running.

$ sudo ufw status

You should get the following output.

Status: inactive

Allow SSH port so that the firewall doesn’t break the current connection on enabling it.

$ sudo ufw allow OpenSSH

Allow HTTP and HTTPS ports as well.

$ sudo ufw allow http
$ sudo ufw allow https

Enable the Firewall

$ sudo ufw enable
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup

Check the status of the firewall again.

$ sudo ufw status

You should see a similar output.

Status: active

To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW       Anywhere
80/tcp                     ALLOW       Anywhere
443                        ALLOW       Anywhere
OpenSSH (v6)               ALLOW       Anywhere (v6)
80/tcp (v6)                ALLOW       Anywhere (v6)
443 (v6)                   ALLOW       Anywhere (v6)

Step 2 – Install Nginx

Debian 12 ships with an older version of Nginx. To install the latest version, you need to download the official Nginx repository.

Import Nginx’s signing key.

$ curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor 
| sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null

Add the repository for Nginx’s stable version.

$ echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg arch=amd64] 
http://nginx.org/packages/debian `lsb_release -cs` nginx" 
| sudo tee /etc/apt/sources.list.d/nginx.list

Update the system repositories.

$ sudo apt update

Install Nginx.

$ sudo apt install nginx

Verify the installation. The sudo is required to run the command on Debian.

$ sudo nginx -v
nginx version: nginx/1.24.0

Start the Nginx server.

$ sudo systemctl start nginx

Step 3 – Install Node.js

Ghost Installer needs Nodejs to work. The first step is to import the Nodesource GPG key.

$ curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/nodesource.gpg

Next, create the Nodesource repository file. We will install Node 18x which is the current LTS (Long Term Support) version which is what Ghost recommends.

$ NODE_MAJOR=18
$ echo "deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list

Update the system repository list.

$ sudo apt update

Install Node.

$ sudo apt install nodejs -y

Confirm Node install.

$ node --version
v18.18.2

Step 4 – Install MySQL using Docker

Debian doesn’t ship with MySQL anymore. Instead, it ships with MariaDB. Ghost only supports MySQL. You can tweak Ghost to work with MariaDB but it is not recommended. Since MySQL’s official repositories haven’t been updated for Debian 12 at the time of writing this tutorial, we will install it using Docker.

Import the Docker GPG key.

$ curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker.gpg

Create a Docker repository file.

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

Update the system repository list.

$ sudo apt update

Install Docker and Docker Compose.

$ sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

By default, Docker requires root privileges. If you want to avoid using sudo every time you run the docker command, add your username to the docker group.

$ sudo usermod -aG docker $(whoami)

You will need to log out of the server and back in as the same user to enable this change or use the following command.

$ su - ${USER}

Confirm that your user is added to the Docker group.

$ groups
navjot wheel docker

Now that the Docker is installed, we need to create a Docker compose file for MySQL. Create a directory for MySQL docker.

$ mkdir ~/mysql

Create and open the docker-compose.yml file for editing.

$ nano docker-compose.yml

Paste the following code in it.

services:
  database:
    image: container-registry.oracle.com/mysql/community-server:latest
    container_name: mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword
      MYSQL_USER: ghost
      MYSQL_PASSWORD: ghostpassword
      MYSQL_DATABASE: ghostdb
    ports:
      - "3306:3306"
    volumes:
      - ./mysql:/var/lib/mysql

Save the file by pressing Ctrl X and entering Y when prompted.

Here we have set the root password and the MySQL credentials for the Ghost database. These will be created when the container is run.

Start the MySQL container.

$ docker compose up -d

Check the status of the Docker container.

$ docker ps
CONTAINER ID   IMAGE                                                         COMMAND                  CREATED         STATUS         PORTS                                                        NAMES
ec42fb205f1e   container-registry.oracle.com/mysql/community-server:latest   "https://www.howtoforge.com/entrypoint.sh mysq…"   4 seconds ago   Up 2 seconds   0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060-33061/tcp   mysql

Ghost can connect to the MySQL container using port 3306 and perform operations on it.

Step 5 – Install Ghost

We can install Ghost using Docker as well which can simplify things but we won’t be doing it here.

The Ghost installation will comprise three components – Ghost-CLI command line tool that installs and manages updates to the Ghost blog and the blog package itself.

Install Ghost-CLI

Run the following command to install the Ghost-CLI tool.

$ sudo npm install ghost-cli@latest -g

Prepare Ghost Directory

Create the Ghost root directory.

$ sudo mkdir -p /var/www/html/ghost

Set the ownership of the directory to the current user.

$ sudo chown $USER:$USER /var/www/html/ghost

Set the correct directory permissions.

$ sudo chmod 755 /var/www/html/ghost

Switch to the Ghost directory.

$ cd /var/www/html/ghost

Install Ghost

Installing Ghost is a single command process.

$ ghost install

During the installation, the CLI tool will ask several questions to configure the blog.

  • Blog URL: Enter your complete blog URL along with the https protocol. (https://example.com)
  • MySQL Hostname: Press Enter to use the default value of localhost since our Ghost install and MySQL are on the same server.
  • MySQL Username: Enter ghost as your MySQL username.
  • MySQL Password: Enter your root password created before in the docker file.
  • Ghost database name: Enter the name of the database (ghostdb) configured in the docker file.
  • Sudo password: It will ask for your sudo password to perform administrative tasks.
  • Set up Nginx? Usually, Ghost-CLI detects your Nginx installation and automatically configures it for your blog. But that works only for Nginx installed using the OS package. Since we installed it using Nginx’s repository, Ghost can’t detect it and will skip it automatically.
  • Set up SSL?: Since it skipped over the Nginx configuration, the CLI tool will also skip setting up an SSL as well.
  • Set up systemd?: Ghost will ask if you want to set up a system service for Ghost. Press Y to proceed.
  • Start Ghost?: Press Y to start your Ghost installation. However, it won’t work because Nginx and SSL aren’t configured yet.

Step 6 – Install SSL

Before we proceed, we need to install the Certbot tool and install an SSL certificate for our domain.

To install Certbot, we will use the Snapd package installer. Snapd always carries the latest stable version of Certbot. Debian doesn’t come with Snapd installed though. Install it first.

$ sudo apt install snapd

Ensure that your version of snapd is up to date.

$ sudo snap install core
$ sudo snap refresh core

Install Certbot.

$ sudo snap install --classic certbot

Use the following command to ensure that the Certbot command can be run by creating a symbolic link to the /usr/bin directory.

$ sudo ln -s /snap/bin/certbot /usr/bin/certbot

Verify the installation.

$ certbot --version
certbot 2.7.1

Generate an SSL certificate.

$ sudo certbot certonly --nginx --agree-tos --no-eff-email --staple-ocsp --preferred-challenges http -m [email protected] -d example.com

The above command will download a certificate to the /etc/letsencrypt/live/example.com directory on your server.

Generate a Diffie-Hellman group certificate.

$ sudo openssl dhparam -dsaparam -out /etc/ssl/certs/dhparam.pem 4096

Check the Certbot renewal scheduler service.

$ sudo systemctl list-timers

You will find snap.certbot.renew.service as one of the services scheduled to run.

NEXT                        LEFT          LAST                        PASSED       UNIT                       ACTIVATES
Tue 2023-10-17 00:00:00 UTC 14h left    Mon 2023-10-16 00:00:18 UTC 9h ago       dpkg-db-backup.timer         dpkg-db-backup.service
Mon 2023-10-16 19:12:00 UTC 9h left     Mon 2023-10-16 07:27:11 UTC 2h 17min ago snap.certbot.renew.timer     snap.certbot.renew.service
Mon 2023-10-16 20:49:14 UTC 11h left    Mon 2023-10-16 07:48:12 UTC 1h 56min ago apt-daily.timer              apt-daily.service

Do a dry run of the process to check whether the SSL renewal is working fine.

$ sudo certbot renew --dry-run

If you see no errors, you are all set. Your certificate will renew automatically.

Step 7 – Configure Nginx

Create and open the file /etc/nginx/conf.d/ghost.conf for editing.

$ sudo nano /etc/nginx/conf.d/ghost.conf

Paste the following code in the ghost.conf file. Replace all instances of example.com with your domain.

server {
  listen 80;
  listen [::]:80;
  server_name example.com;
  location / { 
  	return 301 https://$server_name$request_uri; 
  }
}

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  server_name example.com;
    
  access_log /var/log/nginx/ghost.access.log;
  error_log /var/log/nginx/ghost.error.log;
  client_max_body_size 20m;

  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
  ssl_prefer_server_ciphers off;
  ssl_session_timeout 1d;
  ssl_session_cache shared:SSL:10m;
  ssl_dhparam /etc/ssl/certs/dhparam.pem;
  ssl_stapling on;
  ssl_stapling_verify on;
  resolver 1.1.1.1 1.0.0.1 [2606:4700:4700::1111] [2606:4700:4700::1001] 8.8.8.8 8.8.4.4 [2001:4860:4860::8888] [2001:4860:4860::8844] valid=60s;
  resolver_timeout 2s;

  ssl_certificate         /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key     /etc/letsencrypt/live/example.com/privkey.pem;
  ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;

  location / {
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_pass http://localhost:2368;
  }
}

The above configuration will redirect all HTTP requests to HTTPS and will serve as a proxy for Ghost service to serve it via your domain.

Save the file by pressing Ctrl X and entering Y when prompted.

Open the file /etc/nginx/nginx.conf for editing.

$ sudo nano /etc/nginx/nginx.conf

Add the following line before the line include /etc/nginx/conf.d/*.conf;.

server_names_hash_bucket_size  64;

Save the file by pressing Ctrl X and entering Y when prompted.

Verify your Nginx configuration.

$ sudo nginx -t

If you see no errors, it means you are good to go. Restart the Nginx server to apply the configuration.

$ sudo systemctl restart nginx

Step 9 – Run the Site

Now, you can verify your installation by opening https://example.com in your web browser. You will get the following page indicating a successful installation.

<img alt="Ghost Homepage" data-ezsrc="https://kirelos.com/wp-content/uploads/2024/06/echo/ghost_homepage.png667abd56c5f71.jpg" ezimgfmt="rs rscb10 src ng ngcb9" height="575" loading="lazy" referrerpolicy="no-referrer" src="data:image/svg xml,” width=”750″>

Step 10 – Complete Setup

To finish setting up your Ghost blog, visit https://example.com/ghost in your browser. The extra /ghost at the end of your blog’s domain redirects you to Ghost’s Admin Panel or in this case the setup since you are accessing it for the first time.

Here, you will be required to create your Administrator account and choose a blog title.

<img alt="Ghost Setup Details" data-ezsrc="https://kirelos.com/wp-content/uploads/2024/06/echo/ghost-setup-details.png667abd57189b6.jpg" ezimgfmt="rs rscb10 src ng ngcb9" height="750" loading="lazy" referrerpolicy="no-referrer" src="data:image/svg xml,” width=”639″>

Enter your details and click the Create account & start publishing button to proceed.

Next, you will be taken to the following screen where you are given options such as writing your first post, customizing your site, and importing members.

<img alt="Ghost Installer Suggestions" data-ezsrc="https://kirelos.com/wp-content/uploads/2024/06/echo/ghost_installer-suggestions.png667abd5787566.jpg" ezimgfmt="rs rscb10 src ng ngcb9" height="566" loading="lazy" referrerpolicy="no-referrer" src="data:image/svg xml,” width=”750″>

We will choose the Explore Ghost admin to explore and go to the dashboard directly. At the end of the setup, you will be greeted with the Ghost’s Administration panel.

<img alt="Ghost Admin Dashboard" data-ezsrc="https://kirelos.com/wp-content/uploads/2024/06/echo/ghost-admin-dashboard.png667abd57cde9b.jpg" ezimgfmt="rs rscb10 src ng ngcb9" height="513" loading="lazy" referrerpolicy="no-referrer" src="data:image/svg xml,” width=”750″>

If you want to switch to dark mode, you can do so by clicking on the toggle switch next to the settings gear button at the bottom of the settings page.

<img alt="Ghost Dark Mode Toggle" data-ezsrc="https://kirelos.com/wp-content/uploads/2024/06/echo/ghost-dark-mode-toggle.png667abd581c0e6.jpg" ezimgfmt="rs rscb10 src ng ngcb9" height="73" loading="lazy" referrerpolicy="no-referrer" src="data:image/svg xml,” width=”177″>

You will see a default post. You can unpublish or delete it and start posting.

<img alt="Ghost Posts Panel" data-ezsrc="https://kirelos.com/wp-content/uploads/2024/06/echo/ghost-posts-panel.png667abd585b34d.jpg" ezimgfmt="rs rscb10 src ng ngcb9" height="449" loading="lazy" referrerpolicy="no-referrer" src="data:image/svg xml,” width=”750″>

Step 11 – Configure Mailer

Ghost not only acts as a blogging platform but also as a newsletter manager. For day-to-day operations, you can use any transactional mail service to work with Ghost for sending mail. But if you want to send newsletters via Ghost, the only official bulk mailer supported is Mailgun. You can use a different newsletter service too but for that, you will need to use the Zapier integration feature of Ghost.

Let us first configure an SMTP service for transactional emails. For this open the file /var/www/html/ghost/config.production.json file for editing.

$ nano /var/www/html/ghost/config.production.json

Find the following lines.

 "mail": {
    "transport": "Direct"
  },

Replace them with the following code.

"mail": {
    "from": "'HowtoForge Support' [email protected]",
    "transport": "SMTP",
    "options": {
        "host": "YOUR-SES-SERVER-NAME",
        "port": 465,
        "service": "SES",
        "auth": {
            "user": "YOUR-SES-SMTP-ACCESS-KEY-ID",
            "pass": "YOUR-SES-SMTP-SECRET-ACCESS-KEY"
        }
    }
},

Here we are using the Amazon SES Mail service since it is affordable and doesn’t require monthly fees.

Save the file by pressing Ctrl X and entering Y when prompted. Once finished, restart the Ghost application for the changes to take effect.

$ ghost restart

To configure the newsletter settings, visit the Settings >> Email newsletter section.

<img alt="Ghost Email newsletter Settings" data-ezsrc="https://kirelos.com/wp-content/uploads/2024/06/echo/ghost-email-newsletter-settings.png667abd58a5d16.jpg" ezimgfmt="rs rscb10 src ng ngcb9" height="653" loading="lazy" referrerpolicy="no-referrer" src="data:image/svg xml,” width=”750″>

Click on the Mailgun configuration link to expand.

Fill in your Mailgun Region, domain, and API key.

<img alt="Ghost MailGun newsletter Settings" data-ezsrc="https://kirelos.com/wp-content/uploads/2024/06/echo/ghost-mailgun-newsletter-settings.png667abd58e6463.jpg" ezimgfmt="rs rscb10 src ng ngcb9" height="331" loading="lazy" referrerpolicy="no-referrer" src="data:image/svg xml,” width=”711″>

Click the Save button on the top right to save the settings.

To test the newsletter delivery, create a new test post, hit publish, and select the Email only option. If you want to publish the post as well, select the Publish and email option.

<img alt="Ghost Post Publish/Email Option" data-ezsrc="https://kirelos.com/wp-content/uploads/2024/06/echo/ghost-publish-mail-option.png667abd592c34b.jpg" ezimgfmt="rs rscb10 src ng ngcb9" height="572" loading="lazy" referrerpolicy="no-referrer" src="data:image/svg xml,” width=”726″>

Click the Continue, final review button to proceed. The next page will ask again for final confirmation.

<img alt="Ghost Send Email Newsletter Confirm" data-ezsrc="https://kirelos.com/wp-content/uploads/2024/06/echo/ghost-send-newsletter-confirm.png667abd59b0ae8.jpg" ezimgfmt="rs rscb10 src ng ngcb9" height="367" loading="lazy" referrerpolicy="no-referrer" src="data:image/svg xml,” width=”701″>

Click the Send email, right now button to send the newsletter. You will get the following message once the mail is sent.

<img alt="Ghost Newsletteter Successful Delivery message" data-ezsrc="https://kirelos.com/wp-content/uploads/2024/06/echo/ghost-newsletter-successful-message.png667abd59df46a.jpg" ezimgfmt="rs rscb10 src ng ngcb9" height="211" loading="lazy" referrerpolicy="no-referrer" src="data:image/svg xml,” width=”598″>

Check your email for the post.

<img alt="Ghost Newsletter Email" data-ezsrc="https://kirelos.com/wp-content/uploads/2024/06/echo/ghost-newsletter-email.png667abd5a33bba.jpg" ezimgfmt="rs rscb10 src ng ngcb9" height="573" loading="lazy" referrerpolicy="no-referrer" src="data:image/svg xml,” width=”750″>

Step 12 – Update Ghost

There are two types of Ghost updates – Minor updates and Major updates.

First, take a full backup if you want to run a minor update. It creates a backup of all the posts, members, themes, images, files, and redirect files.

$ cd /var/www/html/ghost
$ ghost backup

Run the update command to perform the minor update.

$ ghost update

To perform a major update, you should follow the official detailed update guide over at Ghost. Depending upon which version you are at currently and the major version you want to update to, steps will vary.

Conclusion

This concludes our tutorial on how to set up Ghost CMS on your Debian 12 server using Nginx. If you have any questions or any feedback, share them in the comments below.