Reusable Helm chart for running a Drupal application on Kubernetes. Packages the PHP-FPM + Nginx pair plus optional cron, Xdebug and Blackfire, so consuming projects only supply their own values.
Published to https://favish.github.io/helm-drupal and consumed as a chart
dependency by favish Drupal projects (kitco-cms-drupal, zerohedge-d8, …).
Shared chart: changes must stay backwards compatible — new behaviour ships
behind default-off values.
- Components
- Layout
- Usage
- Key values
- cron-worker
- Releasing
- Local checks
- Conventions (agents & contributors)
- Pending improvements
| Component | Template | Purpose |
|---|---|---|
| php-fpm Deployment (+HPA) | drupal/templates/php/ |
Runs Drupal via PHP-FPM; autoscales on CPU/memory. |
| nginx Deployment (+HPA) | drupal/templates/nginx/ |
Serves static assets, proxies FastCGI to php-fpm. |
| cron-worker Deployment | drupal/templates/cron-worker/ |
Optional (default-off). Runs drush cron on its own pod, no HPA. |
| Ingress + TLS | drupal/templates/ingress.yaml, tls-secret.yaml |
External routing + certificate. |
| Xdebug StatefulSet | drupal/templates/xdebug/ |
Optional step-debugger. |
| Shared env / init container | _drupal-env.tpl, _drupal-initContainer.tpl |
DB/S3/Blackfire env + the init container that loads app code into the pod. |
php and cron-worker share the same image and init container, so drush runs
against the real codebase and database.
drupal/ # the chart
├── Chart.yaml # metadata (version injected at release time)
├── values.yaml # defaults, documented inline
└── templates/
├── php/ nginx/ # deployment, service, hpa, configmap
├── cron-worker/ # optional drush-cron deployment
├── xdebug/ # optional debugger
├── ingress.yaml tls-secret.yaml
├── _drupal-env.tpl # shared env (DB, S3, Blackfire)
└── _drupal-initContainer.tpl
.github/workflows/release.yml # tag -> publish chart
CHANGELOG.md
Declare the dependency in the consuming umbrella chart:
dependencies:
- name: drupal
repository: https://favish.github.io/helm-drupal
version: 4.3.0helm dependency update ./your-umbrella-chartMinimal values:
drupal:
db: { database: my_db, host: 10.0.0.1, user: my_user, pass: "secret" }
fqdn: cms.example.com
php:
image: favish/php-fpm:4.0.0
autoscaling: { enabled: true, minReplicas: 3, targetCPUUtilizationPercentage: 80 }
initContainer:
image: us.gcr.io/your-project/your-app # tag usually set to the git SHA at deploy time| Value | Default | Meaning |
|---|---|---|
php.image |
favish/php-fpm:3.0.5 |
PHP-FPM image (override per project). |
php.autoscaling.enabled |
false |
php HPA on/off. |
php.autoscaling.targetCPUUtilizationPercentage |
80 |
HPA CPU target. |
php.livenessProbe / php.readinessProbe |
{} (off) |
Optional php-fpm probes. |
php.cronWorker.enabled |
false |
Dedicated cron pod (see below). |
initContainer.image / .tag |
— | Image that loads Drupal code into the pod. |
db.* |
— | MySQL connection. |
ingress.hosts / ingress.tls.enabled |
— | Routing + TLS. |
Full commented reference: drupal/values.yaml.
URL-triggered Drupal cron (/cron/<token>) lands on the serving php pods; a
heavy run (search indexing, feed imports, purges) spikes their CPU and churns the
autoscaler. php.cronWorker.enabled=true instead runs drush cron on a separate
no-HPA Deployment, keeping cron load off the serving fleet.
php:
cronWorker:
enabled: true
period: 60 # seconds between runs
uri: "https://cms.example.com" # optional --uri for URL-building cron tasks
resources:
requests: { cpu: 250m, memory: 512Mi }
limits: { cpu: "2", memory: 1500Mi }Reuses the init container + php.image; no extra image required. Default-off.
GitHub Actions (.github/workflows/release.yml). Merge to master, update
CHANGELOG.md, then tag:
git tag 4.4.0 && git push origin 4.4.0The workflow packages the chart (stamping the tag as the chart version) and
publishes it to the gh-pages Helm repo. workflow_dispatch runs a dry-run
(package + index, no push).
Migrated from CircleCI (its checkout key lost repo access — Permission denied (publickey)); actions/checkout uses the built-in GITHUB_TOKEN, no deploy key.
helm lint ./drupal
# Chart.yaml has no version until release; stamp a temp one to render
cp drupal/Chart.yaml /tmp/c.bak
printf '\nversion: 0.0.0-test\n' >> drupal/Chart.yaml
helm template t ./drupal --set initContainer.image=example --set php.cronWorker.enabled=true
cp /tmp/c.bak drupal/Chart.yaml- Backwards compatibility is mandatory; new behaviour goes behind a default-off /
empty value. Verify with
helm templatebefore and after. - Chart
versionis injected from the git tag at release; it is not inChart.yaml. Local renders need a temporary version stamp (above). - Helm replaces lists wholesale (no merge). Consumers overriding
resources/envfully replace defaults — account for this when changing defaults. - Release = push a git tag. No manual gh-pages step.
Identified in audit, left unchanged to protect consumers; tracked for a coordinated update:
imagePullPolicyunset on app containers;blackfire/blackfiresidecar untagged (defaultslatest). Pin + set explicitly.- HPA renders an empty
metrics:block if autoscaling is enabled with all targets nulled. Guard it or default a metric. ingress.yamluses the deprecatedkubernetes.io/ingress.classannotation →spec.ingressClassName.tls-secret.yamlexpects pre-base64'd input → considerstringData:.- Pod annotations use dotted
prometheus.io.scrapekeys → conventionalprometheus.io/scrape. - DB password + Blackfire/S3 tokens passed as plain env → move to
Secret+secretKeyRef.