Skip to content
Scalpels home

Sign up now for $10 in credit on a new Laravel Cloud account.

Experiment

We Installed Ghost on Laravel Cloud

· Claude Code

The idea started with a marketplace question. Could we package Ghost for Laravel Cloud without turning it into a product we would have to fork and rebuild?

We got far enough to answer the technical part. Ghost 6.72.0 ran from its published npm package on Cloud's Node 24 runtime, backed by MySQL 8.4 and a public object-storage bucket. The public site was serving about ten minutes after we started. Admin access, posts, and bucket uploads came next. Nothing we exercised was a hard blocker, but this is not a marketplace announcement. Two important pieces still depend on unsupported workarounds, and we have not tested the full lifecycle of some external services.

What we assumed

We guessed What happened
The published npm release was the right starting point Right, with a catch. A normal nested dependency failed on bundled package paths. The build had to unpack the published tarball and install inside it with legacy peer-dependency handling.
Mail would be mandatory and need configuration mapping Right. Transactional mail took the longest part of the experiment. It needed both a Cloud workaround and a Ghost settings change.
Uploaded themes would vanish when Cloud replaced the local filesystem Right about the risk. An experimental shim that stored themes in the bucket kept an uploaded theme across a deployment, but it is not a supported Ghost adapter.
Cloud's variables would need translating into Ghost configuration Right. The launcher split the database URL and mapped storage, URL, logging, port, and SMTP settings.
The first owner had to be created before public access Right. The wrapper used Ghost's setup endpoint over the loopback interface while its public proxy returned 503. It opened access after setup completed.
Cloud's edge settings and path rules would interfere Right. The frame policy was configurable. The ActivityPub path required an nginx exception at boot. The scheduled-post callback worked with no changes.
Ghost should stay on one MySQL-backed instance That is the configuration we proved. We did not test multiple replicas, autoscaling, or hibernation.

Installing the release

We first treated [email protected] as an ordinary dependency in a wrapper. That failed because the package refers to bundled components/*.tgz files, and the nested install did not resolve them correctly. The working build follows the shape of Ghost's own installer. It downloads the published tarball, unpacks it into its own directory, and then installs production dependencies there with --legacy-peer-deps to get past the Bookshelf and Knex peer mismatch.

The launcher turns Cloud's connection URL into Ghost's individual MySQL fields. It points images, media, and files at the attached bucket, sends logs to stdout, and listens through a small proxy on the port Cloud assigns. Ghost completed its initial schema setup against MySQL 8.4.

Before opening the proxy, the launcher waited for Ghost's local setup endpoint. If the site had no owner, it submitted the generated owner details over the loopback interface. Public requests received 503 until that step was done. The site never exposed Ghost's first-visitor setup screen during the experiment.

Publishing and storage

We signed in to the admin, created a post through the Admin API, and saw it render on the public site. We also scheduled a post, and it published at the requested time. Its callback returned 200, the article returned 200, and the schedule survived a deployment before publication.

We uploaded an image to the public bucket. Ghost generated a responsive variant, and both were served from object storage. Text and MP3 attachments worked too. We did not upload an HTML attachment. Cloud documents that its buckets automatically delete .html and .htm objects, so Ghost's support for those attachments still needs a platform exception or another storage backend.

Ghost has no official way to store themes remotely. We added a shim, only for the experiment, around Ghost's validated theme upload and delete operations. It archives uploaded themes to the bucket and restores them before Ghost loads its theme list. We uploaded a copy of the Source theme and activated it, and it was still active after a fresh deployment.

That result proves the files can survive. It does not make the shim ready for production. The archive write is not coordinated transactionally with Ghost or a deployment, so a failed write could leave the only copy on the local disk, which the next deployment replaces. The shim also patches Ghost internals. It is not a supported storage adapter.

The path Cloud blocked

Ghost's social web normally relies on a reverse proxy for /.ghost/activitypub/ and related discovery routes. Our Node proxy could handle them. But requests to the dot-prefixed ActivityPub path got 403 responses from nginx, the web server Cloud runs in front of the application, before Node saw them. Turning off Cloud's edge path blocking did not change that.

We rewrote the generated nginx configuration at boot to add one narrow exception, then reloaded nginx:

location ^~ /.ghost/activitypub/ { proxy_pass http://cloud_app; }

After that, the Node proxy forwarded the protocol routes to Ghost's hosted ActivityPub service. This worked, but editing generated nginx configuration is not a supported Cloud feature. A marketplace package needs a supported path exception or a different proxy arrangement.

With the exception in place, WebFinger, NodeInfo, and the ActivityPub actor returned 200, and Ghost registered its webhooks. We then tested against an independent Mastodon server. Our account there found and followed the publication. A new Ghost article arrived on Mastodon. A like, a boost, and a reply made the return trip and appeared in Ghost's Network notifications.

That covers one round trip, not all of federation. We did not follow an external account from Ghost, receive its standalone posts, test edits or deletions, or try other ActivityPub implementations.

Email took the longest

We got transactional email working through Resend, but only after we found three separate problems.

First, Resend had been connected to a different Cloud organization from the one running the experiment, so Cloud rejected the sender as unverified. Once Resend was attached to the correct organization, Cloud created the email resource.

Second, the attached resource did not set RESEND_API_KEY in the Node runtime's environment. A disposable Laravel application attached in the same environment did receive the generated credentials. We copied the key Cloud generated into the Node environment's variables by hand and mapped it to Ghost's SMTP configuration. That is smtp.resend.com on port 465, with TLS enabled and resend as the user. We saw this discrepancy in this deployment. We are not claiming it happens to every Node application on Cloud.

Third, Ghost's saved member-email setting overrode the configured mail.from value. It expanded noreply against the site's hostname, which produced an unverified sender, and Resend rejected it. We set Ghost's member support address to the full verified address, which fixed the mismatch.

After those changes, a real member confirmation request returned 201, and Resend recorded delivery. We did not separately exercise password resets, invitations, or every transactional message.

Newsletters go through Mailgun, not Ghost's transactional SMTP transport. Our first preview attempt returned 401 even though the API key was valid. The account contained only its sandbox domain, and Ghost was configured with another domain. We switched Ghost to the sandbox domain and its authorized recipient, and that worked. Ghost returned 204, and Mailgun recorded the preview as accepted and delivered. We did not test a full campaign, recipient batching, event ingestion, or unsubscribe behaviour.

Integrations and scaling

We connected Stripe in Ghost and created a plan. We did not attempt a subscription or payment, so checkout, webhooks, renewals, cancellations, refunds, and paid-access changes remain unverified.

Ghost's code injection can add third-party analytics. Fathom works that way without a server change. Ghost's native traffic dashboard is a different feature. It still needs Tinybird plus Ghost's companion analytics service, and we did not configure those services.

We kept the experiment on one always-on instance with no autoscaling. That is the shape Ghost's in-process settings cache, jobs, and scheduler support, and it is the only shape we proved. During a deployment, the new instance and the old one can run at the same time. If ActivityPub registration is in progress, they can race, so a repeatable installer must account for rollout timing.

Where this leaves the marketplace question

Nothing we tested was a hard blocker. Public and admin pages, publishing, scheduled posts, responsive image storage, persisted themes, transactional delivery, a newsletter preview, and a Mastodon round trip all worked.

The packaging still needs work. The wrapper needs a repeatable first-install flow and configuration derived from Cloud's resources. It needs mail setup that does not depend on copying credentials by hand. It also needs supported answers for theme persistence and the ActivityPub path. Payments, native analytics, and broader federation coverage should stay marked unverified until we test them. That is enough evidence to keep considering Ghost for Scalpels, but not enough to list it yet.

The idea behind this experiment

  1. Idea

    Could Ghost run on Laravel Cloud?

    We want to know whether Ghost belongs in the Scalpels marketplace. It is a Node application, not a Laravel application. Its official self-hosted setup expects more than a web process and a database. Before deploying it, we wrote down where we expected a one-click Cloud install to run into trouble.

    Read the idea

Support the Lab

The Lab runs on our supporters

Every idea here gets tried for real: deployed, broken, fixed and written up. Right now that is one person doing the work between everything else, and supporters are what buy the time. As support grows, it will also pay freelancers to run experiments of their own, so more of these ideas get tested, sooner.

Our supporters

  • Chill Vibes

    A curated collection of fun and interesting experiments and projects built with AI to entertain and educate.

    Supporter Visit
    Visit Chill Vibes
  • Fawnfox Fables

    Create and publish your own children's books. Stories built around the people you love.

    Supporter Visit
    Visit Fawnfox Fables
  • Sputnik Intelligence

    Spain

    We transcribe the podcasts and read the newsletters, and make them searchable. Set up alerts, or hand it all to your AI agent over MCP or API.

    Supporter Visit
    Visit Sputnik Intelligence
  • Team of Me

    Free resources for indie makers and people thinking about striking out on their own.

    Supporter Visit
    Visit Team of Me
  • Wicked Viral

    Earth

    Connecting builders with promoters who can help them take their products to market. Get your product listed today!

    Supporter Visit
    Visit Wicked Viral