Skip to main content

Ghost Common Shared Configuration Module

The Ghost Common module defines the Ghost publishing platform configuration for the RAD Modules ecosystem. It is a pure configuration module — it creates no GCP resources and produces a config output consumed by platform-specific wrapper modules (Ghost CloudRun and Ghost GKE).

1. Overview

Purpose: To centralize all Ghost-specific configuration (custom container image, MySQL 8.0 database setup, environment variable mapping, health probes, storage bucket, and initialization job) in a single module shared by both Cloud Run and GKE deployments.

Architecture:

Layer 3: Application Wrappers
├── Ghost_CloudRun ──┐
└── Ghost_GKE ──┤── instantiate Ghost_Common

Ghost_Common (this module)
Creates: (no GCP resources)
Produces: config, storage_buckets, path

Layer 2: Platform Modules
├── App_CloudRun (serverless deployment)
└── App_GKE (Kubernetes deployment)

Layer 1: App_Common (networking, database, storage, secrets, IAM)

Key characteristics:

  • The only *_Common module in the ecosystem that uses MySQL 8.0 instead of PostgreSQL.
  • Creates no GCP resources — no secrets, no IAM bindings (compare with Directus Common and Django Common which create Secret Manager secrets).
  • Defines a readiness probe in addition to startup and liveness probes (unique among the *_Common modules).
  • The entrypoint.sh script auto-detects the Cloud Run service URL at runtime via the GCE metadata server and Cloud Run API v2, removing the need to know the URL at Terraform plan time.

2. Outputs

config

The application configuration object passed to the platform module via application_config.

FieldValue / Description
app_name"ghost"
application_versionVersion tag (default: "6.14.0")
container_image"ghost" (public Docker Hub image used as build base)
image_source"custom" — a custom wrapper image is built (see §6)
enable_image_mirroringvar.enable_image_mirroring (default false) — controls whether the image is mirrored to Artifact Registry
container_build_configdockerfile_path = "Dockerfile", context_path = ".", build_args = { APP_VERSION = <version> }
container_port2368
database_type"MYSQL_8_0" — Ghost 6.x requires MySQL 8.0+
db_nameDatabase name (default: "ghost")
db_userDatabase user (default: "ghost")
enable_cloudsql_volumeWhether to mount the Cloud SQL Auth Proxy sidecar (default: true)
cloudsql_volume_mount_path"/cloudsql"
gcs_volumesList of GCS Fuse volume mounts (empty by default)
container_resourcesCPU: 2000m, Memory: 4Gi (higher than other modules — Ghost 6.x is resource-intensive)
environment_variablesPassed through directly from var.environment_variables
secret_environment_variablesvar.secret_environment_variables (default {}) — secret env vars passed to the container; managed externally or via wrapper by default
enable_mysql_pluginsfalse
mysql_plugins[]
initialization_jobsDefault db-init job or custom override — see §5
startup_probeHTTP GET /, 90s initial delay, 10s timeout, 10s period, 10 failure threshold
liveness_probeHTTP GET /, 60s initial delay, 5s timeout, 30s period, 3 failure threshold
readiness_probeHTTP GET /, 30s initial delay, 5s timeout, 10s period, 3 failure threshold

storage_buckets

A list of GCS bucket configurations for provisioning by the platform module:

FieldValue
name_suffix"ghost-content"
locationDeployment region
storage_class"STANDARD"
versioning_enabledfalse
lifecycle_rules[]
public_access_prevention"inherited" (inherits project-level policy)

path

The absolute path to the module directory, used by wrapper modules to locate the scripts/ directory.


3. Input Variables

Application

VariableTypeDefaultDescription
application_namestring"ghost"Application name
application_versionstring"6.14.0"Ghost Docker image tag
descriptionstring"Initialize Ghost Database with MySQL 8.0 settings"Init job description
deployment_idstring""Unique deployment identifier
db_namestring"ghost"MySQL database name
db_userstring"ghost"MySQL application user
cpu_limitstring"2000m"Container CPU limit
memory_limitstring"4Gi"Container memory limit
environment_variablesmap(string){}Environment variables passed directly to the container. Note: wrapper modules (Ghost CloudRun, Ghost GKE) default this to SMTP stub values; Ghost Common itself defaults to an empty map.
initialization_jobslist(object)[]Custom init jobs; empty triggers the default db-init job
startup_probeobjectsee aboveStartup health probe
liveness_probeobjectsee aboveLiveness health probe
enable_image_mirroringboolfalseMirror the container image to Artifact Registry before deployment
min_instance_countnumber0Minimum number of running instances (0 enables scale-to-zero). Overridden to 0 by Ghost CloudRun and 1 by Ghost GKE via hardcoded locals.
max_instance_countnumber3Maximum number of running instances. Overridden to 5 by both Ghost CloudRun and Ghost GKE via hardcoded locals.
secret_environment_variablesmap(string){}Secret environment variables passed to the container

Storage & Volumes

VariableTypeDefaultDescription
enable_cloudsql_volumebooltrueMount Cloud SQL Auth Proxy sidecar socket
gcs_volumeslist(object)[]GCS Fuse volume mounts (name, bucket_name, mount_path, readonly, mount_options)
deployment_regionstring"us-central1"Region for the storage bucket

4. Health Probes

Ghost Common is the only *_Common module that defines all three probe types. All probes target GET / (the Ghost homepage, which returns 200 when the application is fully ready):

ProbeInitial DelayTimeoutPeriodFailure ThresholdPurpose
Startup90s10s10s10Allows up to 190s total for Ghost to complete its first-run database migrations and schema setup
Liveness60s5s30s3Restarts the container if Ghost becomes unresponsive
Readiness30s5s10s3Removes the instance from the load balancer while temporarily unhealthy

The generous startup probe thresholds accommodate Ghost's schema migration process on fresh databases, which can be slow on the first run.


5. Initialization Job

One db-init job runs by default (when initialization_jobs = []):

FieldValue
Imagemysql:8.0-debian
Scriptscripts/db-init.sh
Secrets requiredROOT_PASSWORD (MySQL root, optional), DB_PASSWORD (app user)
execute_on_applytrue
Timeout600s, 1 retry

db-init.sh behavior:

  1. Resolves the target host from DB_HOST (preferred — may carry 127.0.0.1 for the Auth Proxy) or falls back to DB_IP.
  2. Detects connection type: if DB_HOST starts with /, uses a Unix socket (-S); otherwise uses TCP (-h).
  3. Validates that DB_PASSWORD is set; warns and skips DB/user creation if ROOT_PASSWORD is absent (assumes the database already exists).
  4. Polls MySQL using the mysql client (up to 30 retries, 2s apart).
  5. When ROOT_PASSWORD is provided:
    • Creates the database with utf8mb4 charset and utf8mb4_0900_ai_ci collation (MySQL 8.0 default, required by Ghost 6.x).
    • Creates (or recreates) the application user with mysql_native_password authentication — deliberately chosen over caching_sha2_password for compatibility with the Node.js mysql2 driver, which requires RSA key exchange on first TCP connection with caching_sha2_password.
    • Grants ALL PRIVILEGES on the Ghost database plus explicit CREATE, ALTER, DROP, INDEX, REFERENCES for migrations.
  6. When ROOT_PASSWORD is absent: Skips creation and proceeds to verification.
  7. Verifies the application user can connect and queries database charset/collation/version info.
  8. Signals Cloud SQL Proxy shutdown via curl if available, otherwise falls back to raw bash /dev/tcp I/O (the mysql:8.0-debian image does not include curl or wget).

6. Scripts and Container Image

All supporting files are in scripts/. The scripts/ directory is used as the Docker build context.

Dockerfile

Wraps the public ghost:<version> image:

  • Installs curl, jq, and netcat-openbsd for the custom entrypoint.
  • Copies entrypoint.sh to /usr/local/bin/custom-entrypoint.sh.
  • Ensures /var/lib/ghost/content is owned by the node user.
  • Sets WORKDIR /var/lib/ghost.
  • Exposes port 2368.
  • Uses custom-entrypoint.sh as the entrypoint and node current/index.js as the default command.

entrypoint.sh

Runs before the original Ghost entrypoint to configure the runtime environment:

1. Service URL Resolution (three-step priority):

  1. Uses the url environment variable directly if set.
  2. Auto-detects the Cloud Run service URL via the GCE metadata server:
    • Fetches an access token from http://metadata.google.internal/....
    • Retrieves the project ID and region from metadata.
    • Calls the Cloud Run API v2 (https://run.googleapis.com/v2/projects/.../services/...) to get the service URI.
    • Exports both url and admin__url from the detected URI.
  3. Falls back to http://localhost:2368 when not on Cloud Run (i.e., when K_SERVICE is not set — including GKE deployments).

2. Database Variable Mapping: Translates standard platform env vars into Ghost's double-underscore config key syntax:

Platform env varGhost config key
DB_HOST (TCP)database__connection__host
DB_HOST (socket path starting with /)database__connection__socketPath
DB_IP (fallback)database__connection__host
DB_USERdatabase__connection__user
DB_NAMEdatabase__connection__database
DB_PASSWORDdatabase__connection__password
DB_PORTdatabase__connection__port

3. MySQL Configuration Validation: When database__client = "mysql":

  • Validates all required connection variables are present for TCP connections.
  • Waits for TCP reachability using nc (up to 30 attempts, 2s apart) for non-localhost hosts.
  • Skips wait for Unix socket connections (Cloud SQL Proxy creates the socket asynchronously).

4. Startup: Delegates to the original Ghost entrypoint (exec docker-entrypoint.sh "$@").


7. Ghost Configuration via Environment Variables

Ghost reads its configuration from environment variables using double-underscore notation. The entrypoint maps platform-injected variables, but wrapper modules can pass any Ghost config key directly via environment_variables:

environment_variables = {
"database__client" = "mysql"
"mail__transport" = "SMTP"
"mail__options__host" = "smtp.example.com"
"storage__active" = "gcs"
"imageOptimization__resize" = "true"
"privacy__useUpdateCheck" = "false"
}

8. Platform-Specific Differences

AspectGhost CloudRunGhost GKE
service_urlentrypoint.sh auto-detects from GCE metadata API (checks K_SERVICE env var)entrypoint.sh falls back to http://localhost:2368 — GKE pods do not set K_SERVICE, so the URL must be configured via a Ghost url environment variable
min_instance_count0 (scale-to-zero) — hardcoded in Ghost_CloudRun/main.tf, overrides Common default1 (always one pod running) — hardcoded in Ghost_GKE/main.tf, overrides Common default
max_instance_count5 — hardcoded in Ghost_CloudRun/main.tf, overrides Common default of 35 — hardcoded in Ghost_GKE/main.tf, overrides Common default of 3
enable_cloudsql_volumeOptional (default true)Optional (default true)
DB_HOSTCloud SQL Auth Proxy socket pathCloud SQL private IP
URL auto-detectionentrypoint.sh fetches service URL from GCP metadata API (Cloud Run sets K_SERVICE)entrypoint.sh falls back to http://localhost:2368 because GKE pods do not set K_SERVICE; set the url env var explicitly for GKE
NFSEnabled by default (enable_nfs = true) in Ghost CloudRunEnabled by default (enable_nfs = true) in Ghost GKE
RedisEnabled by default (enable_redis = true) in Ghost CloudRunEnabled by default (enable_redis = true) in Ghost GKE

9. Implementation Pattern

# Example: how Ghost_CloudRun instantiates Ghost_Common

module "ghost_app" {
source = "../Ghost_Common"

application_version = var.application_version
db_name = var.db_name
db_user = var.db_user
cpu_limit = var.cpu_limit
memory_limit = var.memory_limit
description = var.description
startup_probe = var.startup_probe
liveness_probe = var.liveness_probe
enable_cloudsql_volume = var.enable_cloudsql_volume
}

# The wrapper merges Ghost_Common config with module-level overrides,
# including injecting database__client = "mysql" at this layer (not in Ghost_Common).
locals {
ghost_module = merge(
module.ghost_app.config,
{
min_instance_count = 0
max_instance_count = 5
environment_variables = merge(
module.ghost_app.config.environment_variables,
{ "database__client" = "mysql" }
)
}
)
}

# config is passed to App_CloudRun via application_config
module "app_cloudrun" {
source = "../App_CloudRun"

application_config = { ghost = local.ghost_module }
module_storage_buckets = module.ghost_app.storage_buckets
scripts_dir = abspath("${module.ghost_app.path}/scripts")
# ... other inputs
}