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
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.3.8-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, andzip - Drupal 11.3 supports PHP 8.5, so the explicit PHP 8.5 tag is a better production pin than the older generic
apache-bookwormform bookwormkeeps the Debian base explicit and conservative for production environments
Official Product
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:
- Wait for Drupal and MySQL to become ready.
- Open the configured URL or port-forwarded service.
- Run the Drupal web installer.
- 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 undersites/- 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
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=truepersistence.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.
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 | Number of Drupal replicas |
image.repository | docker.io/library/drupal | Docker Official Drupal image repository |
image.tag | 11.3.8-php8.5-apache-bookworm | Pinned Drupal image tag |
image.pullPolicy | IfNotPresent | Container pull policy |
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.primary.persistence.enabled | true | Enable MySQL persistence |
mysql.primary.persistence.size | 8Gi | 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.prefix | drupal | Key prefix in the bucket |
backup.s3.existingSecret | "" | Existing secret for S3 credentials |
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 |
securityContext | {} | Container-level security context |
serviceAccount.create | false | Create a dedicated service account |
serviceAccount.name | "" | Existing or custom service account name |
serviceAccount.annotations | {} | Service account annotations |
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.