Skip to content

Drupal

Deploy Drupal on Kubernetes using the Docker Official drupal image with a persistence model tailored for the installer-managed sites/ directory, plus native backup automation and safe scaling guardrails.

Why This Chart Matters

  • Reference CMS chart — built to be HelmForge’s production baseline for installer-driven PHP CMS workloads
  • Seeded sites/ persistence — preserves uploads and installer-generated state without masking Drupal core files from the image
  • Built-in backup automation — archives Drupal sites/ plus either MySQL or SQLite to S3-compatible storage
  • Safe horizontal scaling — HPA is supported only when the storage and database combination is actually safe
  • Installer-first workflow — the chart prepares runtime, storage, ingress, and database connectivity, then guides setup through NOTES.txt
  • Real application listener — containerPorts.http changes both the Pod declaration and Apache on the official image
  • Secure defaults — Kubernetes API token automount is disabled, privilege escalation is blocked, and RuntimeDefault seccomp is enabled
  • Optional network isolation — ingress and egress policy can be tailored to the ingress controller, database, update, and mail paths

Image Decision

This chart uses docker.io/library/drupal because Drupal does not currently publish its own upstream-maintained runtime container image.

HelmForge pins:

  • docker.io/library/drupal:11.4.10-php8.5-apache-bookworm

That choice is intentional:

  • the Apache variant already includes PHP
  • the image includes the Drupal-relevant extensions such as pdo_mysql, pdo_sqlite, sqlite3, gd, mbstring, curl, dom, openssl, and zip
  • Drupal 11.4 supports PHP 8.5, so the explicit PHP 8.5 tag is a better production pin than the older generic apache-bookworm form
  • bookworm keeps the Debian base explicit and conservative for production environments

Official Product

Release Notes

Drupal 11.4.10 fixes SA-CORE-2026-014, SA-CORE-2026-015, and SA-CORE-2026-016; no other fixes are included. Review the official release notes.

The bundled HelmForge MySQL dependency is aligned to the current 2.4.0 chart while preserving the same Drupal installer values for the default bundled database path. Before upgrading production workloads, test the site in staging with the same PHP extensions, Composer dependency set, storage class, and database mode used in production. Review the Drupal 11.4.10 release notes before running database updates.

Architecture And Security Boundary

An optional Ingress routes to a Service whose named target resolves to the configurable Apache listener in the Drupal Deployment. Only /var/www/html/sites is persistent, so mutable site state survives while core continues to come from the pinned image. The database can be the bundled MySQL 2.4.0 subchart, an external MySQL-compatible service, or SQLite for one replica.

The pod does not receive a Kubernetes API token by default, uses RuntimeDefault seccomp, and blocks privilege escalation. The Docker Official Apache image still starts its master process as root to bind port 80 before delegating requests to unprivileged workers; the chart therefore does not claim a fully non-root or read-only-root-filesystem boundary. NetworkPolicy support is opt-in and requires a CNI that enforces it.

Installation

HTTPS Repository

helm repo add helmforge https://repo.helmforge.dev
helm repo update
helm install drupal helmforge/drupal

OCI Registry

helm install drupal oci://ghcr.io/helmforgedev/helm/drupal

Install Example

resources:
  requests:
    cpu: 250m
    memory: 512Mi
  limits:
    cpu: '1'
    memory: 1Gi

mysql:
  enabled: true
  auth:
    database: drupal
    username: drupal

persistence:
  enabled: true
  size: 8Gi

ingress:
  enabled: true
  ingressClassName: traefik
  hosts:
    - host: drupal.example.com
      paths:
        - path: /
          pathType: Prefix

After installation:

  1. Wait for Drupal and MySQL to become ready.
  2. Open the configured URL or port-forwarded service.
  3. Run the Drupal web installer.
  4. Use the database details printed by helm get notes <release>.

Database Modes

Bundled MySQL

database:
  mode: auto

mysql:
  enabled: true

External Database

database:
  mode: external
  external:
    host: db.example.com
    port: 3306
    name: drupal
    username: drupal

mysql:
  enabled: false

SQLite

database:
  mode: sqlite

mysql:
  enabled: false

For SQLite installs, use sites/default/files/.ht.sqlite in the Drupal installer unless you intentionally choose another writable path.

SQLite is supported and backed up by the chart, but it remains a single-replica path and is not the preferred default for most production Drupal sites.

Persistence

This chart persists only /var/www/html/sites.

That layout is intentional:

  • settings.php, uploaded files, and installer output live under sites/
  • Drupal core remains in the container image instead of being shadowed by a PVC
  • an init container seeds the initial sites/ directory into the mounted volume before Drupal starts

Single-replica deployments are the default recommendation. If you scale above one replica, use shared writable storage for sites/.

Backup Strategy

When backup.enabled=true, the chart creates a CronJob that always captures two things:

  • the Drupal sites/ tree
  • the active database

Database backup behavior depends on the selected mode:

  • MySQL or external MySQL-compatible database — mysqldump
  • SQLite — a consistent SQLite snapshot created through Python’s SQLite backup API before compression

Backup Example

backup:
  enabled: true
  schedule: '0 2 * * *'
  s3:
    endpoint: https://minio.example.com
    bucket: drupal-backups
    existingSecret: drupal-backup-s3

For IRSA or another compatible projected credential provider, enable workload identity instead of storing S3 keys in a Secret. This annotation example configures IRSA:

backup:
  enabled: true
  s3:
    endpoint: https://s3.us-east-1.amazonaws.com
    bucket: drupal-backups
    region: us-east-1
    credentialsFromPodIdentity: true
  serviceAccount:
    annotations:
      eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/drupal-backup

EKS Pod Identity does not use a ServiceAccount annotation. Give the dedicated backup ServiceAccount a stable name and create the association in EKS instead:

backup:
  enabled: true
  s3:
    endpoint: https://s3.us-east-1.amazonaws.com
    bucket: drupal-backups
    region: us-east-1
    credentialsFromPodIdentity: true
  serviceAccount:
    name: drupal-backup
aws eks create-pod-identity-association \
  --cluster-name <cluster-name> \
  --namespace <release-namespace> \
  --service-account drupal-backup \
  --role-arn arn:aws:iam::123456789012:role/drupal-backup

This mode uses a dedicated backup ServiceAccount and does not render static S3 credentials. Grant it only bucket-list and object-write access for the selected prefix, plus bucket creation only when that option remains enabled. If egress NetworkPolicy is enabled, allow DNS, HTTPS, and any identity-provider metadata endpoint required by the cluster. Token automount remains disabled unless the provider explicitly needs it; metadata address-family routing remains a cluster responsibility rather than a hardcoded chart address.

If database.mode=external, also provide backup credentials for the dump job:

backup:
  database:
    existingSecret: drupal-backup-db

Autoscaling And HPA

HPA is supported, but only when the chart can guarantee safe shared state.

Required for multi-replica or HPA Drupal:

  • persistence.enabled=true
  • persistence.accessMode=ReadWriteMany
  • a MySQL-compatible database mode

If those requirements are not met, the chart fails during render instead of letting an unsafe deployment reach the cluster.

When HPA is enabled, the Deployment omits spec.replicas and leaves the replica count exclusively under HPA control. This prevents routine Helm upgrades from resetting a scaled workload to a chart value. The first upgrade from an older release that rendered spec.replicas can reduce the Deployment to one replica before the HPA reconciles. A fresh installation with minReplicas > 1 can also start at one replica until that first reconciliation. Plan for reduced capacity or redundancy during this transition.

HPA Example

persistence:
  accessMode: ReadWriteMany

autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 5

pdb:
  enabled: true
  minAvailable: 1

Values

The chart still has an installer-oriented surface, but it now covers production runtime, storage, backups, scaling, networking, probes, and escape hatches.

Runtime And Installer Values

Key Default Description
replicaCount 1 Fixed Drupal replicas when HPA is disabled
image.repository docker.io/library/drupal Docker Official Drupal image repository
image.tag 11.4.10-php8.5-apache-bookworm Pinned Drupal image tag
image.pullPolicy IfNotPresent Container pull policy
image.configureApachePort null Auto-configure a non-default Apache port for the official image
containerPorts.http 80 Actual Apache listener and Pod container port
imagePullSecrets [] Optional image pull secrets
drupal.siteName Drupal Suggested site name shown in install guidance
drupal.installProfile standard Suggested Drupal install profile
drupal.locale en Suggested locale for the installer
drupal.sqlitePath sites/default/files/.ht.sqlite Suggested SQLite path for the web installer
drupal.extraEnv [] Extra container environment variables
php.ini "" Extra PHP configuration mounted as custom.ini

Database And Persistence Values

Key Default Description
database.mode auto Database mode: auto, external, mysql, or sqlite
database.external.host "" External database hostname
database.external.port 3306 External database port
database.external.name drupal External database name
database.external.username drupal External database username
mysql.enabled true Enable the bundled MySQL subchart
mysql.architecture standalone MySQL deployment architecture
mysql.auth.database drupal Database name created by the MySQL subchart
mysql.auth.username drupal Database username created by the MySQL subchart
mysql.auth.password "" Optional application password override
mysql.auth.rootPassword "" Optional root password override
mysql.auth.existingSecret "" Existing secret for MySQL passwords
mysql.auth.existingSecretUserPasswordKey mysql-user-password Secret key used for the app user password
mysql.standalone.persistence.enabled true Enable bundled standalone MySQL persistence
mysql.standalone.persistence.size 8Gi Bundled standalone MySQL PVC size
persistence.enabled true Enable persistence for /var/www/html/sites
persistence.storageClass "" Explicit storage class for the sites PVC
persistence.accessMode ReadWriteOnce Access mode for the sites PVC
persistence.size 8Gi Sites PVC size
persistence.existingClaim "" Reuse an existing sites PVC
persistence.annotations {} Annotations applied to the sites PVC

Backup, Scaling, And Availability Values

Key Default Description
backup.enabled false Enable scheduled backups
backup.schedule 0 3 * * * Cron schedule for backups
backup.s3.endpoint "" S3-compatible endpoint
backup.s3.bucket "" S3 target bucket
backup.s3.region "" Region required for workload identity
backup.s3.credentialsFromPodIdentity false Use workload identity instead of static S3 keys
backup.s3.prefix drupal Key prefix in the bucket
backup.s3.existingSecret "" Existing secret for S3 credentials
backup.serviceAccount.create true Create a dedicated backup ServiceAccount
backup.database.host "" Optional database host override for backup
backup.database.password "" Inline database password override for backup
backup.database.existingSecret "" Existing secret for backup database password
autoscaling.enabled false Enable HPA
autoscaling.minReplicas 2 HPA minimum replicas
autoscaling.maxReplicas 5 HPA maximum replicas
autoscaling.targetCPUUtilizationPercentage 75 CPU utilization target
autoscaling.targetMemoryUtilizationPercentage 80 Memory utilization target
pdb.enabled false Create a PodDisruptionBudget
pdb.minAvailable 1 Minimum healthy pods during voluntary disruption

Service, Ingress, Probes, And Platform Values

Key Default Description
service.type ClusterIP Drupal service type
service.port 80 Drupal service port
service.annotations {} Annotations applied to the service
service.ipFamilyPolicy omitted Service IP family policy: SingleStack, PreferDualStack, or RequireDualStack. Omit for cluster default.
service.ipFamilies omitted Ordered list of IP families (IPv4, IPv6). Omit for cluster default.
ingress.enabled false Enable ingress resources
ingress.ingressClassName "" Ingress class name
ingress.annotations {} Ingress annotations
ingress.hosts [] Ingress host and path definitions
ingress.tls [] TLS secret mappings for ingress
startupProbe.enabled true Enable the startup probe
startupProbe.path / Startup probe HTTP path
livenessProbe.enabled true Enable the liveness probe
livenessProbe.path / Liveness probe HTTP path
readinessProbe.enabled true Enable the readiness probe
readinessProbe.path / Readiness probe HTTP path
resources.requests.cpu 250m Default CPU request for the Drupal container
resources.requests.memory 512Mi Default memory request for the Drupal container
resources.limits.cpu 1 Default CPU limit for the Drupal container
resources.limits.memory 1Gi Default memory limit for the Drupal container
podSecurityContext.fsGroup 33 Group ownership applied to the mounted Drupal files volume
podSecurityContext.seccompProfile.type RuntimeDefault Pod seccomp profile
securityContext.allowPrivilegeEscalation false Prevent the container from gaining privileges
serviceAccount.create false Create a dedicated service account
serviceAccount.name "" Existing or custom service account name
serviceAccount.annotations {} Service account annotations
serviceAccount.automountServiceAccountToken false Mount a Kubernetes API token in Drupal pods
networkPolicy.enabled false Create ingress isolation for Drupal pods
networkPolicy.egress.enabled false Enable explicit egress rules
nodeSelector {} Node selector rules
tolerations [] Pod tolerations
affinity {} Pod affinity and anti-affinity
topologySpreadConstraints [] Topology spread constraints
priorityClassName "" Priority class for the pod
terminationGracePeriodSeconds 30 Pod termination grace period
podLabels {} Extra pod labels
podAnnotations {} Extra pod annotations
extraVolumes [] Additional volumes injected into the pod
extraVolumeMounts [] Additional volume mounts injected into the Drupal container
extraManifests [] Extra Kubernetes manifests rendered by the chart

Dual-stack Networking

The Drupal Service accepts Kubernetes dual-stack configuration. By default, both service.ipFamilyPolicy and service.ipFamilies are unset and the chart inherits whatever the cluster advertises (matching prior behavior).

service:
  ipFamilyPolicy: PreferDualStack
  ipFamilies:
    - IPv4
    - IPv6

PreferDualStack is the safer choice for clusters that may be single- or dual-stack: omit ipFamilies and the cluster auto-populates whatever it supports. Set ipFamilies explicitly only when the cluster is configured for both families — the Kubernetes API rejects an explicit family the cluster does not advertise. RequireDualStack enforces both.

Application Port And NetworkPolicy

service.port is client-facing. containerPorts.http changes the actual Apache listener and remains the target of the named Service port:

containerPorts:
  http: 8080
service:
  port: 80

The official image is configured automatically. A custom image must use the same /etc/apache2 layout and explicitly set image.configureApachePort=true, otherwise the chart rejects a non-default listener.

NetworkPolicy is disabled by default to avoid silently blocking Drupal updates, mail, databases, or integrations. When enabled, declare the actual ingress controller and every egress path required by the site:

networkPolicy:
  enabled: true
  ingress:
    allowSameNamespace: false
    extraFrom:
      - namespaceSelector:
          matchLabels:
            kubernetes.io/metadata.name: ingress-nginx
        podSelector:
          matchLabels:
            app.kubernetes.io/name: ingress-nginx
            app.kubernetes.io/component: controller
  egress:
    enabled: true
    allowDNS: true
    allowSameNamespaceDatabase: true
    allowHTTPS: true

Troubleshooting

Use kubectl logs deploy/<release>-drupal -c drupal for Apache and PHP output. For a non-default port, inspect the one-time configuration step with kubectl logs deploy/<release>-drupal -c configure-apache-port and confirm the live virtual host with kubectl exec deploy/<release>-drupal -c drupal -- apache2ctl -S. If isolation blocks traffic, inspect the rendered policy and verify its namespace and pod selectors against the ingress controller and database placement.

More Information