How to deploy Ghost using Ansible, Terraform, and Ghost CLI

Install Ghost blog instance using Terraform, Ansible, and Ghost CLI.

How to deploy Ghost using Ansible, Terraform, and Ghost CLI

I maintain a repo for deployment files of this blog. It already uses Ansible, and Terraform to deploy this instance of Ghost. The only issue is that it uses the unofficial Docker image of Ghost to deploy the instance. I wanted to updated my deployment to use the Ghost recommended infrastructure. This meant, for one, an Ubuntu server with NodeJS 18, and using the Ghost CLI to deploy the actual Ghost instance. I wanted to go one step further and deploy a separate VM for the MySQL database so I can maintain the Ghost instance independently of the database.

Terraform spec updates

The update to the spec file are pretty predictable. I simply added two different servers with lower CPU and RAM configurations. One gigabyte of RAM and one CPU should be enough for most instance of Ghost. Especially with caching implemented.

resource "digitalocean_droplet" "ghost_server_4" {
  image             = "ubuntu-24-04-x64"
  name              = "ghost-server-4"
  region            = "tor1"
  size              = "s-1vcpu-1gb"
  monitoring        = true
  ssh_keys          = [var.ssh_key_id]
  tags              = ["blog", "ghost"]
  droplet_agent     = true
  graceful_shutdown = true
  backups           = false
}

resource "digitalocean_droplet" "ghost_db_1" {
  image             = "ubuntu-24-04-x64"
  name              = "ghost-db-1"
  region            = "tor1"
  size              = "s-1vcpu-1gb"
  monitoring        = true
  ssh_keys          = [var.ssh_key_id]
  tags              = ["blog", "ghost", "db"]
  droplet_agent     = true
  graceful_shutdown = true
  backups           = false
}

resource "digitalocean_monitor_alert" "cpu_alert" {
  alerts {
    email = [var.alert_email]
  }
  window  = "5m"
  type    = "v1/insights/droplet/cpu"
  compare = "GreaterThan"
  value   = 70
  enabled = true
  entities = [
    digitalocean_droplet.ghost_server_4.id,
    digitalocean_droplet.ghost_db_1.id
  ]
  description = "Alert about CPU usage"
}

infra.tf

DigitalOcean | Cloud Infrastructure for Developers
An ocean of simple, scalable cloud solutions.

Playbook update

The Ansible playbook contains the bulk of the changes for this update. The first significant change was to remove the Docker dependency and all tasks that used Docker to deploy Ghost. While this method of deployment was working fine, it did take more resources than necessary and is not officially supported by Ghost. I then added the MySQL and NodeJS Ansible roles from Geerlingguy. The MySQL role exposes the variables needed to setup the database for Ghost, and NodeJS role is used to install the Ghost supported version of NodeJS.

---
- name: "DB play"
  hosts: dbservers
  vars:
    mysql_root_password: "{{ lookup('env', 'GHOST_DB_PASSWORD') }}"
    mysql_databases:
      - name: ghost
    mysql_users:
      - name: "{{ lookup('env', 'GHOST_DB_USER') }}"
        host: "%"
        password: "{{ lookup('env', 'GHOST_DB_PASSWORD') }}"
        priv: "ghost.*:ALL"
  roles:
    - role: geerlingguy.mysql
      become: true


- name: "Ghost play"
  hosts: webservers
  vars:
    nodejs_version: "18.x"
    nodejs_npm_global_packages:
      - name: ghost-cli
  roles:
    - role: geerlingguy.nginx
    - role: geerlingguy.nodejs
    - role: ghost

playbook.yaml

The main updates were made in my custom Ghost role tasks. Instead of simply copying the compose file to the server and running the docker-compose command, there is some setup required to run the Ghost CLI. After creating the custom ghost user, I had to create a custom sudoers file to allow the custom ghost user sudo privileges without a password. This, passwordless sudo, functionality is required to complete the ghost installation without human intervention.

---

- name: Add the user 'ghostuser' with a specific uid and a primary group of 'sudo'
  ansible.builtin.user:
    name: ghostuser
    group: sudo

- name: Copy a new sudoers file into place, after passing validation with visudo
  ansible.builtin.template:
    src: 100-ghost-user
    dest: /etc/sudoers.d/100-ghost-user
    mode: '440'

- name: Create a directory if it does not exist
  ansible.builtin.file:
    path: /var/www/hackandslash
    owner: ghostuser
    group: root
    state: directory
    mode: '755'

- name: Install Ghost
  become: true
  become_user: ghostuser
  ansible.builtin.shell: |
    /usr/local/lib/npm/bin/ghost \
      install 5.87.1 \
      --no-prompt \
      --dir /var/www/hackandslash \
      --url https://hackandslash.blog \
      --port 2368 \
      --sslemail "{{ lookup('env', 'ALERT_EMAIL') }}" \
      --db mysql \
      --dbhost 10.137.0.3 \
      --dbuser "{{ lookup('env', 'GHOST_DB_USER') }}" \
      --dbpass "{{ lookup('env', 'GHOST_DB_PASSWORD') }}" \
      --dbname ghost \
      --mail SMTP \
      --mailservice Mailgun \
      --mailuser "{{ lookup('env', 'GHOST_MAIL_USER') }}" \
      --mailpass "{{ lookup('env', 'GHOST_MAIL_PASSWORD') }}" \
      --mailhost smtp.mailgun.org \
      --mailport 465 \
      --process systemd
  args:
    executable: /bin/bash

tasks/main.yaml

# User rules for ghostuser
ghostuser ALL=(ALL) NOPASSWD:ALL

templates/100-ghost-user

After running the Terraform plan, and the playbook, everything should install without human interaction. You can find the full source code in the repo below.

GitHub - mbbaig/ghost-deployment: A Terraform and Ansible project to deploy the Ghost platform
A Terraform and Ansible project to deploy the Ghost platform - mbbaig/ghost-deployment

Future changes

I will be updating the repo with a couple of new features soon:

  • Ability to update installed instance of Ghost
  • Firewall rules to block public access to the DB server