docs/contributing/development-setup.md
This guide runs the Ghost monorepo in its standard development configuration: Ghost Core and its backing services run in Docker, while frontend build watchers run on the host.
Install:
22.23.1 (the version in .nvmrc and
.node-version)For Node.js support in older Ghost releases, see the Node.js compatibility reference.
The default environment binds ports 80, 2368, 3306, 6379, 8025, and
8026. Stop local services using those ports before starting Ghost.
The repository pins its pnpm version in package.json. Activate that version
before first use rather than installing a separate global version of pnpm:
corepack enable pnpm
Clone the canonical repository with its submodules:
git clone --recurse-submodules [email protected]:TryGhost/Ghost.git
cd Ghost
If you already cloned without submodules, the setup command in the next section initializes them. Contributors without write access can create a fork and add it as a remote when they are ready to submit a pull request; a fork is not required to run Ghost locally.
From the repository root:
pnpm setup
pnpm setup installs the workspace and initializes all Git submodules. Run it
after a fresh clone and whenever a branch changes workspace dependencies or
submodules.
pnpm dev
The first run builds the development image and may take longer than subsequent starts. The command starts:
http://localhost:2368Wait for Docker Compose to report healthy services, then open:
On a new database, the Admin URL opens Ghost's setup screen. Create a local owner account there; the development environment does not define shared login credentials.
As a quick health check, confirm that the site and Admin load and that
docker compose -f compose.dev.yaml ps reports the Docker services as running or
healthy.
Press Ctrl+C in the development process to stop its watchers and containers.
Docker volumes preserve the database and uploaded development content between
runs.
| Service | Address |
|---|---|
| Ghost site | http://localhost:2368 |
| Ghost site (gateway alias) | http://localhost |
| Ghost Admin | http://localhost:2368/ghost/ |
| Mailpit | http://localhost:8025 |
| Mailpit (E2E) | http://localhost:8026 |
| MySQL | localhost:3306 using the ghost_dev database |
| Redis | localhost:6379 |
| Tinybird | http://localhost:7181 with pnpm dev:analytics |
| MinIO console | http://localhost:9001 with pnpm dev:storage |
| MinIO S3 API | http://localhost:9000 with pnpm dev:storage |
Run one root command at a time. Each variant includes the standard development environment and adds the listed tooling:
| Command | Use it when working on |
|---|---|
pnpm dev | Ghost Core, Admin, or Portal |
pnpm dev:public | Comments UI, Signup Form, Search, Announcement Bar, or Admin Toolbar |
pnpm dev:lexical | Koenig's Lexical editor inside Ghost Admin |
pnpm dev:analytics | Tinybird-backed analytics with the latest published version of the Traffic Analytics service |
pnpm dev:analytics:local | Tinybird-backed analytics with your locally running instance of the Traffic Analytics service |
pnpm dev:storage | S3-compatible storage through MinIO on ports 9000 and 9001 |
pnpm dev:stripe | Stripe webhooks exactly as production receives them; requires Tailscale, see below |
pnpm dev:full | Public app watchers plus analytics, storage, and Stripe |
Copy .env.example to .env only when you need an
optional integration. Never commit credentials or the local .env file.
To open Ghost on a phone or another computer, or to exercise HTTPS, subdirectory, and separate-Admin URL behaviour, see Testing development URLs and devices.
pnpm dev:stripe runs the webhook path production runs. It publishes Ghost's
webhook route, and nothing else, through
Tailscale Funnel, and Ghost registers a
pinned webhook endpoint at that address once Stripe is connected in Admin, then
deletes it on shutdown.
The site and Admin stay on localhost, so hot reload and the rest of the
development environment work as usual. Use it when the shape of a webhook
payload matters, for example when reading new fields from a checkout session.
Ghost logs an error whenever an event arrives rendered at a different API
version from the one it pins, in any environment.
The webhook route is reachable from the internet while the command runs; every request to it must carry a valid Stripe signature. The tunnel is a child process of the command and ends with it, including on Ctrl-C. Only a forced kill of the command can leave the tunnel running, and even then it does not survive a restart of Tailscale or the machine.
Funnel needs Tailscale 1.52 or newer with MagicDNS, HTTPS certificates and Funnel enabled for your tailnet and node. The command reports when Tailscale is missing, not signed in, or has no MagicDNS name; for the other requirements it shows Tailscale's own error.
pnpm dev:stripe --listen forwards events with stripe listen instead, which
needs STRIPE_SECRET_KEY in the environment or a local .env file but no
Tailscale. The CLI renders every event at your Stripe account's default API
version, which cannot be pinned, so an event can carry a different shape from
the one production receives; the command warns about this at startup and Ghost
logs an error when a mismatched event arrives. Use it only when the payload
shape does not matter.
After creating the local owner account, populate a development site with stable sample data:
pnpm reset:data
This clears the development database while preserving the owner, then creates
1,000 members and 100 posts. Use pnpm reset:data:empty for an empty site. Both
commands are destructive and require the Docker development environment to be
running.
When developing a database migration, apply pending migrations to the running development database with:
pnpm migrate:db
Development email is captured by Mailpit rather than delivered. Open http://localhost:8025 to inspect messages.
Before starting new work, update your local main from the canonical repository:
git fetch origin
git switch main
git pull --ff-only origin main
pnpm setup
If dependencies or Nx state become inconsistent after switching branches, run:
pnpm fix
This prunes the pnpm store, removes workspace node_modules directories,
reinstalls dependencies, and resets Nx state.
For narrower build and cache problems, use:
pnpm nx reset # Clear the Nx cache
pnpm build:clean # Clear the Nx cache and Ghost build output
pnpm docker:build # Rebuild the local development images
To stop containers outside a running pnpm dev process:
pnpm docker:down
As a last resort, pnpm docker:clean removes the development containers,
volumes, and locally built images. This deletes the local development database
and uploaded content; do not use it when you need to preserve that data.
If startup fails, inspect docker compose -f compose.dev.yaml ps and
docker compose -f compose.dev.yaml logs SERVICE-NAME. Check for occupied ports,
an unhealthy Docker daemon, and stale dependencies before resetting data or
volumes.
Use the README beside the area you are changing for its focused commands and architecture. The codebase documentation index links to the main workspace guides.