async function bootGhost({backend = true, frontend = true, server = true} = {}) {
    // Metrics
    const startTime = Date.now();
    debug('Begin Boot');

    // We need access to these variables in both the try and catch block
    let bootLogger;
    let config;
    let ghostServer;
    let logging;
    let metrics;

    // These require their own try-catch block and error format, because we can't log an error if logging isn't working
    try {
        // Step 0 - Load config and logging - fundamental required components
        // Version is required by logging, sentry & Migration config & so is fundamental to booting
        // However, it involves reading package.json so its slow & it's here for visibility on that slowness
        debug('Begin: Load version info');
        require('@tryghost/version');
        debug('End: Load version info');

        // Loading config must be the first thing we do, because it is required for absolutely everything
        debug('Begin: Load config');
        config = require('./shared/config');
        debug('End: Load config');

        // Logging is also used absolutely everywhere
        debug('Begin: Load logging');
        logging = require('@tryghost/logging');
        metrics = require('@tryghost/metrics');
        bootLogger = new BootLogger(logging, metrics, startTime);
        debug('End: Load logging');

        // At this point logging is required, so we can handle errors better

        // Add a process handler to capture and log unhandled rejections
        debug('Begin: Add unhandled rejection handler');
        process.on('unhandledRejection', (error) => {
            logging.error('Unhandled rejection:', error);
        });
        debug('End: Add unhandled rejection handler');
    } catch (error) {
        console.error(error); // eslint-disable-line no-console
        process.exit(1);
    }

    try {
        // Step 1 - require more fundamental components

        // Sentry must be initialized early, but requires config
        debug('Begin: Load sentry');
        const sentry = require('./shared/sentry');
        debug('End: Load sentry');

        // Initialize prometheus client early to enable metrics collection during boot
        // Note: this does not start the metrics server yet to avoid increasing boot time
        const prometheusClient = initPrometheusClient({config});

        // Step 2 - Start server with minimal app in global maintenance mode
        debug('Begin: load server + minimal app');
        const rootApp = require('./app')();

        if (server) {
            const GhostServer = require('./server/GhostServer');
            ghostServer = new GhostServer({url: config.getSiteUrl(), env: config.get('env'), serverConfig: config.get('server')});
            await ghostServer.start(rootApp);
            bootLogger.log('server started');

            // Ensure the prometheus client is stopped when the server shuts down
            ghostServer.registerCleanupTask(async () => {
                if (prometheusClient) {
                    prometheusClient.stop();
                }
            });
            debug('End: load server + minimal app');
        }

        // Step 3 - Get the DB ready
        debug('Begin: Get DB ready');
        await initDatabase({config});
        bootLogger.log('database ready');
        const connection = require('./server/data/db/connection');
        sentry.initQueryTracing(
            connection
        );
        debug('End: Get DB ready');

        // Step 4 - Load Ghost with all its services
        debug('Begin: Load Ghost Services & Apps');
        await initCore({ghostServer, config, frontend});

        // Instrument the knex instance and connection pool if prometheus is enabled
        // Needs to be after initCore because the pool is destroyed and recreated in initCore, which removes the event listeners
        if (prometheusClient) {
            prometheusClient.instrumentKnex(connection);
        }

        const {dataService} = await initServicesForFrontend({bootLogger});

        if (frontend) {
            await initFrontend(dataService);
        }
        const ghostApp = await initExpressApps({frontend, backend, config});

        if (frontend) {
            await initDynamicRouting();
            await initAppService();
        }

        await initServices();
        debug('End: Load Ghost Services & Apps');

        // Step 5 - Mount the full Ghost app onto the minimal root app & disable maintenance mode
        debug('Begin: mountGhost');
        rootApp.disable('maintenance');
        rootApp.use(config.getSubdir(), ghostApp);
        debug('End: mountGhost');

        // Step 6 - We are technically done here - let everyone know!
        bootLogger.log('booted');
        bootLogger.metric('boot-time');
        notifyServerReady();

        // Step 7 - Init our background services, we don't wait for this to finish
        initBackgroundServices({config});

        // If we pass the env var, kill Ghost
        if (process.env.GHOST_CI_SHUTDOWN_AFTER_BOOT) {
            process.exit(0);
        }

        // We return the server purely for testing purposes
        if (server) {
            debug('End Boot: Returning Ghost Server');
            return ghostServer;
        } else {
            debug('End boot: Returning Root App');
            return rootApp;
        }
    } catch (error) {
        const errors = require('@tryghost/errors');

        // Ensure the error we have is an ignition error
        let serverStartError = error;
        if (!errors.utils.isGhostError(serverStartError)) {
            serverStartError = new errors.InternalServerError({message: serverStartError.message, err: serverStartError});
        }

        logging.error(serverStartError);

        // If ghost was started and something else went wrong, we shut it down
        if (ghostServer) {
            notifyServerReady(serverStartError);
            ghostServer.shutdown(2);
        } else {
            // Ghost server failed to start, set a timeout to give logging a chance to flush
            setTimeout(() => {
                process.exit(2);
            }, 100);
        }
    }
}
----------------------------------
Boot Ghost - The magic starts here
----------------------------------

- This function is written with async/await so you can read, line by line, what happens on boot
- All the functions above handle init/boot logic for a single component
@returns {Promise<object>} ghostServer