Modern Workplace Brewer, MVP & MCT

Recently I moved my Ghost blog from my on-prem Docker instance on a Synology to Azure App Services. I can tell you, sometimes it kept me awake at night. I found some blog posts, but all from some years ago and running with Ghost 1.x and they used the old classic Azure portal. Finally, I found 1 good blog, but with reduced information about how to setup up Ghost as an Azure App Service.
Since 24th of November 2020, I finally finished my move to Azure. I will try to create a step-by-step guide, on how I did my move from Docker to Azure App Service.

figure-1-1
As you can see, I had my Ghost website running in Docker on my Synology. I wanted to move this to Azure.

So, what do you need?

First you need of course an Azure account and a subscription.
Furthermore, you will need:

  • Resource group
  • Storage account
  • Azure Database for MySQL server
  • App Service plan
  • App Service

In this tutorial, I used the Azure Cloud Shell at https://shell.azure.com. But it is also working via the WSL Terminal.

Variables

We need the following variables before we execute the script blocks.

RG=<resourcegroupname>
WEBAPPNAME=<webappname>
LOCATION=<nearestazurelocation>
APPSERVICE=<appservicename>
SQLUSER=admin_$RANDOM
SQLPASS=<generateastrongpassword>
SQLSERVERNAME=sqlserver$RANDOM
PLAN=B1
SANAME=<storageaccountname>
FILESHARENAME=<azurefilesharename>
FILESHARECUSTOMID=<customfileshareid>
SQLSKU=B_Gen5_1

Resource group

First, we need a Resource group to put everything in.

#Resource group
echo "Creating resource group"
az group create --name $RG --location $LOCATION

That was the easy part. 😊

Storage account

Up next, we are going to create the Storage account and an Azure File Share. With this we can make the data persistent. Your website data will not be saved inside the Docker container, but on Azure File Shares. This script will also save the Access Key to a variable. We need this Access Key later.

#Storage Account
echo "Creating storage account, fileshare and retrieving accesskey"
az storage account create --resource-group $RG  --name $SANAME --access-tier Cool --location $LOCATION --sku Standard_LRS
az storage share create --account-name $SANAME --name $FILESHARE
SAKEY=$(az storage account keys list -g $RG -n $SANAME --query [0].value -o tsv)

Azure Database for MySQL

In a normal Docker instance, Ghost makes use of a local db file. If you want to restart your instance, then this db is wiped and you must build your website again. So therefore, we are going to use the Azure Database for MySQL service.

#MySQL Server
echo "Spinning up MySQL $SQLSERVERNAME in group $RG Admin is $SQLUSER"
az mysql server create --resource-group $RG --location $LOCATION --name $SQLSERVERNAME --admin-user $SQLUSER --admin-password $SQLPASS --sku-name $SQLSKU --ssl-enforcement Disabled --storage-size 5120 --auto-grow Enabled

Create firewall rules

The MySQL Server is by default "closed" for incoming connections. We need to create a firewall rule, so that the Azure Cloud Shell can communicate with the MySQL to create database or if you want to connect to a database with e.g., MySQL Workbench.

#Checking your IP
echo "Guessing your external IP address from ipinfo.io"
IP=$(curl -s ipinfo.io/ip)
echo "Your IP is $IP"

# Create firewall rules, so we can access the MySQL server
echo "Popping a hole in firewall for IP address $IP (that's you)"
az mysql server firewall-rule create --resource-group $RG --server $SQLSERVERNAME --name MyHomeIP --start-ip-address $IP --end-ip-address $IP

# Open up the firewall so wordpress can access - this is internal IP only
echo "Popping a hole in firewall for Azure services"
az mysql server firewall-rule create --resource-group $RG --server $SQLSERVERNAME --name AllowAzureIP --start-ip-address 0.0.0.0 --end-ip-address 0.0.0.0

Create the database

Because we created a firewall rule, the Azure Cloud Shell can now communicate with the MySQL server and we are able to create a database.

#Create the database
echo "Creating Ghost Database"
mysql --host=$SQLSERVERNAME.mysql.database.azure.com \
      --user=$SQLUSER@$SQLSERVERNAME --password=$SQLPASS \
      -e 'create database ghost;' mysql

We also set some ENV variables for MySQL.

#Set ENV variables
echo "Setting ENV variables locally"
MYSQL_SERVER=$SQLSERVERNAME.mysql.database.azure.com
MYSQL_USER=$SQLUSER@$SQLSERVERNAME
MYSQL_PASSWORD=$SQLPASS
MYSQL_PORT=3306

Your Ghost website (web app) contains two things. An App Service plan and an App Service. We need some sort of hardware to run the website. Here comes the App Service plan. An App Service plan is to determine the resources that are available to your website.


App Service plan

As mentioned above, we need some hardware. For my production website, I have chosen for the B3 version, but for this post, I will use the B1 version of the App Service plan.

#Create the AppService plan
echo "Creating AppService Plan"
az appservice plan create --name $APPSERVICE --resource-group $RG --sku $PLAN --is-linux

App Service

Meanwhile we have the "hardware" where we can put the Ghost instance. So let us create a web app with the latest Ghost image.

#Creating the web app
echo "Creating Web app"
az webapp create --resource-group $RG --plan $APPSERVICE --name $WEBAPPNAME --deployment-container-image-name ghost

The Ghost instance will start automatically, but we need to set other web app settings on it, so we need to stop the instance with the following code:

#Stop the webapp
az webapp stop --name $WEBAPPNAME --resource-group $RG

If you look in the Azure portal in your Resource group, you will see the four services that we have created.
figure-2

Add, set, and configure web app settings

First, we need to tell where the db is located, set the url and change the path to Azure File Share.

The URL-variable is now set to the default $appname.azurewebsites.net domain. After you are ready to change your DNS to your "new" website, you have to change the URL-variable to your custom domain.

#Adding some functional settings to it
echo "Adding app settings"
az webapp config appsettings set --name $WEBAPPNAME \
                                 --resource-group $RG \
                                 --settings \
                                 database__client=mysql \
                                 database__connection__database=ghost \
                                 database__connection__host=$MYSQL_SERVER \
                                 database__connection__user=$MYSQL_USER \
                                 database__connection__password=$MYSQL_PASSWORD \
                                 WEBSITES_PORT=2368 \
                                 WEBSITES_ENABLE_APP_SERVICE_STORAGE=true \
                                 NODE_ENV=production \
                                 url=http://$WEBAPPNAME.azurewebsites.net \
                                 paths__contentPath=/var/lib/ghost/content_files

Your web app should be enabled for Always-on and disable the FTPS.

#Turn on Always-on and disable FTPS
echo "Doing something with general settings"
az webapp config set --name $WEBAPPNAME \
                     --resource-group $RG \
                     --always-on true \
                     --ftps-state Disabled  

Now we map the instance to Azure File Share to have persistent storage.

#Configure path mappings
echo "Add some persistent storage to the webapp"
az webapp config storage-account add --resource-group $RG --name $WEBAPPNAME \
                                --account-name $SANAME \
                                --custom-id $FILESHARECUSTOMID \
                                --share-name $FILESHARENAME \
                                --access-key $SAKEY \
                                --storage-type AzureFiles \
                                --mount-path /var/lib/ghost/content_files

Start the webapp and login

Yes! We made it! This is the final step. We are going to start the Web App and login to the site admin.

#Start the webapp
az webapp start --name $WEBAPPNAME --resource-group $RG

#All done. Opening the site admin to create a account and personalize it.
echo "Opening site admin. This might give a 502. Just wait for an extra minute."
echo "When it does start, head to https://$WEBAPPNAME.azurewebsites.net/ghost to set it up."

You can download the whole script from my GitHub account.


Do I need to do something more?

Yes, you can. You can attach a custom domain to your web app, and you can upload your own SSL certificate.

Custom Domain

In the App Service, go to the Custom Domains option and click on Add custom domain. Follow the instructions to add records to your DNS of your domain.
figure-3

Upload certificate

After that, you can upload an existing certificate which belongs to your custom domain. After you uploaded a PFX or a CER, then you must bind the certificate and your custom domain together.
figure-4

Export and import your current Ghost website

You need to export your current Ghost blog to a JSON file. After that you need to import this JSON file in your new Ghost blog.

Export and import your data

You can export and import your data, like images, etc, to your new Azure Web App, with Azure Storage Explorer.

Change your DNS records

If you think that everything is fine, you have to change your DNS records to your Azure App services website.

You’ve successfully subscribed to Jeroen Burgerhout
Welcome back! You’ve successfully signed in.
Great! You’ve successfully signed up.
Success! Your email is updated.
Your link has expired
Success! Check your email for magic link to sign-in.