Jack Pearce

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.

Build, Deploy and Scale Apps Quickly

](https://www.digitalocean.com/products/app-platform) 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 https://github.com/colinmeinke/ghost-storage-adapter-s3
RUN git clone https://github.com/TryGhost/Alto
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 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 with your database cluster ID and replace 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 ams3.digitaloceanspaces.com 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. digitaloceanspaces.com for ams3. You can setup a custom subdomain and DigitalOcean will manage the TLS certificate for you automatically.

    name: ghost-deployment region: lon services:

    • name: ghost-app envs:
      • 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 value:
      • key: storage__s3__secretAccessKey scope: RUN_TIME type: SECRET value:
      • key: storage__s3__region scope: RUN_TIME value: ams
      • key: storage__s3__bucket scope: RUN_TIME value:
      • key: storage__s3__assetHost scope: RUN_TIME value:
      • key: storage__s3__endpoint scope: RUN_TIME value: ams3.digitaloceanspaces.com
      • key: storage__s3__forcePathStyle scope: RUN_TIME value: “true”
      • key: storage__s3__acl scope: RUN_TIME value: public-read dockerfile_path: Dockerfile git: branch: main repo_clone_url: https://github.com/jkpe/ghost-app.git health_check: port: 2368 instance_count: 1 instance_size_slug: basic-xxs internal_ports:
      • 2368
    • name: ghost-proxy dockerfile_path: Dockerfile git: branch: main repo_clone_url: https://github.com/jkpe/ghost-proxy.git http_port: 80 instance_count: 1 instance_size_slug: basic-xxs routes:
      • path: / databases:
    • 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 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)