Creating a Ghost Blog on an Azure App Service

Premier Developer

Premier

App Dev Manager Chris Tjoumas walks through how to setup continuous deployment of a blog using Ghost, Azure Functions, and Azure App Services.


Have you ever wanted to run your own blogging website but not sure if you want to build your own site or purchase an existing service? I had the same question myself and when I was studying for an Azure certification, I came across a demo of using this Ghost platform and the Azure Container Registry (ACR). It turns out this is fully open source and they provide both images and source code. I started to play around with setting up the ACR, but of course if you have content and you push an updated Ghost image to your registry, it will wipe out any information you have and I didn’t want to spend more time figuring out a way to keep the database it uses persistent. I then started looking at using the open source nodejs-based software and saw that Scott Hanselman had written about this – twice. First he showed how to modify the source code to get this to run on Azure, then posted an updated blog showing how this was updated to allow for a single click to deploy to Azure. This would take various parameters for your resource group, location, subscription, web app name, Sku, etc and deploy Ghost as an App Service to your subscription. When Ghost upgraded to version 1.x (current version is now 2.25.4 as of 7/1/2019), this deployment stopped working. Luckily for us, Radoslav Gatev fixed this with a Ghost-Azure project.

In this post, I’m going to be starting from another GitHub repo which branched off of Radoslav’s, as updates were no longer being made there. Since I wanted to ensure I had the latest working version, I started using Yannick Reekmans’ project. You can probably use either one, but I’ve done all of my testing with the latter and started my repo from there.

What I would like to do is setup two apps – one for staging and one for production. Radoslav wrote a nice Azure function which will check the official Ghost GitHub repo for an updated release and merge it into a repo of your choice. I would like to be able to use my staging app to get the release updates so I can test them and then decide if/when I want to merge that into my production app. So, let’s start by creating your own GitHub repo and get your staging and production ghost apps up and running:

  1. Fork the repo from here: https://github.com/YannickRe/Ghost-Azure
  2. Notice you will have an azure and azure-prod repo. What we want to do is use the azure branch as our staging branch and the production as, well, our production branch
  3. We are now going to create two Ghost app services – one for staging and one for production
  4. Create the staging app
    1. In your GitHub account/repo, make sure you are on the “azure” branch and click the “Deploy to Azure” button to get the ghost blog installed.
    2. Choose the free app service and fill the form out and click deploy
    3. Once the deployment is successful, after a few minutes, your self-hosted Ghost blog is up and running in Azure
  5. Create your production app
    1. In your GitHub account/repo, switch to the “azure-prod” branch and click the “Deploy to Azure” button to get the ghost blog installed.
    2. This time, choose at least Basic so you can setup a custom domain and fill the form out and deploy again
    3. You now have your production app service up and running

Note: If you end up setting up a custom domain, you will need to add that domain to your App Service Application Settings. In here, you create a new setting named “url” and the value will be your domain.

Automatic Updates of Ghost software

What we want to do now is to setup continuous deployment with our staging and production apps so that when we make a change to our staging ghost blog app source in GitHub, the app will pull these changes and deploy this new build. This way, we can run our staging app and verify everything works as expected. We then want to setup the same continuous deployment for our production app so a new production build will kick off when an update to the production branch is detected. For our production app, we won’t be making changes to the code though, we will simply be merging our staging repo into our production repo.

Finally, instead of relying on ourselves to make updates to the ghost blog (which you can certainly do!), I’d rather pull in updates from the official Ghost blog repo. So, we’ll use an Azure Function which will monitor the official repo for changes and, once detected, merge them into our staging branch. We’ll want to also get notified when an update is made, which I’ll show later, so we can go and check out our staging app before merging our staging to our production. Note: This is very important! The official Ghost repo pushes new releases often and there is a possibility that an update may break your app. As a matter of fact, on 6/25/19 when release 2.25.2 was released, I received a notification e-mail that my staging app build failed. While my prod app remained on build 2.25.1 and worked fine, I had the option of fixing my dev app or waiting. I began working on fixing the issue, figuring it was more to do with our app being tweaked to work on Azure. Just as I was about to test the changes, the Ghost team had a fix for it in release 2.25.3; less than a day later. I guess sometimes it just pays to wait 🙂

On to setting up our continuous deployment…

  1. There really isn’t anything you need to do here other than to ensure automatic deployment is setup through Deployment Center, which it should be once you perform the deploy to Azure. But, let’s check it here and become familiar with Deployment Center. For now, we will leave the default to Kudu since we just have a simple deployment need. But, as we want to do automated testing, gate checks, etc, you can use Azure Pipelines.
    1. Navigate to your App Service in the Azure Portal and click on Deployment Center
    2. Notice the Build is set to Kudu and the Branch is set to “azure”, which we said is our staging branch. What this does is, behind the scenes, creates a WebHook so that any change made to this branch will be detected and a new build will kick off.
    3. We’ll want to setup an alert so that once we create our Azure Function to automatically merge updates from the official Ghost repo, we know when our staging app had a new release deployed so we can test it and then merge that into our own production branch. To do this, we’ll create an Azure Function and use SendGrid to send an e-mail when a deployment (success or failure) is performed
        1. First, let’s create the SendGrid service. In the Azure portal, click Create a resource and search for SendGrid
        2. Click Create
        3. In the new Create a New SendGrid Account blade, fill out the required fields. For the subscription, a Free Trial should be more than sufficient as it allows for 25,000 emails per month and I don’t plan on having that many updates!
          Home > New > Marketplace > Get Started > SendG Create a New SendGrid A... D x CREATE * Name ghostdeplcymentemail * Password O * Confirm Password * Subscription Visual Studio Enterprise * Resource group O C) Create new (O) use existing * Pricing tier Promotion Code o * Contact Information Completed. Legal terms Legal terms accepted Automation options
        4. Once you click Create to create your SendGrid account, your account will be created after a few moments. Either by searching in the Azure portal for your SendGrid account, or clicking the resource in the notification once it’s deployed, go to your SendGrid account.
        5. Navigate to Configurations and copy the username and the SMTP server so you can generate the SendGrid API key. In order to use the SendGrid account by Azure Functions, we need to provide this SendGrid API as input to the Azure Function.
        6. To generate a SendGrid API Key, go to the SendGrid portal by going back to the main SendGrid blade in the Azure portal and clicking Manage. This will open a new browser window showing the SendGrid portal.
        7. In the portal, expand the Settings section on the left and click on API Keys
        8. Click on Create API Key.
        9. In the Create API Key window, enter an API Key Name and select the API access. I’ll leave mine as Full Access and click the Create & View button.
        10. Once the API key is created, click on the key to copy it to the clipboard.
        11. We’ll now need to configure the Azure Function with the SendGrid API Key. Back in the Azure portal, click +Create a resource and type in Function App and click Create. Give it a name, select your subscription, select the resource group your ghost blog resources are, and leave the Runtime stack as .NET. Navigate to your Azure Function and click on + New function and select In-portal and click Continue. Select Webhook + API and click Create. Select the name of your Azure function (the top-level of the function app tree) and click Platform features at the top and select the Configuration link. Here we want to add an Application setting. For this setting, the name will be the SendGrid API Key name (from step ix above) and the value is your SendGrid API key you copied from step x. Click Update and then Save.
        12. Back on your Azure Function, click the Integrate button under your function and under Outputs, click the + New Output option  DetectWebAppDeployment - HttpTrigger1 Apæ p "DetectWebAppDepIoyment" Visual Studio Enterprise Function Apps DetectWebAppDeploy... Functions f HttpTrigger1 Integrate Manage Q Monitor Proxies Slots (preview) (reg) HTTP trigger x delete Allowed HTTP methods O Selected methods Route template O Route template Selected HTTP methods O Triggers O POST Inputs O + New Input Request parameter name O Authorization level O Function Outputs O (S return) New Output DELETE OPTIONS
        13. Select the SendGrid binding and click the Select button to add the binding.
        14. Now you can fill out the necessary information for the output:
          1. Message parameter name: leave this as the default “message”
          2. To address: enter the e-mail you’d like to receive notifications
          3. Message subject: keep this blank so we can update this programmatically in our function
          4. SendGrid API Key App Setting: When you click on this field, you’ll see the app setting we added to the Azure Function which holds the SendGrid API.
          5. From address: the address sending this e-mail
          6. Message Text: Again, keep this blank so we can update programmatically in our function
        15. Click Save
        16. What we’d like to do is know if the deployment was a success or not. So, our Azure function is going to find this in the json that the Azure App Service post deployment webhook delivers via Kudu. What I’m interested in is the status of the deployment so we know if it was successful or not. The JSON from an Azure Web app deployment will look like:
          "id": "ed4e846f308d2c50e13c8068fb3e7218d2d72717" , "status": "success" "statusText": "authorEmaiI": ' "author": "ctjoumas" , "message": "Update tag. hbs "progress": " "deployer": "GitHub" , "recei vedTime : "startTime" : "2019-04-26T11 : 15. 600736Z" , "2019-04-26T11 . 6788633? , "endTime : " "2019-04-26T11 :49. 9727747Z" , "I astSuccessEndTi me" : "complete": true, "siteName": "hostName" : "2019-04-26T11 , . scm. azur ewebsi tes. net
          What we want to pull out is the “status” field. But, before we leave this page, we can go ahead and hook this deployment event up to our function app. Back in your function app, select the trigger and at the top, click the </> Get function URL link. Hop back over to your ghost staging app’s kudu page and select Tools–>Web hooks. In the Add Subscriber Url section, paste in your function URL, leaving the dropdown set as “PostDeployment” and click Add Url. Now, once any deployment completes to your staging app, it will trigger your function app, so let’s go ahead and write that to grab the status and send the email using SendGrid.You may also want to know the version which is being deployed. This will be stored in the “message” field so I’ve also added logic to pull that out and put into the e-mail.
        17. Navigate back to your Function app and select your HttpTrigger. At the top, add a new reference to SendGrid:
        18. Also, add this using statement:
        19. Then, add a new parameter of type mail to the end of the Run function and remove the async keyword as you cannot use the out parameter needed for SendMail with an async function. Your signature now looks like:
        20. You’ll also need to modify the requestBody assignment for a non-async function and put a bit of logic in to pull out the status and do something with it. This is fairly self-explanatory, including creation of usage of the SendGridMessage object, as you can see in my function below in its entirety:

       

      1. We aren’t quite done yet though. With Azure Functions 2, you will need to add the Storage extensions to the app. To do so, follow these steps:
        1. In the Overview tab of your function, select Stop, if you’ve tested it and have it running. This will unlock files in your app so that changes can be made.
        2. Choose the Platform features tab under the Development tools and select Advanced Tools (Kudu). This will open the Kudu endpoint of your app in a new window.
        3. In the Kudu window, select Debug console > CMD
        4. In the command window, navigate to D:\home\site\wwwroot and choose the delete icon next to bin to delete the folder.
        5. Choose the edit icon next to the extensions.csproj file to open the online editor.
        6. Inside the <ItemGroup> element, add the following: <PackageReference Include=”Microsoft.Azure.WebJobs.Extensions.Storage” Version=”3.0.0″ /> and click Save.
        7. Back in the console at the wwwroot folder, run the following command to rebuild the referenced assemblies in the bin folder:
          dotnet build extensions.csproj -o bin –no-incremental –packages D:\home\.nuget
        8. Back in the Overview tab in the portal, click Start to restart the function app.

 

  1. To put this all in practice, as mentioned earlier, we want to detect when the official Ghost repository is updated and automatically merge that into our ghost staging app. Luckily, an Azure Function was already created to do this which we will use. Thanks to the work we did above, once the function detects a new release in the official repo and it merges that into our staging app, we’ll get an e-mail letting us know there has been an update so we can go test it, then perform a merge from our staging app repo to our production app repo which will automatically deploy the latest release to our production app. So, let’s go ahead and create the new function app:
    1. First, let’s fork over the function app from here: https://github.com/YannickRe/Ghost-Release-Uploader
    2. Once it’s forked over, we’ll create our function app and use our newly forked github repo, with a few application setting updates in order to hook everything up correctly.
      1. Once your function app is created, click the + icon next to Functions and set up your project
      2. Select Visual Studio
      3. Use Deployment Center in order to have a continuous deployment pipeline
      4. Use the Finish and go to Deployment Center button below to navigate to Deployment Center and finish setting up your app. This will take you through a new wizard to configure a variety of deployment options, which we will be using GitHub. I’m going to keep it simple and use Kudu. Select your Ghost-Release-Uploader repo and click Finish
      5. Update the app settings per the README in GitHub.
      6. Also, this function app targets Azure Functions runtime 1 so you will need to go to your function app settings and switch to runtime version to 1. Once this is done, restart your function app and go to the deployment center to ensure everything runs correctly.
        1. One thing to note, you may not have an actual release for the function app to compare against. To test this, go to https://api.github.com/repos/<username>/<your ghost repo, which should be Ghost-Azure>/releases/latest. If you get a “Not Found” message returned, you need to create a release in your repo. Once that is done, re-run the API call to check the latest release and ensure you are getting a response. Also, for your git password, you’ll need to create and use a token here. Finally, make sure the release name is valid as the function will compare the release number in your repo against the official Ghost repo.

 

That’s it! You have successfully created your staging and production Ghost blog web apps, setup an Azure function to poll the official Ghost GitHub repo for a new release and merge an update into your staging repo, and created an Azure function to use send you an e-mail when a successful (or unsuccessful) release was pushed to your staging app. Now the only thing left to do is start working on your blog!

Premier Developer
Premier Developer

Premier Support for Developers

Follow Premier   

3 Comments
Avatar
Josh Hansen 2019-08-14 13:13:07
The deploy is failing for me. Here is the output from the failure: Command: deploy.cmd Handling node.js deployment. Creating app_offline.htm KuduSync.NET from: 'D:\home\site\repository' to: 'D:\home\site\wwwroot' Deleting file: 'hostingstart.html' Copying file: '.gitignore' Copying file: 'azuredeploy.json' Copying file: 'config.development.json' Copying file: 'config.production.json' Copying file: 'db.js' Copying file: 'entry.js' Copying file: 'Gruntfile.js' Copying file: 'iisnode.yml' Copying file: 'index.js' Copying file: 'LICENSE' Copying file: 'MigratorConfig.js' Copying file: 'package.json' Copying file: 'PRIVACY.md' Copying file: 'README.md' Copying file: 'renovate.json' Copying file: 'yarn.lock' Copying file: 'content\adapters\README.md' Copying file: 'content\data\README.md' Copying file: 'content\images\README.md' Copying file: 'content\logs\README.md' Copying file: 'content\settings\README.md' Copying file: 'content\themes\casper\author.hbs' Copying file: 'content\themes\casper\default.hbs' Copying file: 'content\themes\casper\error-404.hbs' Copying file: 'content\themes\casper\error.hbs' Copying file: 'content\themes\casper\gulpfile.js' Copying file: 'content\themes\casper\index.hbs' Copying file: 'content\themes\casper\package.json' Copying file: 'content\themes\casper\page.hbs' Copying file: 'content\themes\casper\post.hbs' Copying file: 'content\themes\casper\README.md' Copying file: 'content\themes\casper\renovate.json' Copying file: 'content\themes\casper\tag.hbs' Copying file: 'content\themes\casper\yarn.lock' Copying file: 'content\themes\casper\assets\screenshot-desktop.jpg' Copying file: 'content\themes\casper\assets\screenshot-mobile.jpg' Copying file: 'content\themes\casper\assets\built\global.css' Copying file: 'content\themes\casper\assets\built\global.css.map' Copying file: 'content\themes\casper\assets\built\infinitescroll.js' Copying file: 'content\themes\casper\assets\built\infinitescroll.js.map' Copying file: 'content\themes\casper\assets\built\jquery.fitvids.js' Copying file: 'content\themes\casper\assets\built\jquery.fitvids.js.map' Copying file: 'content\themes\casper\assets\built\screen.css' Copying file: 'content\themes\casper\assets\built\screen.css.map' Copying file: 'content\themes\casper\assets\css\csscomb.json' Copying file: 'content\themes\casper\assets\css\global.css' Copying file: 'content\themes\casper\assets\css\screen.css' Omitting next output lines... Using start-up script index.js from package.json. Node.js versions available on the platform are: 0.6.20, 0.8.2, 0.8.19, 0.8.26, 0.8.27, 0.8.28, 0.10.5, 0.10.18, 0.10.21, 0.10.24, 0.10.26, 0.10.28, 0.10.29, 0.10.31, 0.10.32, 0.10.40, 0.12.0, 0.12.2, 0.12.3, 0.12.6, 4.0.0, 4.1.0, 4.1.2, 4.2.1, 4.2.2, 4.2.3, 4.2.4, 4.3.0, 4.3.2, 4.4.0, 4.4.1, 4.4.6, 4.4.7, 4.5.0, 4.6.0, 4.6.1, 4.8.4, 5.0.0, 5.1.1, 5.3.0, 5.4.0, 5.5.0, 5.6.0, 5.7.0, 5.7.1, 5.8.0, 5.9.1, 6.0.0, 6.1.0, 6.2.2, 6.3.0, 6.5.0, 6.6.0, 6.7.0, 6.9.0, 6.9.1, 6.9.2, 6.9.4, 6.9.5, 6.10.0, 6.10.3, 6.11.1, 6.11.2, 6.11.5, 6.12.2, 6.12.3, 7.0.0, 7.1.0, 7.2.0, 7.3.0, 7.4.0, 7.5.0, 7.6.0, 7.7.0, 7.7.4, 7.10.0, 7.10.1, 8.0.0, 8.1.4, 8.4.0, 8.5.0, 8.7.0, 8.8.0, 8.8.1, 8.9.0, 8.9.3, 8.9.4, 8.10.0, 8.11.1, 10.0.0, 10.6.0, 10.14.1, 10.15.2. Selected node.js version 10.15.2. Use package.json file to choose a different version. Selected npm version 6.4.1 Updating iisnode.yml at D:\home\site\wwwroot\iisnode.yml Running npm install Failed exitCode=-4048, command="D:\Program Files (x86)\nodejs\10.15.2\node.exe" "D:\Program Files (x86)\npm\6.4.1\node_modules\npm\bin\npm-cli.js" install --production --no-package-lock removed 45 packages in 32.519s Checking database D:\home\site\wwwroot\node_modules\knex-migrator\lib\utils.js:38 throw new errors.KnexMigrateError({ ^ InternalServerError: Please provide a file named MigratorConfig.js in your project root. Failed exitCode=1, command="D:\Program Files (x86)\nodejs\10.15.2\node.exe" db.js at new KnexMigrateError (D:\home\site\wwwroot\node_modules\knex-migrator\lib\errors.js:7:26) at Object.loadConfig (D:\home\site\wwwroot\node_modules\knex-migrator\lib\utils.js:38:19) An error has occurred during web site deployment. at new KnexMigrator (D:\home\site\wwwroot\node_modules\knex-migrator\lib\index.js:18:24) at Object.<anonymous> (D:\home\site\wwwroot\db.js:2:20) at Module._compile (internal/modules/cjs/loader.js:689:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10) at Module.load (internal/modules/cjs/loader.js:599:32) at tryModuleLoad (internal/modules/cjs/loader.js:538:12) at Function.Module._load (internal/modules/cjs/loader.js:530:3) at Function.Module.runMain (internal/modules/cjs/loader.js:742:12) at startup (internal/bootstrap/node.js:283:19) at bootstrapNodeJSCore (internal/bootstrap/node.js:743:3) D:\home\site\wwwroot\node_modules\knex-migrator\lib\utils.js:38\r\n throw new errors.KnexMigrateError({\r\n ^\r\nInternalServerError: Please provide a file named MigratorConfig.js in your project root.\r\n at new KnexMigrateError (D:\home\site\wwwroot\node_modules\knex-migrator\lib\errors.js:7:26)\r\n at Object.loadConfig (D:\home\site\wwwroot\node_modules\knex-migrator\lib\utils.js:38:19)\r\n at new KnexMigrator (D:\home\site\wwwroot\node_modules\knex-migrator\lib\index.js:18:24)\r\n at Object.<anonymous> (D:\home\site\wwwroot\db.js:2:20)\r\n at Module._compile (internal/modules/cjs/loader.js:689:30)\r\n at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)\r\n at Module.load (internal/modules/cjs/loader.js:599:32)\r\n at tryModuleLoad (internal/modules/cjs/loader.js:538:12)\r\n at Function.Module._load (internal/modules/cjs/loader.js:530:3)\r\n at Function.Module.runMain (internal/modules/cjs/loader.js:742:12)\r\n at startup (internal/bootstrap/node.js:283:19)\r\n at bootstrapNodeJSCore (internal/bootstrap/node.js:743:3)\r\nD:\Program Files (x86)\SiteExtensions\Kudu\82.10503.3890\bin\Scripts\starter.cmd deploy.cmd Is this repo out of date and no longer working or are there additional steps I'm missing?