Ghost Boot Sequence Explained: From Cold Start to Running Server in TryGhost/Ghost

The Ghost boot sequence is a 10-phase asynchronous orchestration governed by the bootGhost() function in ghost/core/core/boot.js, which systematically initializes configuration, database connections, core services, Express applications, and background jobs to transition the platform from a cold process to a fully operational publishing server.

The initialization logic in the TryGhost/Ghost repository is deliberately linear and heavily instrumented to ensure dependencies load in strict order. Whether starting a production instance or spinning up a test environment, the bootGhost() entry point coordinates every aspect of the platform lifecycle through discrete initialization phases.

Entry Point and Boot Architecture

The bootGhost() function exported from ghost/core/core/boot.js serves as the sole gateway for starting the application. It accepts an options object—typically {backend: true, frontend: true, server: true}—that determines which components to initialize. This design allows developers to boot only the backend API for testing or enable the full stack including the HTTP server.

The sequence is structured as a series of await statements rather than parallel initialization, ensuring that foundational services like configuration and logging exist before dependent systems attempt to load.

Phase 1: Foundation Loading (Configuration and Logging)

The boot process begins by loading the bare-minimum foundations required for error handling and diagnostics. According to the source code in ghost/core/core/boot.js, this phase executes between lines L76-L99.

First, the system loads version information via require('@tryghost/version'), which logging tools and Sentry require for proper tagging. Immediately after, the configuration singleton loads from ./shared/config (L81-L83), followed by the logging infrastructure (@tryghost/logging and @tryghost/metrics at L86-L89).

Before proceeding further, the process installs a global unhandled rejection handler:

process.on('unhandledRejection', (reason, promise) => {
    logging.error('Unhandled rejection:', reason);
});

This ensures that any stray Promise failures during subsequent async operations are captured and logged. If configuration or logging initialization fails, the entire boot sequence halts immediately within a try...catch block, as the system cannot recover without these primitives.

Phase 2: Early Service Initialization

Once diagnostics are operational, the sequence initializes error tracking and server infrastructure. This phase spans lines L9-L44 and L70-L84 in boot.js.

Sentry initializes first via require('./shared/sentry') (L9-L11) to capture any errors during the remaining startup sequence. Optionally, if metrics collection is enabled, initPrometheusClient({config}) executes at L13-L16.

The system then creates a minimal root Express application by invoking require('./app')() (L18-L20), which starts in maintenance mode. If the server option is truthy, the code instantiates GhostServer from ./server/ghost-server.js (L22-L25), preparing the HTTP listener.

Database readiness follows through initDatabase({config}), which delegates to DatabaseStateManager.makeReady() (L70-L84). This manager executes pending migrations and verifies schema compatibility. Once connected, Sentry hooks into the Knex connection for query tracing at sentry.initQueryTracing(connection) (L40-L44).

Phase 3: Core System Initialization (initCore)

With the database and server infrastructure ready, initCore() establishes services that must exist before any request handling begins (lines L96-L158).

The phase loads URL utilities (./shared/url-utils) early for timing purposes, followed by the limits service via await limits.init() (L101-L105), which creates resource caps used by the settings module. The settings service then hydrates all settings rows from the database and synchronizes the email verification flag (L107-L112).

Internationalization loads next via await i18n.init() (L114-L117), parsing all locale files into memory. The URL service initializes at L119-L129 with either lazy or eager loading depending on configuration, registering cleanup tasks when eager. Finally, background job queues initialize through jobService.initTestMode() and mentionsJobService.initTestMode() (L132-L158), establishing the async work infrastructure.

Phase 4: Frontend and Theme Services

When the frontend option is enabled, the sequence executes initServicesForFrontend() and initFrontend() to prepare the presentation layer.

Route settings load the routes.yaml configuration (L78-L81), while custom redirects parse redirects.json (L84-L86) and link redirects initialize click-tracking URL handlers (L88-L90). The theme service loads the active theme and triggers custom theme settings initialization at L98-L101, followed by marketing offers at L104-L107.

Frontend template helpers—such as {{asset}} and {{date}}—initialize separately via:

const helperService = require('./frontend/services/helpers');
await helperService.init();

This separation ensures the theme engine is fully prepared before the system mounts HTTP routes.

Phase 5: Express Application Assembly

The system now constructs the HTTP request pipeline through initExpressApps(). A parent Express instance loads from ./server/web/parent/app, then mounts backend and frontend sub-applications using virtual hosting:

const parentApp = require('./server/web/parent/app')();
const backendApp = require('./server/web/parent/backend')();
parentApp.use(vhost(config.getBackendMountPath(), backendApp));

const frontendApp = require('./server/web/parent/frontend')({urlService});
parentApp.use(vhost(config.getFrontendMountPath(), frontendApp));

Following mount configuration, initDynamicRouting() parses the routes file and builds the router manager, while initAppService() loads administrative applications such as posts and stats (lines 78-96).

Phase 6: Final Service Initialization and Launch

The remaining core services start in parallel via await Promise.all([...]) within initServices() (lines 13-40). This batch includes identity tokens, Stripe, members, tiers, permissions, IndexNow, webhooks, post-scheduling, comments, media-inliner, donations, gifts, recommendations, stats, and automations.

Once all services signal readiness, the system mounts the fully initialized Ghost application onto the root Express instance:

rootApp.disable('maintenance');
rootApp.use(config.getSubdir(), ghostApp);

This transition disables maintenance mode and exposes the complete application stack. The boot sequence concludes by notifying the parent process that the server is ready via notifyServerReady(), then launches background jobs through initBackgroundServices() (lines 78-86), including theme caching, ActivityPub, email analytics, and update checks.

Programmatic Usage Examples

For testing scenarios or custom integrations, you can invoke the boot sequence programmatically without starting the HTTP server:

const bootGhost = require('ghost/core/core/boot');

// Full production boot with server
bootGhost({backend: true, frontend: true, server: true})
    .then((ghostServer) => {
        console.log('Ghost is listening on', ghostServer.address);
    })
    .catch((err) => {
        console.error('Boot failed:', err);
        process.exit(1);
    });

To obtain only the Express application for unit testing:

bootGhost({backend: true, frontend: true, server: false})
    .then((rootApp) => {
        // rootApp is the configured Express instance
        // suitable for supertest or similar frameworks
    });

Summary

The Ghost boot sequence follows a strict dependency chain through ten distinct phases:

  • Foundation loading: Version info, configuration, logging, and global error handlers initialize first (L76-L99).
  • Early services: Sentry, Prometheus, minimal Express app, GhostServer, and database connections establish the runtime environment (L9-L44, L70-L84).
  • Core initialization: URL utils, limits, settings, i18n, and job queues prepare the business logic layer (L96-L158).
  • Frontend preparation: Routes, redirects, themes, and template helpers load when frontend functionality is required.
  • Express mounting: Backend and frontend applications mount on a parent Express instance with virtual host routing.
  • Service completion: Remaining services (payments, members, webhooks) start in parallel.
  • Launch: Maintenance mode disables, the full application mounts, and background jobs begin processing.

Frequently Asked Questions

What is the entry point for the Ghost boot sequence?

The entry point is the bootGhost() function exported from ghost/core/core/boot.js. This async function accepts an options object specifying which components to initialize—such as backend, frontend, and server—and returns a promise that resolves to either a GhostServer instance or the root Express application depending on the configuration.

How does Ghost handle database initialization during startup?

Database initialization occurs through the initDatabase() helper, which instantiates DatabaseStateManager from ghost/core/core/server/data/db/database-state-manager.js. The manager runs pending migrations via makeReady() and loads schema information through databaseInfo.init(). This step occurs early in the sequence (L70-L84) because most subsequent services require database connectivity.

What is the difference between initCore and initServices in Ghost?

initCore() initializes foundational services that must exist before other components load, including URL utilities, limits, settings, internationalization, and the URL service (L96-L158). initServices() executes later and initializes the remaining business logic—such as Stripe, members, permissions, and webhooks—via Promise.all() once core dependencies are available (L13-L40).

Can I start Ghost without the HTTP server for testing?

Yes. Pass server: false in the options object to bootGhost(). This configuration skips GhostServer instantiation and returns the root Express application instead, allowing you to mount the application in test frameworks like Mocha or Jest without binding to a network port.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →