Setting Up Ghost with S3 Storage Using ghost-storage-adapter-s3: Part 1

Setting Up Ghost with S3 Storage Using ghost-storage-adapter-s3: Part 1

AWS, Infrastructure, Home Lab

By default, Ghost stores uploaded images and media locally on the same server that runs Ghost. That works fine for small blogs, but if you want to run Ghost in a scalable environment (like Docker, Kubernetes, or multiple servers behind a load balancer), you’ll quickly run into problems.

To make Ghost more flexible, you can use Amazon S3 (or an S3-compatible service like MinIO, DigitalOcean Spaces, or Backblaze B2) for media storage. This decouples your storage from the Ghost instance, ensuring uploads are consistent across deployments.

The main driver behind this, was to use Ghost as a completely headless CMS allowing for Gatsby to serve images easily and separately without exposing my Ghost instance to the public. This process allows you to upload images within Ghost and have them available within a Gatsby generated site.

In this post, I’ll walk through setting up Ghost with ghost-storage-adapter-s3. Using Amazon S3 and Amazon CloudFront to seamlessly serve images for both Ghost as CMS and Gatsby as a static website.


Why Use S3 for Ghost Storage?

  • Scalability: Media is available no matter how many Ghost instances you run.
  • Durability: S3 handles replication and durability better than local disk.
  • Flexibility: Works with AWS S3 or any S3-compatible service.
  • Portability: Makes running Ghost in containers or serverless setups easier.

Why Use CloudFront for asset delivery?

  • Fast: Caches your content across a global network of edge locations, so users get data from the nearest server instead of your origin. This reduces latency and speeds up page loads.
  • Scalability: Your site can handle sudden traffic spikes without overwhelming your origin server. It distributes requests across multiple edge locations, keeping things stable even under heavy load.
  • Security: integrates with AWS Shield, WAF, and SSL/TLS to protect against DDoS attacks, bots, and other threats — while ensuring encrypted connections for your users.
  • Cost: By caching content at the edge, CloudFront reduces the amount of data pulled directly from your origin (like S3 or an EC2 instance). That means lower bandwidth costs and more efficient infrastructure usage.

Prerequisites

  • A running Ghost instance (self-hosted).
  • Node.js environment (to install the adapter).
  • An Amazon S3 bucket. (Steps to create these will also be detailed below)
  • An Amazon CloudFront distribution. (Steps to create these will also be detailed below)
  • AWS IAM Access key + Secret key with appropriate permissions. (Steps to create these will also be detailed below)

Step 1: Install ghost-storage-adapter-s3

Navigate to your Ghost installation directory and install the adapter:

npm install ghost-storage-adapter-s3
mkdir -p ./content/adapters/storage
cp -r ./node_modules/ghost-storage-adapter-s3 ./content/adapters/storage/s3

Ghost looks in content/adapters/storage/ for storage adapters, so the directory structure is important

  • Node.js environment (to install the adapter).
  • An S3 bucket (AWS or compatible).
  • S3 access key + secret key with appropriate permissions.

Step 2: Configure AWS resources

There are two ways to proceed with AWS, I would strongly suggest using the CloudFormation template and sticking with Configuration as Source Code, but I have included the manual steps also if that is more preferable. At the end of this s you should have the values for

  • AWS CLI Access Key - Used by Ghost to authenticate with AWS
  • AWS CLI Secret Access Key - Used in conjunction with the Access Key
  • AWS Region - The region in which your resources are deployed
  • AWS CloudFront distribution URL - Used as assetHost

Using a CloudFormation Template

I have provided a GitHub repository for this article that offers a bare bones CloudFormation template which will provision the following AWS resources:

  • 24 Hour CloudFront Cache Policy
  • CloudFront Origin Access Control (OAC)
  • S3 Storage Bucket
  • S3 Storage Bucket Policy - Linking the Distribution/OAC with the bucket
  • IAM Ghost User - Used by Ghost to access the bucket securely
  • IAM Ghost User Policy
  • CloudFront Distribution

The code can be reviewed here: https://github.com/atownsend247/brightbot-source-files/blob/main/ghost-storage-adapter-s3/cloudformation-template.yaml

You can use this to create a new CloudFormation stack from the AWS Console, then update the Project parameter to suit your requirement.

Finally once created complete, reference the Outputs tab of CloudFormation to get the CloudFront distribution URL to use as assetHost later on.

Now the IAM user is created navigate to it in the AWS Console, click on Security Credentials -> Create access key. Select Command Line Interface (CLI), select the confirmation, give it a friendly name and either download the credentials.csv or copy the Access key value and Secret access key to somewhere safe as we need these later.

Manually creating resources

You'll likely want to configure a separate S3 bucket for your blog, a specific IAM role, and, optionally, CloudFront, to serve from a CDN.

S3

Create a new bucket. The region isn't important at this stage.

As we are setting this up manually, you can let Amazon CloudFront setup the OAC (Origin Access Control) on your behalf.

IAM

You'll want to create a custom user role in IAM that just gives your Ghost installation the necessary permissions to manipulate objects in its S3 bucket.

Go to IAM in your AWS console and add a new user. Give it a username specific to your blog, and select Programmatic access as the Access type.

Next, on the permissions page, select Attach existing policies directly and click to Create policy. For the policy click on the JSON editor and add the following policy. You'll want to replace where it says your-bucket-name with the name of your blog's S3 bucket.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::your-bucket-name"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:PutObjectVersionAcl",
                "s3:DeleteObject",
                "s3:PutObjectAcl"
            ],
            "Resource": "arn:aws:s3:::your-bucket-name/*"
        }
    ]
}

IAM User Policy

What this policy does is allow the user access to see the contents of the bucket (the first statement), and then manipulate the objects stored in the bucket (the second statement).

Finally, create the user and copy the Access key and Secret access key, these are what you'll use in your configuration.

At this point your done with the Ghost access to Amazon S3

CloudFront

CloudFront is a CDN that replicates objects in servers around the world so your blog's visitors will get your assets faster by using the server closest to them. It uses your S3 bucket as the "source of truth" that it populates its servers with.

Got to CloudFront in AWS and choose to Create a Distribution. On the next screen you'll want to leave everything the same, except change the following:

  • Origin - Select Amazon S3
    • Click the Browse S3 button and find your newly created bucket
    • Ensure Allow private S3 bucket access to CloudFront - Recommended is selected as this will setup the secure/private permissions for you
  • Viewer Protocol Policy: Redirect HTTP to HTTPS
  • Compress Objects Automatically: Yes

Then create the distribution.

Finally, in your configuration, use the subdomain for the CloudFront distribution as your setting for assetHost.


Step 3: Configure Ghost to Use the Adapter

In your Ghost config file (config.production.json), add the storage block:

{
  ...,
  "storage": {
    "active": "s3",
    "s3": {
      "accessKeyId": "YOUR_AWS_ACCESS_KEY",
      "secretAccessKey": "YOUR_AWS_SECRET_KEY",
      "region": "your-region",
      "bucket": "your-s3-bucket-name",
      "assetHost": "https://your-cloudfront-endpoint",
      "serverSideEncryption": "AES256",
      "forcePathStyle": true,
      "acl": "private"
    }
  }
}
  • accessKeyId -> Your AWS access key for the user with required permissions.
  • secretAccessKey -> Your AWS secret key for the user with required permissions.
  • region → your S3 bucket’s AWS region.
  • bucket → the name of your S3 bucket.
  • assetHost → for serveing media via CloudFront.

Step 4: Restart Ghost

Restart your Ghost instance to apply changes:

ghost restart

From now on, uploaded images and media will go directly to your S3 bucket instead of local disk.


Step 5: Verify Uploads

  1. Log in to your Ghost admin panel.
  2. Upload an image to a post.
  3. Check your S3 bucket (or CDN) to confirm the file is there.

If you see the image in S3 and it loads in your post, you’re good to go.


Conclusion

With ghost-storage-adapter-s3, you can decouple Ghost’s media storage from your server, making your blog more resilient and easier to scale. Whether you’re running Ghost in Docker, on Kubernetes, or just want the reliability of cloud storage, this setup is a solid way to go.

If you also serve your media through a CDN (like CloudFront), you’ll get the added bonus of faster load times for readers all over the world.

Head over to Part 2 linked below, to expand this further and use a custom domain with AWS CloudFront.



References

Ghost: The best open source blog & newsletter platform
Beautiful, modern publishing with email newsletters and paid subscriptions built-in. Used by Platformer, 404Media, Lever News, Tangle, The Browser, and thousands more.
GitHub - colinmeinke/ghost-storage-adapter-s3: An AWS S3 storage adapter for Ghost
An AWS S3 storage adapter for Ghost. Contribute to colinmeinke/ghost-storage-adapter-s3 development by creating an account on GitHub.
GitHub - atownsend247/brightbot-source-files: Provides git access to scripts and templates for associated BrightBot blog posts
Provides git access to scripts and templates for associated BrightBot blog posts - atownsend247/brightbot-source-files