ghost-cloudflare-r2
v2.0.1
Published
Ghost 6 storage adapter for Cloudflare R2.
Readme
ghost-cloudflare-r2
Ghost 6 storage adapter for Cloudflare R2.
The adapter extends ghost-storage-base and uses the Cloudflare-documented
S3-compatible R2 data API through AWS SDK v3 for object upload, read,
existence checks, and deletion.
Changelog
v2.0.1
- Improve sync reliability: read
GHOST_CONTENTenv var for content path detection; verbose logging on every sync step; auto-create parent directories before writing.r2-syncedlock file.
v0.2.0
- Add
syncOnBootconfig option (defaults totrue). When enabled, the adapter automatically uploads all existing local files undercontent/images,content/media, andcontent/filesto the R2 bucket on Ghost startup. A.r2-syncedlock file is written tocontent/after the first successful sync so subsequent boots skip the scan. Delete the lock file to force a re-sync. Set"syncOnBoot": falseto opt out. - Expose
syncLocalToR2()method for programmatic use.
Install
1. Enter your Ghost site directory
cd /path/to/your/ghost/siteIf you installed Ghost via the CLI, the default location is typically:
# Ghost-CLI install (recommended)
cd /var/www/ghost
# Local development
cd ~/my-ghost-blog2. Install the package
npm install ghost-cloudflare-r2This downloads the adapter to node_modules/ghost-cloudflare-r2.
3. Link the adapter to Ghost's storage directory
Ghost loads custom storage adapters from content/adapters/storage/. Create the
directory and symlink the installed package.
⚠️ Permission note for Ghost-CLI installs: Ghost-CLI creates a
ghostsystem user to run the site. Your login user does not have write access to/var/www/ghost/content/by default. Before creating the symlink, run:# Add your user to the ghost group sudo usermod -aG ghost $USER # Apply the group change in the current shell newgrp ghost # Give the group write permission to the content directory # (adjust the path to match your actual Ghost install location) sudo chmod -R 775 /var/www/ghost/contentNow create the directory and symlink:
# Create the adapters directory if it doesn't exist
mkdir -p content/adapters/storage
# Symlink the package into it
ln -s ../../../node_modules/ghost-cloudflare-r2 content/adapters/storage/ghost-cloudflare-r2The resulting structure should look like:
content/
└── adapters/
└── storage/
└── ghost-cloudflare-r2 -> ../../../node_modules/ghost-cloudflare-r2Why a symlink? Ghost resolves adapters by looking for a folder named after the adapter under
content/adapters/storage/. Symlinking avoids duplicating the package — updates vianpm update ghost-cloudflare-r2take effect automatically. If you prefer not to symlink, you can copy the package there instead:cp -r node_modules/ghost-cloudflare-r2 content/adapters/storage/ghost-cloudflare-r2(You will need to re-copy on each update.)
4. Configure Ghost
Add the adapter configuration to your config.production.json (or
config.development.json for local dev) as shown below.
Configuration
Prerequisites
Before configuring, create the following credentials in your Cloudflare Dashboard:
| Field | Where to create it | What it is |
|-------|-------------------|------------|
| accountId | R2 Dashboard → top right, or visible in the URL after r2/ | Your Cloudflare account ID (31-32 hex chars) — always visible on any R2 page |
| accessKeyId / secretAccessKey | R2 → your bucket → Manage → S3-compatible credentials → Create | S3-compatible access key for reading/writing the bucket via AWS SDK |
| publicUrl | R2 → your bucket → Settings → Public URL | Public-facing URL for the bucket — either a custom domain or the auto-generated r2.dev subdomain |
Adapter config
Use a public R2 custom domain or an enabled r2.dev public URL for publicUrl.
Do not use the S3 API endpoint as publicUrl; it is for authenticated object
API calls.
To store all Ghost uploads in Cloudflare R2, configure the images, media,
and files storage features to use this adapter. Ghost keeps themes in a
separate theme storage service, so this does not move theme zip uploads.
config.production.json:
{
"adapters": {
"storage": {
"active": "ghost-cloudflare-r2",
"ghost-cloudflare-r2": {
"accountId": "YOUR_CLOUDFLARE_ACCOUNT_ID",
"bucket": "ghost",
"accessKeyId": "YOUR_R2_S3_ACCESS_KEY",
"secretAccessKey": "YOUR_R2_S3_SECRET_KEY",
"publicUrl": "https://cdn.example.com",
"cacheControl": "public, max-age=31536000, immutable",
"syncOnBoot": true
},
"images": {
"adapter": "ghost-cloudflare-r2",
"prefix": "content/images"
},
"media": {
"adapter": "ghost-cloudflare-r2",
"prefix": "content/media"
},
"files": {
"adapter": "ghost-cloudflare-r2",
"prefix": "content/files"
}
}
}
}Environment variable fallbacks:
CLOUDFLARE_ACCOUNT_ID
R2_BUCKET
R2_ACCESS_KEY_ID
R2_SECRET_ACCESS_KEY
R2_PUBLIC_URL
R2_PREFIX
R2_ENDPOINT
R2_REGION
R2_FORCE_PATH_STYLE
R2_CACHE_CONTROLOptions
accountId, bucket, accessKeyId, secretAccessKey, and publicUrl are
required.
endpoint defaults to:
https://<accountId>.r2.cloudflarestorage.comregion defaults to auto, matching Cloudflare R2 examples. prefix defaults
to content/images; for full upload coverage, set feature-specific prefixes as
shown above.
Scripts
npm test
npm run lint