Running Ghost blog on Azure Container Apps

WordPress has become one of the most prominent website builders on the internet. It is free and is used by a lot of organizations and consumers. Millions of extensions and themes are built for WordPress. It's easy to install, easy to use, and easy to extend if necessary. But let's face it, it's also a hacker/spam/malware magnet because of its popularity and the many (unsafe) extensions.
But to be honest I was looking for something new. I am a Nerd and I want something to fiddle with.
After some searching, I stumbled upon Ghost. Ghost: The Creator Economy Platform. It looks beautiful, but I was a little bit turned down by the fact that I needed to run it on NodeJs and currently I also don't have a spare server lying around. Also, my knowledge of NodeJs is somewhat limited. Fortunately, I saw that they also offer a Docker image. ghost - Official Image | Docker Hub. Now Docker is a technology that I do know. :-)
I love Microsoft Azure so I immediately looked at what kind of cool options I have to host the Ghost Docker image. The most obvious option is an Azure WebApp running a Docker container. But that's boring because it's familiar. So I thought let's try Azure Container Apps. Azure Container Apps | Microsoft Azure
With Azure Container Apps we basically have AKS (Azure Kubernetes Service) without all the complex infrastructure setup.

But in order to scale up my new Blog, I must use a central database so that all my pods can access the same data at the same time. For example when you need to scale out.
I choose to use a MySql flexible server on Azure. (But you can also use a database container or even a local db file on Azure Storage)
az mysql flexible-server create --location westeurope --resource-group testGroup --name ghostserver --admin-user <<USERNAME>> --admin-password <<PASSWORD>> --sku-name Standard_B1ms
Next is to set up shared storage for the content/media files, etc. Otherwise, the content can only be loaded on a local pod. So if you have multiple instances of your website, it's essential that all pods can access the same images and other files.
So first we create a storage account.
az storage account create \
--resource-group <<RESOURCE GROUP>> \
--name <<ACCOUNT_NAME>> \
--kind FileStorage \
--sku Premium_LRS \
--output none
Next, we use that storage account to create a file share
az storage share-rm create \
--resource-group <<RESOURCE GROUP>> \
--storage-account <<ACCOUNT_NAME>> \
--name <<NAME OF THE FILE SHARE>> \
--access-tier "TransactionOptimized" \
--output none
In our next step, we want to link our file-share storage to the Azure Container App environment.
The Azure Container Apps environment acts as a secure boundary around a group of container apps.
But in order to link that storage account to the environment, we first need to create that environment ;-) like so...
az containerapp env create \
--name <<YOUR ENVIRONMENT NAME>> \
--resource-group <<RESOURCE GROUP>> \
--location westeurope
Now we need to configure the newly created Azure Storage as a storage mount in your Azure Container App. For that, you can use the following code.
az containerapp env storage set \
--access-mode ReadWrite \
--azure-file-account-name $STORAGE_ACCOUNT_NAME \
--azure-file-account-key $STORAGE_ACCOUNT_KEY \
--azure-file-share-name $STORAGE_SHARE_NAME \
--storage-name $STORAGE_MOUNT_NAME \
--name <<YOUR ENVIRONMENT NAME>> \
--resource-group <<RESOURCE GROUP>> \
--output table
Now we have created a storage mount, to our Azure File Share, inside our Azure Container App environment. With that in place, we can configure a volume for our containers in our YAML templates.
template:
containers:
- image: ghost:latest
name: my-container
volumeMounts:
- mountPath: /var/lib/ghost/content
volumeName: azure-files-volume
volumes:
- name: azure-files-volume
storageName: storagename
storageType: AzureFile
Now let's create a YAML template in which we specify all the details of our Azure Container App. Note: You can also see the details for the Azure File Share that we created.
type: Microsoft.App/containerApps
resourceGroup: <<RESOURCE GROUP>>
template:
containers:
- env:
- name: database__client
value: mysql
- name: database__connection__host
value: <<DATABASE HOST>>
- name: database__connection__user
value: <<DB USERNAME>>
- name: database__connection__password
value: <<PASSWORD>>
- name: database__connection__database
value: <<DATABASE NAME>>
image: docker.io/ghost:latest
name: <<YOUR APP NAME>>
resources:
cpu: 0.25
ephemeralStorage: 3Gi
memory: 0.5Gi
volumeMounts:
- mountPath: /var/lib/ghost/content
volumeName: azure-files-volume
initContainers: null
revisionSuffix: ''
scale:
maxReplicas: 2
minReplicas: 1
rules: null
volumes:
- name: azure-files-volume
storageName: <<STORAGE NAME>>
storageType: AzureFile
Replace all the <<PARAMETER>> parameters with your own details. We use that file to create our new Azure Container App. Let's call that file containerapp.yaml
Now we use that file to create the container app.
az containerapp create \
--name <<YOUR APP NAME>> \
--environment <<YOUR ENVIRONMENT NAME>> \
--resource-group <<RESOURCE GROUP>> \
--yaml containerapp.yaml
Finally, we have to wait for Azure for finishing up our Azure Container App.
Troubleshooting
In order to see what is going on inside your Azure Container App, you can easily navigate (through the Azure Portal) to your Azure Container App logs.

In order to connect to the MySql server, we must configure SSL in our environment variable.
- name: database__connection__ssl__ca
value: -----BEGIN CERTIFICATE-----\n...bla bla bla...\n-----END CERTIFICATE-----\n
You can find the certificate in your Azure Database MySql Networking settings.

References
Use storage mounts in Azure Container Apps | Microsoft Learn
Ghost: The #1 open source headless Node.js CMS
GitHub - docker-library/ghost: Docker Official Image packaging for Ghost
Quickstart: Deploy your code to Azure Container Apps | Microsoft Learn