Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 29 additions & 9 deletions .devcontainer/compose.devcontainer.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ services:
mysql:
# Smaller InnoDB buffer pool than the baseline compose.dev.yaml (1G).
# Lets the dev stack stay within a 2-core / 8 GB Codespace's budget
# after Ghost + 6 Vite dev servers are running. 256 MB is plenty for
# dev data volumes.
# after Ghost + the default Admin/Portal dev watchers are running.
# 256 MB is plenty for dev data volumes.
command: --innodb-buffer-pool-size=256M --innodb-log-buffer-size=64M --innodb-flush-log-at-trx_commit=0 --innodb-flush-method=O_DIRECT
mem_limit: 512m
restart: unless-stopped
Expand All @@ -19,8 +19,28 @@ services:
# The original ./ghost:/home/ghost/ghost mount from compose.dev.yaml is
# preserved via merge semantics so nodemon hot-reload still works for anyone
# running the backend the original way.
#
# pnpm-store is a separate named volume, NOT under /workspaces/Ghost, so
# it survives the bind mount above (which fully shadows anything baked
# into the image at that path with the host's — node_modules-less — repo
# checkout). onCreateCommand/postCreateCommand still have to run a real
# `pnpm install` every time since the source is live-mounted, but with
# the store already warm it's linking already-fetched content instead of
# re-downloading and recompiling native deps (better-sqlite3, sharp, etc.)
# from scratch.
volumes:
- ./:/workspaces/Ghost:cached
- pnpm-store:/pnpm-store
environment:
npm_config_store_dir: /pnpm-store
Comment on lines +42 to +43

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== package.json packageManager ==\n'
if [ -f package.json ]; then
  grep -n '"packageManager"' package.json || true
fi

printf '\n== repo files likely relevant ==\n'
git ls-files | rg '(^|/)(package.json|pnpm-lock\.yaml|pnpm-workspace\.yaml|\.devcontainer/compose\.devcontainer\.yaml)$' || true

printf '\n== inspect package.json if present ==\n'
if [ -f package.json ]; then
  cat -n package.json | sed -n '1,120p'
fi

printf '\n== inspect devcontainer file ==\n'
cat -n .devcontainer/compose.devcontainer.yaml | sed -n '1,120p'

Repository: TryGhost/Ghost

Length of output: 13418


🌐 Web query:

pnpm 11.8.0 environment variable config prefixes npm_config_store_dir pnpm_config_store_dir store-dir docs

💡 Result:

In pnpm 11.8.0, configuration is managed through environment variables prefixed with pnpm_config_ or PNPM_CONFIG_ [1][2]. The documentation explicitly states that pnpm no longer reads npm_config_* environment variables [2]. Users are advised to transition to using the pnpm_config_ prefix for all configuration settings [2]. For setting the package store directory, you should use the store-dir configuration setting [3]. You can configure this using the pnpm_config_store_dir environment variable [1][2]. Summary of Key Points: - Environment Variable Prefix: Use pnpm_config_ (e.g., pnpm_config_store_dir) [1][2]. The legacy npm_config_ prefix is no longer supported for pnpm configuration [2]. - Configuration Setting: The setting for the package store directory is store-dir [3]. - Setting via CLI: You can also set this configuration persistently using the command: pnpm config set store-dir /path/to/.pnpm-store [3]. - Behavior: If no store-dir is configured, pnpm will automatically create a store on the same drive and filesystem where the installation is taking place [3][4]. For optimal performance and to enable hard linking, the store must reside on the same drive as the project [4][5]. If the store is on a different drive, pnpm will copy files instead of linking them [4][5].

Citations:


Use pnpm_config_store_dir for the devcontainer store path

packageManager pins pnpm@11.8.0, which reads pnpm_config_*/PNPM_CONFIG_* only. npm_config_store_dir will be ignored, so the cache volume won’t take effect. Switch it to pnpm_config_store_dir: /pnpm-store.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.devcontainer/compose.devcontainer.yaml around lines 34 - 35, The
devcontainer environment is using the wrong store-path variable for pnpm, so the
cache volume will be ignored. In the devcontainer compose config, update the
environment entry under the devcontainer service from npm_config_store_dir to
pnpm_config_store_dir so pnpm@11.8.0 in the packageManager field can read it
correctly. Keep the value pointing to /pnpm-store and ensure the setting is
applied in the same environment block.

# Prefer the prebuilt image (docker/ghost-dev/Dockerfile, published by
# .github/workflows/devcontainer-build.yml) over a local build. cache_from
# makes a local `docker compose build` (e.g. after a Dockerfile edit)
# reuse those layers too, rather than starting from zero.
image: ghcr.io/tryghost/ghost-devcontainer:latest
build:
cache_from:
- ghcr.io/tryghost/ghost-devcontainer:latest
# Override the default `pnpm dev` command. VS Code controls startup and the
# user runs backend/frontend dev servers as tasks (see .vscode/tasks.json).
command: ["sleep", "infinity"]
Expand All @@ -38,15 +58,15 @@ services:
ghost-dev-gateway:
# Point Caddy at the dev servers running inside the ghost-dev container
# instead of host.docker.internal (which is the default for the hybrid
# host/container dev setup).
# host/container dev setup). Public apps (Portal, Comments UI, Signup
# Form, Sodo Search, Announcement Bar, Admin Toolbar) have no entry here
# — Caddy serves their umd/ build output directly via file_server, so
# there's no dev-server host:port to point at.
environment:
GHOST_BACKEND: ghost-dev:2368
ADMIN_DEV_SERVER: ghost-dev:5174
ADMIN_LIVE_RELOAD_SERVER: ghost-dev:4200
PORTAL_DEV_SERVER: ghost-dev:4175
COMMENTS_DEV_SERVER: ghost-dev:7173
SIGNUP_DEV_SERVER: ghost-dev:6174
SEARCH_DEV_SERVER: ghost-dev:4178
ANNOUNCEMENT_DEV_SERVER: ghost-dev:4177
ADMIN_TOOLBAR_DEV_SERVER: ghost-dev:4176
LEXICAL_DEV_SERVER: ghost-dev:4173

volumes:
pnpm-store:
30 changes: 17 additions & 13 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,18 @@
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}
},

// Codespaces prebuild step — runs once when the image is built.
// Primes the pnpm store so first-open is fast.
// Runs on the host/Codespaces VM before the container is built or pulled.
// Warms the local image cache with the prebuilt ghost-dev image
// (.github/workflows/devcontainer-build.yml) so ghost-dev's cache_from
// (see compose.devcontainer.yaml) has something to reuse layers from,
// whether this container gets pulled directly or rebuilt locally.
// Best-effort: don't fail creation if the registry is unreachable.
"initializeCommand": "docker pull ghcr.io/tryghost/ghost-devcontainer:latest || true",

// Runs once the workspace mount is ready, after the container is created.
// Note: no Codespaces prebuild is configured for this repo, so this runs
// live on every creation rather than being baked into a snapshot ahead of
// time — same as postCreateCommand below.
"onCreateCommand": "corepack enable && corepack prepare --activate && cd /workspaces/Ghost && pnpm install --prefer-offline || true",

// Runs after the workspace mount is ready on every container create.
Expand All @@ -40,6 +50,10 @@
// The script guards against double-starting if the stack is already up.
"postAttachCommand": "bash .devcontainer/start-dev-stack.sh",

// Portal, Comments UI, Signup Form, Sodo Search, Announcement Bar, and
// Admin Toolbar are NOT listed here — since the Caddy file_server switch
// (#28970) they build to disk (`vite build --watch`) instead of running
// a dev server, so there's no port to forward for them.
"forwardPorts": [
2368,
3306,
Expand All @@ -48,11 +62,6 @@
8025,
5174,
4200,
4175,
7173,
6174,
4178,
4177,
4173
],
"portsAttributes": {
Expand All @@ -63,12 +72,7 @@
"8025": {"label": "Mailpit Web"},
"5174": {"label": "Admin (Vite)"},
"4200": {"label": "Ember live-reload"},
"4175": {"label": "Portal"},
"7173": {"label": "Comments UI"},
"6174": {"label": "Signup Form"},
"4178": {"label": "Sodo Search"},
"4177": {"label": "Announcement Bar"},
"4173": {"label": "Koenig Lexical"}
"4173": {"label": "Koenig Lexical (optional)"}
},

"customizations": {
Expand Down
5 changes: 4 additions & 1 deletion .devcontainer/postCreate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ corepack prepare --activate

git submodule update --init --recursive

pnpm install --prefer-offline
# pnpm install already ran in onCreateCommand against this same fully-mounted
# workspace (submodules are theme content, not workspace packages, so their
# absence above didn't affect that install) — a second pass here would just
# re-walk the whole dependency graph for nothing.

# Build workspace packages that ghost/core imports at runtime with build
# outputs (not source). @tryghost/parse-email-address is the only one today
Expand Down
10 changes: 9 additions & 1 deletion .devcontainer/start-dev-stack.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@ nohup pnpm --filter ghost dev >> /tmp/ghost-backend.log 2>&1 &
disown

{ echo "=== $(date -Is) starting frontends ==="; } >> /tmp/ghost-frontends.log
# Matches root `pnpm dev`'s default fan-out (Admin + Portal only) — most
# devcontainer sessions never touch the public UMD apps, and those watchers
# are the heaviest processes in the stack. To also start them, run e.g.:
# pnpm nx run-many -t dev --projects=@tryghost/comments-ui,@tryghost/signup-form,@tryghost/sodo-search,@tryghost/announcement-bar,@tryghost/admin-toolbar
nohup pnpm nx run-many -t dev \
--projects=@tryghost/admin,@tryghost/portal,@tryghost/comments-ui,@tryghost/signup-form,@tryghost/sodo-search,@tryghost/announcement-bar \
--projects=@tryghost/admin,@tryghost/portal \
>> /tmp/ghost-frontends.log 2>&1 &
disown

Expand All @@ -33,4 +37,8 @@ Ghost dev stack starting in the background.
Gateway: http://localhost:2368/

Give it ~30-60s, then open http://localhost:2368/ghost/ for admin.

Only Admin + Portal dev watchers start by default. To add the public UMD
apps (Comments, Signup Form, Sodo Search, Announcement Bar, Admin Toolbar):
pnpm nx run-many -t dev --projects=@tryghost/comments-ui,@tryghost/signup-form,@tryghost/sodo-search,@tryghost/announcement-bar,@tryghost/admin-toolbar
MSG
12 changes: 5 additions & 7 deletions docker/dev-gateway/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,14 @@ FROM caddy:2-alpine@sha256:5f5c8640aae01df9654968d946d8f1a56c497f1dd5c5cda4cf95a

COPY --from=builder /usr/bin/caddy /usr/bin/caddy

# Default proxy targets (can be overridden via environment variables)
# Default proxy targets (can be overridden via environment variables).
# Public apps (portal, comments-ui, signup-form, sodo-search,
# announcement-bar, admin-toolbar) are served directly from their umd/
# build output via the Caddyfile's file_server block, not proxied to a
# dev server — they have no *_DEV_SERVER entry here.
ENV GHOST_BACKEND=ghost-dev:2368 \
ADMIN_DEV_SERVER=host.docker.internal:5174 \
ADMIN_LIVE_RELOAD_SERVER=host.docker.internal:4200 \
PORTAL_DEV_SERVER=host.docker.internal:4175 \
COMMENTS_DEV_SERVER=host.docker.internal:7173 \
SIGNUP_DEV_SERVER=host.docker.internal:6174 \
SEARCH_DEV_SERVER=host.docker.internal:4178 \
ANNOUNCEMENT_DEV_SERVER=host.docker.internal:4177 \
ADMIN_TOOLBAR_DEV_SERVER=host.docker.internal:4176 \
LEXICAL_DEV_SERVER=host.docker.internal:4173 \
ANALYTICS_PROXY_TARGET=analytics:3000 \
ACTIVITYPUB_PROXY_TARGET=host.docker.internal:8080
Expand Down
18 changes: 5 additions & 13 deletions docker/dev-gateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ This directory contains the Caddy reverse proxy configuration for the Ghost deve
## Purpose
The Caddy reverse proxy container:
1. **Routes Ghost requests** to the Ghost container backend
2. **Proxies asset requests** to local dev servers running on the host
3. **Enables hot-reload** for frontend development without rebuilding Ghost
2. **Proxies asset requests** to local dev servers running on the host (Admin, Lexical)
3. **Serves public app assets** (Portal, Comments UI, Signup Form, Sodo Search, Announcement Bar, Admin Toolbar) directly from their `umd/` build output via `file_server` — no dev server involved
4. **Enables hot-reload** for frontend development without rebuilding Ghost

## Configuration
### Environment Variables
Expand All @@ -14,11 +15,6 @@ Caddy uses environment variables (set in `compose.dev.yaml`) to configure proxy
- `GHOST_BACKEND` - Ghost container hostname (e.g., `ghost-dev:2368`)
- `ADMIN_DEV_SERVER` - React admin dev server (e.g., `host.docker.internal:5174`)
- `ADMIN_LIVE_RELOAD_SERVER` - Ember live reload WebSocket (e.g., `host.docker.internal:4200`)
- `PORTAL_DEV_SERVER` - Portal dev server (e.g., `host.docker.internal:4175`)
- `COMMENTS_DEV_SERVER` - Comments UI (e.g., `host.docker.internal:7173`)
- `SIGNUP_DEV_SERVER` - Signup form (e.g., `host.docker.internal:6174`)
- `SEARCH_DEV_SERVER` - Sodo search (e.g., `host.docker.internal:4178`)
- `ANNOUNCEMENT_DEV_SERVER` - Announcement bar (e.g., `host.docker.internal:4177`)
- `LEXICAL_DEV_SERVER` - *Optional:* Local Koenig Lexical editor dev server (e.g., `host.docker.internal:4173`)
- For developing Lexical in the separate [Koenig repository](https://github.com/TryGhost/Koenig)
- Requires `EDITOR_URL=/ghost/assets/koenig-lexical/` when starting admin dev server
Expand All @@ -43,15 +39,11 @@ The Caddyfile defines these routing rules:
| `/.well-known/webfinger` | ActivityPub server (port 8080) | *Optional:* WebFinger for federation |
| `/.well-known/nodeinfo` | ActivityPub server (port 8080) | *Optional:* NodeInfo for federation |
| `/ghost/assets/koenig-lexical/*` | Lexical dev server (port 4173) | *Optional:* Koenig Lexical editor (falls back to Ghost if not running) |
| `/ghost/assets/portal/*` | Portal dev server (port 4175) | Membership UI |
| `/ghost/assets/comments-ui/*` | Comments dev server (port 7173) | Comments widget |
| `/ghost/assets/signup-form/*` | Signup dev server (port 6174) | Signup form widget |
| `/ghost/assets/sodo-search/*` | Search dev server (port 4178) | Search widget (JS + CSS) |
| `/ghost/assets/announcement-bar/*` | Announcement dev server (port 4177) | Announcement widget |
| `/ghost/assets/{portal,comments-ui,signup-form,sodo-search,announcement-bar,admin-toolbar}/*` | `apps/<name>/umd` (file_server) | Public app widgets — served from disk, rebuilt on change by each app's `vite build --watch` |
| `/ghost/assets/*` | Admin dev server (port 5174) | Other admin assets — rewritten to `/__admin-dev__/assets/*` |
| `/__admin-dev__/*` | Admin dev server (port 5174) | Vite internals (HMR, modules, refresh runtime, dev-only assets) |
| `/ghost`, `/ghost/` | Admin dev server (port 5174) | Admin HTML entry — rewritten to `/__admin-dev__/` |
| `/ghost/*` (deep links) | Ghost backend | Express middleware redirects deep links to `/ghost/#/<path>` |
| Everything else | Ghost backend | Main Ghost application |

**Note:** All port numbers listed are the host ports where dev servers run by default.
**Note:** Port numbers listed for Admin and Lexical are the host ports where those dev servers run by default. Public apps have no dev-server port — Caddy reads their build output straight off disk.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@
"docker:up": {
"executor": "nx:run-commands",
"options": {
"command": "docker compose -f compose.dev.yaml ${DEV_COMPOSE_FILES} up -d --force-recreate --wait --quiet-pull"
"command": "if [ -n \"$CODESPACES$REMOTE_CONTAINERS\" ]; then echo 'Skipping docker compose up --force-recreate: already running inside a devcontainer, whose own compose lifecycle brought these services up.'; else docker compose -f compose.dev.yaml ${DEV_COMPOSE_FILES} up -d --force-recreate --wait --quiet-pull; fi"
},
"dependsOn": [
"docker:build"
Expand All @@ -126,7 +126,7 @@
"docker:build": {
"executor": "nx:run-commands",
"options": {
"command": "docker compose --progress=quiet -f compose.dev.yaml ${DEV_COMPOSE_FILES} build"
"command": "if [ -n \"$CODESPACES$REMOTE_CONTAINERS\" ]; then echo 'Skipping docker compose build: already running inside a devcontainer, whose own compose lifecycle built/pulled these images.'; else docker compose --progress=quiet -f compose.dev.yaml ${DEV_COMPOSE_FILES} build; fi"
}
},
"docker:dev": {
Expand Down
Loading