Ghost
Modern open-source publishing platform for blogs, newsletters, and memberships. Ghost provides a polished writing editor, a built-in membership and subscription system, email newsletter delivery, and a headless CMS API — all backed by MySQL.
Key Features
- Modern editor — clean, distraction-free writing experience with Markdown and rich cards
- Memberships and newsletters — built-in subscription tiers and email newsletter delivery
- Headless CMS API — Content API and Admin API for frontend frameworks and integrations
- MySQL backend — bundled subchart or external MySQL database
- Content persistence — PVC-backed content directory for media, themes, routes, and redirects
- S3 content backup — scheduled archive of the Ghost content directory to S3-compatible storage
- Ingress support — TLS via cert-manager with configurable ingress class
- Hardened runtime — non-root, read-only root filesystem, dropped capabilities, seccomp, and no API token mount
- Operational policies — optional disruption and network policies with explicit database egress
- Configurable listener — one value configures Ghost, probes, the Service target, and network policy
Ghost supports adapters installed outside the content directory. Keep custom adapters in the image rather than the content PVC so application code and user-managed content retain separate lifecycles.
Ghost 6.71.0 adds automation analytics, self-service site exports, member location maps and refreshed post and member screens. It also includes fixes across scheduled posts, member imports, Portal, uploads, the editor, social previews and analytics links. Back up content and the database, then validate themes, integrations, newsletters, comments and member signup paths before upgrading production sites.
Upgrading a release with bundled MySQL from chart 2.1.x to 2.2.0 changes the MySQL StatefulSet configuration checksum. Kubernetes restarts the MySQL pod once as a result. Because the default standalone topology has one replica, schedule a maintenance window, back up the database, and wait for the MySQL StatefulSet rollout before validating Ghost.
Architecture and Boundaries
The chart runs one Ghost Deployment with a persistent content volume and either the HelmForge MySQL 2.2.0 dependency or an external MySQL-compatible service. A ClusterIP Service exposes the named HTTP port to an optional Ingress or Gateway API HTTPRoute. Ghost remains a singleton: its PodDisruptionBudget can protect voluntary maintenance, but it does not provide application high availability.
The default runtime is non-root and read-only, has no Kubernetes API token, and limits writable storage to content and bounded temporary volumes. Credentials come from Kubernetes Secrets. Optional NetworkPolicy can restrict ingress and egress, while TLS termination, database availability, SMTP delivery, backup retention, restore testing and platform observability remain operator responsibilities.
The configurable listener, hardened runtime, bounded ephemeral storage, PodDisruptionBudget and NetworkPolicy documented on this page require Ghost chart 1.3.0 or later. Publish the companion chart release before publishing this documentation update.
Installation
HTTPS repository:
helm repo add helmforge https://repo.helmforge.dev
helm repo update
helm install ghost helmforge/ghost
OCI registry:
helm install ghost oci://ghcr.io/helmforgedev/helm/ghost
Deployment Examples
# values.yaml — Ghost with bundled MySQL (default)
ghost:
url: 'https://blog.example.com'
mysql:
enabled: true
auth:
password: 'mysql-password'
persistence:
enabled: true
size: 10Gi
ingress:
enabled: true
ingressClassName: traefik
hosts:
- host: blog.example.com
paths:
- path: /
pathType: Prefix# values.yaml — Ghost with an existing MySQL instance
ghost:
url: 'https://blog.example.com'
mysql:
enabled: false
database:
external:
host: mysql.database.svc
port: '3306'
name: ghost
username: ghost
password: 'db-password'
persistence:
enabled: true
size: 10Gi
ingress:
enabled: true
ingressClassName: traefik
hosts:
- host: blog.example.com
paths:
- path: /
pathType: Prefix# values.yaml — external MySQL password synchronized by External Secrets Operator
ghost:
url: 'https://blog.example.com'
mysql:
enabled: false
database:
external:
host: mysql.database.svc
port: '3306'
name: ghost
username: ghost
existingSecret: ghost-db-credentials
externalSecrets:
enabled: true
items:
- fullnameOverride: ghost-db-credentials
spec:
secretStoreRef:
name: platform-secrets
kind: ClusterSecretStore
data:
- secretKey: password
remoteRef:
key: ghost/database
property: password# values.yaml — Ghost with SMTP for newsletter delivery
ghost:
url: 'https://blog.example.com'
extraEnv:
- name: mail__transport
value: SMTP
- name: mail__options__host
value: smtp.example.com
- name: mail__options__port
value: '587'
- name: mail__options__auth__user
value: [email protected]
- name: mail__options__auth__pass
valueFrom:
secretKeyRef:
name: ghost-smtp-secret
key: password
- name: mail__from
value: '"My Blog" <[email protected]>'
mysql:
enabled: true
auth:
password: 'mysql-password'
persistence:
enabled: true
size: 10Gi
ingress:
enabled: true
ingressClassName: traefik
hosts:
- host: blog.example.com
paths:
- path: /
pathType: Prefix# values.yaml — Daily backup of Ghost content directory to S3
# NOTE: The backup archives the content PVC (images, media, themes).
# MySQL data is NOT included — back up the database separately.
ghost:
url: 'https://blog.example.com'
mysql:
enabled: true
auth:
password: 'mysql-password'
persistence:
enabled: true
size: 10Gi
backup:
enabled: true
schedule: '0 3 * * *'
s3:
endpoint: https://s3.amazonaws.com
bucket: my-ghost-backups
accessKey: '<set-me>'
secretKey: '<set-me>'# values.yaml — custom Ghost image with adapters outside the content PVC
image:
repository: registry.example.com/ghost-with-adapters
tag: '6.71.0'
ghost:
url: 'https://blog.example.com'
extraEnv:
- name: paths__installedAdaptersPath
value: /opt/ghost/adaptersBuild /opt/ghost/adapters into the custom image. Keeping installed adapters
outside /var/lib/ghost/content prevents the content PVC from shadowing them
when the container starts.
Configuration Reference
Core
| Parameter | Type | Default | Description |
|---|---|---|---|
nameOverride |
string | "" |
Override the chart name. |
fullnameOverride |
string | "" |
Override the full release name. |
commonLabels |
object | {} |
Extra labels added to all resources. |
Image
| Parameter | Type | Default | Description |
|---|---|---|---|
image.repository |
string | docker.io/library/ghost |
Ghost container image. |
image.tag |
string | "6.71.0" |
Image tag. |
image.pullPolicy |
string | IfNotPresent |
Image pull policy. |
imagePullSecrets |
array | [] |
Pull secrets for private registries. |
Ghost Configuration
| Parameter | Type | Default | Description |
|---|---|---|---|
ghost.url |
string | "" |
Public URL of the Ghost instance (e.g. https://blog.example.com). |
ghost.extraEnv |
array | [] |
Extra environment variables for SMTP, integrations, and advanced settings. |
| Parameter | Type | Default | Description |
|---|---|---|---|
containerPorts.http |
integer | 2368 |
Ghost process listener, container port and Service target endpoint. |
Ghost uses ghost.url to generate all links: newsletter emails, canonical URLs, webhook payloads, and API responses.
If this value does not match the URL your visitors use, email links will be broken, SEO canonical tags will be wrong,
and the Admin UI may fail to redirect correctly.
Ghost only supports MySQL (or MariaDB) as its database engine. This chart does not support PostgreSQL. Use the bundled
MySQL subchart or provide an external MySQL instance via database.external.
Ghost starts without SMTP configured, but newsletter sending and member invitation emails will fail silently.
Configure SMTP via ghost.extraEnv using the mail__* environment variable convention shown in the “With Email
(SMTP)” example above.
Ghost accepts paths__installedAdaptersPath for custom container images. Set it through ghost.extraEnv to the
directory baked into the image. Do not place that directory under /var/lib/ghost/content, because the content PVC
replaces that path at runtime.
Database — Embedded Subchart
| Parameter | Type | Default | Description |
|---|---|---|---|
mysql.enabled |
boolean | true |
Deploy a bundled MySQL subchart for Ghost. |
mysql.architecture |
string | standalone |
MySQL deployment architecture. |
mysql.auth.database |
string | ghost |
Database name created by the subchart. |
mysql.auth.username |
string | ghost |
Database username created by the subchart. |
mysql.auth.password |
string | "" |
Database password (auto-generated if empty). |
Database — External
| Parameter | Type | Default | Description |
|---|---|---|---|
database.external.host |
string | "" |
External MySQL hostname or IP. |
database.external.port |
string | "3306" |
External MySQL port. |
database.external.name |
string | ghost |
Database name on the external server. |
database.external.username |
string | ghost |
Username for the external database. |
database.external.password |
string | "" |
Password for the external database (plain text — prefer secret). |
database.external.existingSecret |
string | "" |
Existing secret containing the database password. |
database.external.existingSecretPasswordKey |
string | password |
Key inside the existing secret for the password. |
External Secrets
Install External Secrets Operator separately before enabling this integration.
The chart renders external-secrets.io/v1 resources and supports multiple complete definitions through
externalSecrets.items. Each item must set name or fullnameOverride so every resource has a unique identity.
| Parameter | Type | Default | Description |
|---|---|---|---|
externalSecrets.enabled |
boolean | false |
Render ExternalSecret resources. |
externalSecrets.refreshInterval |
string | 1h |
Default sync interval for items without spec.refreshInterval. |
externalSecrets.items |
array | [] |
Complete ExternalSecret definitions with metadata and spec. |
Set database.external.existingSecret to the same name rendered by the database ExternalSecret. Using
items[].fullnameOverride provides a stable literal name. The matching item must project the configured password key
through spec.data, or use spec.dataFrom. The legacy secretStoreRef and data fields remain compatible for
existing releases, but new configurations should use externalSecrets.items.
Content Persistence
| Parameter | Type | Default | Description |
|---|---|---|---|
persistence.enabled |
boolean | true |
Enable a PVC for /var/lib/ghost/content (media, themes, routes, redirects). |
persistence.size |
string | 10Gi |
PVC size. |
persistence.storageClass |
string | "" |
StorageClass for the PVC. |
persistence.accessModes |
array | ["ReadWriteOnce"] |
PVC access modes. |
persistence.existingClaim |
string | "" |
Use an existing PVC instead of creating one. |
persistence.emptyDirSizeLimit |
string | 1Gi |
Maximum ephemeral content storage when persistence is disabled. |
Backup
The S3 backup CronJob archives the Ghost content directory (/var/lib/ghost/content) — uploaded images, media files,
custom themes, routes.yaml, and redirects.yaml. It does not back up the MySQL database, which contains posts,
members, settings, and newsletter data. Configure a separate MySQL backup strategy (e.g. the HelmForge MySQL chart
backup feature) to protect the full Ghost dataset.
| Parameter | Type | Default | Description |
|---|---|---|---|
backup.enabled |
boolean | false |
Enable scheduled S3 content backup CronJob. |
backup.schedule |
string | "0 3 * * *" |
Cron schedule for backups. |
backup.suspend |
boolean | false |
Suspend the CronJob without deleting it. |
backup.concurrencyPolicy |
string | Forbid |
CronJob concurrency policy. |
backup.successfulJobsHistoryLimit |
integer | 3 |
Number of successful Job records to keep. |
backup.failedJobsHistoryLimit |
integer | 3 |
Number of failed Job records to keep. |
backup.backoffLimit |
integer | 1 |
Job retry limit. |
backup.archivePrefix |
string | ghost |
Prefix for backup archive filenames. |
backup.images.backup |
string | docker.io/library/busybox:1.37 |
Image used for tar archive. |
backup.images.uploader |
string | docker.io/helmforge/mc:1.0.0 |
Image used for S3 upload. |
backup.resources |
object | {} |
Resources for backup containers. |
backup.scratchSizeLimit |
string | 2Gi |
Maximum ephemeral archive size per backup Job. |
backup.s3.endpoint |
string | "" |
S3-compatible endpoint URL. |
backup.s3.bucket |
string | "" |
Target bucket name. |
backup.s3.prefix |
string | ghost |
Key prefix within the bucket. |
backup.s3.createBucketIfNotExists |
boolean | true |
Create the bucket automatically if it does not exist. |
backup.s3.existingSecret |
string | "" |
Existing secret containing S3 access and secret keys. |
backup.s3.existingSecretAccessKeyKey |
string | access-key |
Key in the existing secret for the S3 access key. |
backup.s3.existingSecretSecretKeyKey |
string | secret-key |
Key in the existing secret for the S3 secret key. |
backup.s3.accessKey |
string | "" |
Inline S3 access key (ignored when existingSecret is set). |
backup.s3.secretKey |
string | "" |
Inline S3 secret key (ignored when existingSecret is set). |
Service
| Parameter | Type | Default | Description |
|---|---|---|---|
service.type |
string | ClusterIP |
Kubernetes service type. |
service.port |
integer | 80 |
Service port exposed to the cluster. |
service.annotations |
object | {} |
Annotations for the Service. |
Ingress
| Parameter | Type | Default | Description |
|---|---|---|---|
ingress.enabled |
boolean | false |
Enable an Ingress resource. |
ingress.ingressClassName |
string | traefik |
Ingress class name. |
ingress.annotations |
object | {} |
Annotations for the Ingress (e.g. cert-manager). |
ingress.hosts |
array | [] |
Ingress host and path rules. |
ingress.tls |
array | [] |
TLS configuration (secret name and hosts). |
Probes
Probes use the Ghost Admin API endpoint /ghost/api/admin/site/ to verify the application is ready.
| Parameter | Type | Default | Description |
|---|---|---|---|
probes.startup.enabled |
boolean | true |
Enable startup probe. |
probes.startup.initialDelaySeconds |
integer | 15 |
Startup probe initial delay. |
probes.startup.periodSeconds |
integer | 5 |
Startup probe period. |
probes.startup.timeoutSeconds |
integer | 3 |
Startup probe timeout. |
probes.startup.failureThreshold |
integer | 30 |
Startup probe failure threshold. |
probes.liveness.enabled |
boolean | true |
Enable liveness probe. |
probes.liveness.initialDelaySeconds |
integer | 0 |
Liveness probe initial delay. |
probes.liveness.periodSeconds |
integer | 15 |
Liveness probe period. |
probes.liveness.timeoutSeconds |
integer | 5 |
Liveness probe timeout. |
probes.liveness.failureThreshold |
integer | 3 |
Liveness probe failure threshold. |
probes.readiness.enabled |
boolean | true |
Enable readiness probe. |
probes.readiness.initialDelaySeconds |
integer | 0 |
Readiness probe initial delay. |
probes.readiness.periodSeconds |
integer | 10 |
Readiness probe period. |
probes.readiness.timeoutSeconds |
integer | 5 |
Readiness probe timeout. |
probes.readiness.failureThreshold |
integer | 3 |
Readiness probe failure threshold. |
Resources and Security
| Parameter | Type | Default | Description |
|---|---|---|---|
resources |
object | configured | Requests: 100m CPU/256Mi; limits: 500m CPU/768Mi. |
podSecurityContext |
object | hardened | Non-root UID/GID 1000 with RuntimeDefault seccomp. |
securityContext |
object | hardened | Read-only root, no escalation, and all caps dropped. |
runtimeStorage.tmpSizeLimit |
string | 256Mi |
Maximum writable /tmp storage. |
Service Account
| Parameter | Type | Default | Description |
|---|---|---|---|
serviceAccount.create |
boolean | false |
Create a dedicated ServiceAccount. |
serviceAccount.name |
string | "" |
Override the ServiceAccount name. |
serviceAccount.annotations |
object | {} |
Annotations for the ServiceAccount. |
serviceAccount.automountServiceAccountToken |
boolean | false |
Mount the Kubernetes API token into Ghost and backup pods. |
Scheduling
| Parameter | Type | Default | Description |
|---|---|---|---|
nodeSelector |
object | {} |
Node selector for scheduling. |
tolerations |
array | [] |
Tolerations for scheduling. |
affinity |
object | {} |
Affinity rules. |
topologySpreadConstraints |
array | [] |
Topology spread constraints. |
priorityClassName |
string | "" |
PriorityClass for the pod. |
terminationGracePeriodSeconds |
integer | 30 |
Termination grace period. |
podLabels |
object | {} |
Extra labels for the pod. |
podAnnotations |
object | {} |
Extra annotations for the pod. |
Availability and Network Policy
| Parameter | Type | Default | Description |
|---|---|---|---|
podDisruptionBudget.enabled |
boolean | false |
Protect the singleton pod during voluntary disruption. |
podDisruptionBudget.minAvailable |
integer | 1 |
Minimum available server pods. |
podDisruptionBudget.maxUnavailable |
integer | null |
Alternative disruption limit; clear minAvailable when used. |
networkPolicy.enabled |
boolean | false |
Restrict inbound traffic to the Ghost HTTP port. |
networkPolicy.ingressFrom |
array | [] |
Allowed ingress peers; empty permits all peers on the HTTP port. |
networkPolicy.egress.enabled |
boolean | false |
Isolate egress in addition to ingress. |
networkPolicy.egress.allowDNS |
boolean | true |
Permit DNS while egress is isolated. |
networkPolicy.egress.externalDatabaseCIDR |
string | "" |
Required database CIDR for isolated external database mode. |
networkPolicy.egress.extraEgress |
array | [] |
Additional complete egress rules for SMTP, S3 and integrations. |
Extra
| Parameter | Type | Default | Description |
|---|---|---|---|
extraVolumes |
array | [] |
Extra volumes to attach to the pod. |
extraVolumeMounts |
array | [] |
Extra volume mounts for the container. |
extraManifests |
array | [] |
Extra Kubernetes manifests deployed alongside the chart. |
Common Issues
Verify the database Secret, DNS name and port. If egress isolation is enabled, bundled MySQL is allowed by pod labels;
external MySQL requires networkPolicy.egress.externalDatabaseCIDR.
Set containerPorts.http instead of injecting server__port through ghost.extraEnv. The chart reserves that
environment variable so the real Ghost listener, probes, Service target and NetworkPolicy cannot diverge.
Ghost starts successfully without SMTP configured, but no emails will be delivered. If member invitations, newsletter
sends, or password reset emails are missing, configure the mail__* environment variables via ghost.extraEnv. Check
the Ghost logs (kubectl logs) for SMTP connection errors.
The Ghost Admin panel is available at https://blog.example.com/ghost/. On first deployment, Ghost presents a setup
wizard to create the admin account and configure the publication name and description.