Our Ghost CloudFormation Template

We no longer maintain our AMI, but we will leave this here in case you want to run with your own, or set one up yourself

Running Ghost on Amazon's Web Services (AWS) has been a very popular option since the beginning. With the option of running a Ghost blog free for a year, on a full VPS is very attractive to many people. We created the Ghost AMI back when Ghost first lauched and have been updating and maintaining the AMI consistently since. The process and tools we use are likely a blackhole for anybody that has tried to look. We will document our process here for our own benifit but also for the curious, attempting to be more open source, and for those that are worried our AMI may not be secure.

The tools we use:

  • AWS
    • EC2
    • CloudFormation
    • Bash scripts

First, a little background. Most poeple are familiar with EC2 but less are familiar with CloudFormation. CloudFormation is a tool that AWS provides for free to systemically create AWS resouces. For example, you can use CloudFormation to create any number of EC2 instances, create security groups that get assigned to those EC2 instances that allow traffic on port 80 and 443, and stick all of those EC2 instances behind a load balancer, repeatedly, over and over again. After using CloudFormation once, you quickly see the value in reproducibilty which is why we have picked it for creating our Ghost AMI.

So, our process starts everytime with our CloudFormation template. In the following section we will describe in depth our template.

CloudFormation Template

We host our CloudFormation template for all to see on GitHub and you can find it here.

If you have comments or concers about what you see in the template please let us know by contacting us (details below) or creating a GitHub issue.


We currently have two parameters for our CloudFormation template, the EC2 instance type and what AMI to base our AMI on. We almost always use a t2.small for the instance type for no other reason than it works wonderfully and is extremely cheap. For our base AMI we always choose the current AMI ID (list of AMI IDs) of Amazon's Linux AMI in the us-east-1 region.


The UserData section gets turned into a bash script that AWS runs on the EC2 instance on first boot:

"UserData"       : { "Fn::Base64" : { "Fn::Join" : ["", [
        "#!/bin/bash -ex","\n",

		"# Start software updates","\n",
		"/usr/bin/yum update -y","\n",

		"# Install and setup Nginx","\n",
		"/usr/bin/yum install nginx -y","\n",
		"service nginx start","\n",
		"chkconfig nginx on","\n",
		"echo 'server { listen 80; location / { proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $http_host; proxy_pass; } }' > /etc/nginx/conf.d/virtual.conf","\n",
		"service nginx restart","\n",

		"# Installing Node and NPM","\n",
	    "curl -sL https://rpm.nodesource.com/setup | bash -","\n",
        "yum install -y nodejs","\n",
        "yum groupinstall -y 'Development Tools'","\n",
		"curl -L https://npmjs.org/install.sh | sh","\n",

		"# Download and install Ghost","\n",
        "mkdir -p /var/www/","\n",
        "cd /var/www/","\n",
        "curl -L -O https://ghost.org/zip/ghost-latest.zip","\n",
        "/usr/bin/unzip -d ghost ghost-*","\n",
        "cd ghost","\n",

		"# Setup config.js for Ghost","\n",
        "cp /var/www/ghost/config.example.js /var/www/ghost/config.js","\n",
        "sed -i -e 's/mail: {},/mail: {\\\n\t    transport: \\x27SMTP\\x27,\\\n\t    options: {\\\n\t        service: \\x27sendmail\\x27,\\\n\t    }\\\n\t},/' /var/www/ghost/config.js","\n",
		"# One of the processes while compiling sqlite requires the HOME variable to be set","\n",
		"export HOME=/root","\n",
		"/usr/bin/npm install --production","\n",
		"# now that all ghost modules are intalled set owner","\n",
		"useradd ghost","\n",
		"chown -R ghost:ghost /var/www/ghost/","\n",

		"# Install pm2","\n",
        "/usr/bin/npm install -g pm2","\n",
        "export NODE_ENV=production","\n",
        "su -c \"cd /var/www/ghost/; /usr/bin/pm2 start index.js --name ghost\" -s /bin/bash ghost","\n",
		"su -c \"/usr/bin/pm2 dump\" -s /bin/bash ghost","\n",
        "/usr/bin/pm2 startup centos -u ghost","\n",

		"# Download Ghost init script from GitHub and setup a cron to run it on first boot","\n",
		"wget -O - https://raw.githubusercontent.com/andyboutte/cloudformation-templates/master/ghost_init.sh >> /home/ec2-user/ghost_init.sh","\n",
		"chmod +x /home/ec2-user/ghost_init.sh","\n",
		"echo \"@reboot root /home/ec2-user/ghost_init.sh >> /var/log/ghost_init.log\" >> /etc/cron.d/ghost_init","\n",

		"# Clean up","\n",
		"rm /var/www/*.zip","\n",

        "curl -X PUT -H 'Content-Type:' --data-binary '{\"Status\" : \"SUCCESS\",",
                                                       "\"Reason\" : \"The application myapp is ready\",",
                                                       "\"UniqueId\" : \"myapp\",",
                                                       "\"Data\" : \"Done\"}' ",
         "\"", {"Ref" : "WaitForInstanceWaitHandle"},"\"\n" ]]}}

The UserData section is where most of the magic happens. In a nutshell, this section is a series of Bash commands that get run on the EC2 instance after it starts up. So in this section we:

  • Run software updates
  • Install nginx
  • Install Node.js
  • Install npm
  • Install the yum Development Tools group
  • Install Ghost
  • Modify the Ghost config.js file
  • Add the ghost user
  • Install pm2
  • Start Ghost from the ghost user with pm2
  • Set pm2 to start Ghost on startup
  • Download and set our ghost_init.sh script to run at startup

All of the above steps are probably somewhat familiar except our ghost_init.sh script which we will talk about in the next section.


The Ghost team has started to pick up speed and are starting to come out with releases every couple of weeks. This was starting to be a challange for us to keep up and get our new AMIs through the AWS submission process. To solve this, we created a process to update Ghost on startup.

Our ghost_init.sh process can been found here on GitHub.

During the UserData section we curl this file from GitHub to /home/ec2-user/ghost_init.sh and then create a cron in /etc/cron.d/ghost_init. This cron is set to @reboot so it will only run the next time the instance starts up.

The next time the instance starts up (ie when anybody finds our AMI in the AWS Marketplace) the /home/ec2-user/ghost_init.sh script is executed and performs the following tasks:

  • Determine the version of Ghost currently installed on the server
  • Determine the current version of Ghost available at ghost.org
  • If the installed version of Ghost is not equal to the version available at ghost.org then
    • Stop Ghost
    • Backup the config.js file
    • Download the latest version of Ghost
    • Expand the new version of Ghost and install it
    • Copy the backed up config.js back into place
    • Start Ghost with pm2
    • Delete the ghost_init.sh script and the cron that ran the script

With this new process we can still provide the Ghost AMI and users can still get the updated version of Ghost quickly. We will definitely still ship new versions of the AMI to keep up with OS software updates but we will end up spending significantly less time maintaining the AMI.

If you are curious about what happened when ghost_init.sh ran on your EC2 instance, you can see the output in /var/log/ghost_init.log.


Next the CloudFormation template creates what is called a Security Group which gets attached to the EC2 instance that is created. You can think of this Secutiry Group like an on host firewall. The Secutiry Group is going to allow incoming connections on port 22 and 80 from anywhere.

Note that this SecurityGroup is only used during our testing. When anybody takes our AMI from the AWS Marketplace and spins up an EC2 instance you will be defining your own SecurityGroup.

Scrubbing the AMI

Once our EC2 instance is created we perform the following checks:

  • We SSH into the EC2 instance and:
    • is Ghost set to run on start up
    • has the ghost_init.sh script been created
    • is the cron set to run ghost_init.sh
  • In the web interface we load up Ghost and make sure everything is looking good on the front end

After we have verified everything is looking ok we have to clean or "scrub" the instance of anything we do not want passed along to the end user. This includes

  • bash history
  • SSH keys
  • and log files

To perform this task we use this script. While SSH'd into the instance, we execute that script with the following command:

HISTSIZE=0; sudo wget -O - https://raw.githubusercontent.com/howtoinstallghost/Scrub-AMI/master/scrubAMI_Ghost.sh | sudo bash

That one liner sets the environment to not log commands to history and then pulls the script from GitHub and executes it.

Submitting the AMI to AWS

This section is more for a reminder to us but may help somebody else out.

Future Plans and Ideas

First and formost, a big part of why we are doing this is to get your ideas, feedback, and contributions. Please do not hesitate to contact us about any aspect of this.

Some ideas we have for the future are:

  • Convert the CloudFormation template over to cloudformation-ruby-dsl? Dealing with JSON as a "programming language" can be exausting
  • Convert over to using a Chef cookbook for the userdata section? This would give everybody an immense level of flexibility and could be reused outside of our process
    • Support installing different versions of Ghost?
    • Support installing multiple copies of Ghost?
    • Support installing a list of themes?
    • Support Apache or Nginx
    • Use the Application cookbook to support versions of Ghost, support rolling back versions?