How to deploy Ghost on DigitalOcean App Platform

How to deploy Ghost on DigitalOcean App Platform

Today we will look at deploying Ghost on DigitalOcean App Platform. But why use this over a standard Droplet? (virtual server) and what are the drawbacks?

Ghost powers some of world's most well-known blogs. To name a few: Apple, Sky News, DuckDuckGo, Mozilla, OpenAI, Square, CloudFlare, Tinder, the Bitcoin Foundation and many more.

This post is mostly for fun and educational purposes. For alternatives you can check out Ghost(Pro) or DigitalOcean's Marketplace app

Ghost is free and open-source. There are 2 primary ways to deploy Ghost.

  1. Use Ghost(Pro)
  2. 'Self-host' on your own server. The DigitalOcean Ghost Droplet will have you up and running in minutes
DigitalOcean App Platform | Build, Deploy and Scale Apps Quickly
Get apps to market faster using App Platform, DigitalOcean’s platform to build, deploy and scale apps quickly. Starting at $5/mo.

DigitalOcean App Platform is a Platform-as-a-Service (PaaS) offering that allows developers to publish code directly to DigitalOcean servers without worrying about the underlying infrastructure.

Seperation of Responsibilities

A few notes of interest before we start:

  • We'll end up with a fully functional instance of Ghost 👻
  • OS updates, patching/upgrading MySQL, managing firewalls and provisioning TLS certificates are a thing of the past thanks to App Platform & a managed database cluster 🎉
  • Updating Ghost is easy with automated rollbacks
  • CDN is provisioned automatically 🌎
  • We'll be using a Ghost Storage Adapter & DigitalOcean Spaces (S3 compatible object storage) to store our media 📷
  • We'll deploy a Docker image for both Ghost and Caddy (reverse proxy) 🐳
  • Our Ghost Docker image allows us to persist our theme and S3 storage adapter across deployments (there is currently no persistent storage with App Platform)
Below are the Dockerfiles we will be using. App Platform will use these to build our Ghost and Caddy containers. You can use my git repos as a reference but feel free to fork and modify to your liking.

jkpe/ghost-proxy (repo #1)
This will deploy an instance of Caddy to serve as a reverse proxy for Ghost. We're using Caddy to tell App Platform's built-in CDN to cache slightly more agressively. You could use nginx here instead.

FROM caddy:2
ADD Caddyfile /etc/caddy/Caddyfile

CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile"]

:80 {
	@ghost_nocache {
		path /ghost/*
		path /p/*
		path /membership/*
		path /account/*
		path /robots.txt
		path /sitemap.xml
		path /sitemap.xsl
		path /rss/*
	handle @ghost_nocache {
		header ?Cache-Control "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0"
	handle {
		header Cache-Control "max-age=3600" {
	reverse_proxy ghost-app:2368 {
		header_up X-Forwarded-Proto "https"

jkpe/ghost-app (repo #2)
This is our 'custom' Ghost docker image. Here we ensure that our S3 storage adapter and theme of choice persists between App Platform deployments. In the example below I am pulling in the Alto theme and the latest version of Ghost 5.x.

FROM node:18-alpine as build
RUN apk add git
RUN git clone
RUN git clone
RUN cd ghost-storage-adapter-s3 && npm install

FROM ghost:5-alpine
COPY --from=build ghost-storage-adapter-s3 content/adapters/storage/s3
COPY --from=build Alto content/themes/alto

EXPOSE 2368/tcp

We'll be using doctl where possible to deploy and configure resources but you can use the Digital Ocean Control Panel if you are more comfortable there

Step 1: Deploy a managed database cluster

Ghost requires a database, we'll deploy a managed MySQL v8 database cluster for this:

doctl databases create ghost --engine mysql --num-nodes 1 --region lon1 --size db-s-1vcpu-1gb --version 8

--num-nodes Anything greater than 1 will add standby nodes.

In a database cluster, standby nodes maintain a copy of the primary node. If the primary node fails, a standby node is automatically promoted to replace it. MySQL clusters can have up to two standby nodes.

--size Database nodes come in all different sizes with differing amounts of vCPU, RAM and storage and different types, some offering dedicated CPU or NVMe. We'll use the smallest/cheapest size db-s-1-vcpu-1gb

Step 2: Configure the database

Lets create a new database and database user within our database cluster for Ghost to use. You can get a list of existing database clusters and their IDs by calling:

doctl databases list --format Name,ID

Then use that ID to create the database and a database user

doctl databases db create <db-id> ghost_app_platform
doctl databases user create <db-id> ghost_app_platform_user

Replace <db-id> with your database cluster ID

Step 3: Create an App in App Platform

We're going to create a blank app in App Platform so that we can add it as a trusted source to our managed database cluster. We'll update this app with the full deployment later in step 6.

name: blank-app
region: lon


doctl apps create --spec blank-app.yaml

Note down the ID given to this new app.

Step 4: Add your app as a Database Cluster trusted source

We want to restrict connections to our database cluster to our App only, when you do, all other public and private connections will be denied.

doctl databases firewalls append <db-id> --rule app:<app id>

Replace <db-id> with your database cluster ID and replace <app id> with your App ID

Step 5: Create a DigitalOcean Space (S3-compatible object storage)

App Platform runs your docker container with no persistent storage but Ghost by default will store your theme and media on disk. To work around this our Docker container imports a theme and the Ghost S3 storage adapter on every deployment. We'll use DigitalOcean Spaces with the S3 storage adapter.

To create a space follow the Spaces Quickstart. In addition to creating a space, generate a Spaces access token so that we can use this in our deployment later.

Step 6: Deploy Ghost to App Platform

Now we're going to deploy Ghost and Caddy to App Platform using an App Spec.

The application specification, or app spec, is a YAML manifest that declaratively states everything about your App Platform app, including each resource and all of your app’s environment variables and configuration variables.

Update the environmental varibles to match your deployment

Below in the provided App Spec (for service ghost-app ) you'll see some environmental variables, these tell Ghost how to connect to MySQL and where to store our media.

  • Update value for storage__s3__accessKeyId with your Spaces Access Key
  • Update value for storage__s3__SecretAccessKey with your Spaces Secret Key
  • Update value for storage__s3__region with your Spaces Region such as ams3
  • Update value for storage__s3__endpoint with your Spaces endpoint such as for ams3
  • Update value for storage__s3__bucket with your Spaces bucket name
  • Update value for storage__s3__assetHost with your Edge/CDN URL for example https://bucketname.ams3.cdn. for ams3. You can setup a custom subdomain and DigitalOcean will manage the TLS certificate for you automatically.
name: ghost-deployment
region: lon
- name: ghost-app
  - key: url
    scope: RUN_TIME
    value: ${APP_URL}
  - key: database__client
    scope: RUN_TIME
    value: mysql
  - key: database__connection__host
    scope: RUN_TIME
    value: ${ghost.HOSTNAME}
  - key: database__connection__database
    scope: RUN_TIME
    value: ${ghost.DATABASE}
  - key: database__connection__port
    scope: RUN_TIME
    value: ${ghost.PORT}
  - key: database__connection__user
    scope: RUN_TIME
    value: ${ghost.USERNAME}
  - key: database__connection__password
    scope: RUN_TIME
    value: ${ghost.PASSWORD}
  - key: database__connection__ssl__ca
    scope: RUN_TIME
    value: ${ghost.CA_CERT}
  - key: storage__active
    scope: RUN_TIME
    value: s3
  - key: storage__s3__accessKeyId
    scope: RUN_TIME
  - key: storage__s3__secretAccessKey
    scope: RUN_TIME
    type: SECRET
  - key: storage__s3__region
    scope: RUN_TIME
    value: ams
  - key: storage__s3__bucket
    scope: RUN_TIME
  - key: storage__s3__assetHost
    scope: RUN_TIME
  - key: storage__s3__endpoint
    scope: RUN_TIME
  - key: storage__s3__forcePathStyle
    scope: RUN_TIME
    value: "true"
  - key: storage__s3__acl
    scope: RUN_TIME
    value: public-read
  dockerfile_path: Dockerfile
    branch: main
    port: 2368
  instance_count: 1
  instance_size_slug: basic-xxs
  - 2368
- name: ghost-proxy
  dockerfile_path: Dockerfile
    branch: main
  http_port: 80
  instance_count: 1
  instance_size_slug: basic-xxs
  - path: /
- cluster_name: ghost
  db_name: ghost_app_platform
  db_user: ghost_app_platform_user
  engine: MYSQL
  name: ghost
  production: true
  version: "8"


doctl apps update <app id> --spec  ghost-do-app.yaml --wait

Replace <app id> with your App ID

After a few minutes the deployment will complete and your terminal will show the URL of your app, visit /ghost to setup Ghost!

Updating Ghost in the future

The image we're using is the 'Docker Official' image for Ghost, contributed by the community, whose tags follow Ghost releases. For example: ghost:5.18.0, ghost:5.18, ghost:5, ghost:latest

The Docker image for Ghost is an unofficial community package maintained by people within the Ghost developer community.

Triggering a deployment of our App on App Platform will pull in the latest Ghost Docker image therefore upgrading us to the latest version.

doctl apps create-deployment <app id>

We could automate this in the future based on when a new Docker image is publish.

Remember! New users to DigitalOcean get $200 credit to use in the first 60 days. Sign up here (this isn't a referral link)
Jack Pearce

Jack Pearce

Hi, I'm Jack, working as a Solutions Engineer