TryGhost/Ghost · error · Error
Build gateway image not found: ${BUILD_GATEWAY_IMAGE}\n\nTo
Error message
Build gateway image not found: ${BUILD_GATEWAY_IMAGE}\n\nTo fix this, either:\n 1. Pull gateway image: docker pull ${BUILD_GATEWAY_IMAGE}\n 2. Use a different gateway image: GHOST_E2E_MODE=build GHOST_E2E_GATEWAY_IMAGE=<image> pnpm --filter @tryghost/e2e test What it means
Companion to error 101: after the main Ghost build image is verified, verifyBuildImageExists() separately inspects BUILD_GATEWAY_IMAGE (the traffic-routing gateway container image, e.g. nginx/traefik fronting Ghost). Inspect rejection throws with gateway-specific remediation. Only fires in build mode. Shares the same verifiedBuildImageKey cache, so it runs once per process per image pair.
Source
Thrown at e2e/helpers/environment/service-managers/ghost-manager.ts:144
} catch {
throw new Error(
`Build image not found: ${BUILD_IMAGE}\n\n` +
`You are running in "build" mode, which requires a pre-built Docker image.\n` +
`For local development, "dev" mode is recommended instead.\n\n` +
`To fix this, either:\n` +
` 1. (Recommended) Run "pnpm dev" first, then re-run tests — dev mode is auto-detected and doesn't need this image\n` +
` 2. Build locally: pnpm --filter @tryghost/e2e build:docker (with GHOST_E2E_BASE_IMAGE set)\n` +
` 3. Pull from registry: docker pull ${BUILD_IMAGE}\n` +
` 4. Use a different image: GHOST_E2E_MODE=build GHOST_E2E_IMAGE=<image> pnpm --filter @tryghost/e2e test`
);
}
try {
const gatewayImage = this.docker.getImage(BUILD_GATEWAY_IMAGE);
await gatewayImage.inspect();
debug(`Build gateway image verified: ${BUILD_GATEWAY_IMAGE}`);
} catch {
throw new Error(
`Build gateway image not found: ${BUILD_GATEWAY_IMAGE}\n\n` +
`To fix this, either:\n` +
` 1. Pull gateway image: docker pull ${BUILD_GATEWAY_IMAGE}\n` +
` 2. Use a different gateway image: GHOST_E2E_MODE=build GHOST_E2E_GATEWAY_IMAGE=<image> pnpm --filter @tryghost/e2e test`
);
}
GhostManager.verifiedBuildImageKey = buildImageKey;
}
/**
* Get existing container if running, otherwise create new one.
* This handles Playwright respawning processes after test failures.
*/
private async getOrCreateContainer(name: string, create: () => Promise<Container>): Promise<Container> {
try {
const existing = this.docker.getContainer(name);
const info = await existing.inspect();View on GitHub (pinned to 47d8b0e2ad)
Solutions
- Pull the gateway image: docker pull ${BUILD_GATEWAY_IMAGE}.
- Point at a different gateway image: GHOST_E2E_MODE=build GHOST_E2E_GATEWAY_IMAGE=<image> pnpm --filter @tryghost/e2e test.
- Confirm the tag matches what the build pipeline pushes; add the gateway image to CI's docker-pull step alongside BUILD_IMAGE.
Example fix
# before
GHOST_E2E_MODE=build pnpm --filter @tryghost/e2e test
# after
docker pull ${BUILD_GATEWAY_IMAGE} && GHOST_E2E_MODE=build pnpm --filter @tryghost/e2e test Defensive patterns
Strategy: validation
Validate before calling
async function gatewayImageExists(): Promise<boolean> {
try { await new Docker().getImage(BUILD_GATEWAY_IMAGE).inspect(); return true; }
catch { return false; }
}
if (GHOST_E2E_MODE === 'build' && !await gatewayImageExists()) {
throw new Error(`Pre-flight: gateway image ${BUILD_GATEWAY_IMAGE} missing. docker pull it.`);
} Prevention
- Pull the gateway image alongside the Ghost image in CI's setup step.
- Document GHOST_E2E_GATEWAY_IMAGE in the env docs so contributors set both.
- Cache both images in CI to avoid repeated registry pulls.
When it happens
Trigger: BUILD_GATEWAY_IMAGE (from GHOST_E2E_GATEWAY_IMAGE) is absent locally. The main image exists but the gateway image was never pulled/built. Gateway tag was changed via env without re-pulling. Different registry/source than the Ghost image so a single pull doesn't cover both.
Common situations: Hardened CI that caches the Ghost image but not the gateway image; switching gateway variants; new contributor running build mode who only pulled the Ghost image.
Related errors
- Build image not found: ${BUILD_IMAGE}\n\nYou are running in
- Cannot create without a persistence adapter. Use buildMany()
- Egress monitor sidecar did not start in time
- Egress monitor container has no network IP
- Ghost container became unhealthy during initialization
AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13).
Data as JSON: /api/errors/7ac7ca00d2220124.
Report an issue: GitHub.