WordPress Helm chart with FrankenPHP, MySQL, Varnish, Memcached, and S3 backups
2.2K
A Helm chart for deploying WordPress with MySQL, Varnish, and Memcached on Kubernetes.
This repository contains an OCI Helm chart, not a container image. Install it with Helm, not docker run. The WordPress runtime image is aapjeisbaas/wp-frankenphp.
Current chart: 0.2.11 · Default runtime: v0.3.1 (PHP 8.4) · Source code
This chart deploys a complete WordPress environment with the following components:
kubectl access--reset-then-reuse-values)Install directly from Docker Hub; no helm repo add is needed. Choose a storage class available in your cluster (kubectl get storageclass). The chart defaults to standard with ReadWriteMany volumes. For a single WordPress replica on storage that only supports ReadWriteOnce, save this as my-values.yaml and replace your-storage-class:
wordpress:
persistence:
storageClass: your-storage-class
accessMode: ReadWriteOnce
mysql:
persistence:
storageClass: your-storage-class
accessMode: ReadWriteOnce
helm install my-wordpress oci://registry-1.docker.io/aapjeisbaas/wordpress-helm \
--version 0.2.11 \
--namespace my-wordpress --create-namespace \
--values my-values.yaml \
--wait --timeout 10m
Inspect all defaults before installing:
helm show values oci://registry-1.docker.io/aapjeisbaas/wordpress-helm --version 0.2.11
The chart creates internal ClusterIP services; it does not create an Ingress or configure TLS. For a local preview:
kubectl --namespace my-wordpress port-forward service/my-wordpress-varnish 8080:80
Open http://localhost:8080. For public access, configure your ingress controller or reverse proxy to route to my-wordpress-varnish on port 80 in the release namespace. my-wordpress-wordpress:80 bypasses Varnish.
To install the chart from a local directory with the release name my-wordpress:
git clone https://gitlab.com/aapjeisbaas/wordpress-helm.git
helm install my-wordpress wordpress-helm \
--set wordpress.persistence.accessMode=ReadWriteOnce \
--set mysql.persistence.accessMode=ReadWriteOnce
0.2.11 defaults to wp-frankenphp:v0.3.1 with PHP 8.4 for broader compatibility. PHP 8.5 remains available via wordpress.image.tag=v0.3.1-php8.5.0.2.10 defaults to wp-frankenphp:v0.3.0 with PHP 8.5. Check plugin and theme compatibility before upgrading. To stay on PHP 8.4, set wordpress.image.tag=v0.3.0-php8.4.helm upgrade.ReadWriteOnce, keep using:--set wordpress.persistence.accessMode=ReadWriteOnce \
--set mysql.persistence.accessMode=ReadWriteOnce
Note for
0.1.11(nginx-unit) ->0.2.x(FrankenPHP): this transition is designed for in-place upgrade.
Note for
0.2.2: backup/restore support is opt-in. Existing releases can enable it withhelm upgrade --reset-then-reuse-valuesand thebackup.*,backupStorage.s3.*, and optionalrestore.*settings.
The following table lists the configurable parameters of the WordPress chart and their default values.
| Parameter | Description | Default |
|---|---|---|
networkPolicies.enabled | Enable network policies | true |
| Parameter | Description | Default |
|---|---|---|
wordpress.replicaCount | Number of WordPress pods | 1 |
wordpress.containerPort | WordPress container listen port | 8080 |
wordpress.dataPath | WordPress document root mounted from the WordPress PVC and archived/restored as files backup | /var/www |
wordpress.image.repository | WordPress image repository | aapjeisbaas/wp-frankenphp |
wordpress.image.tag | WordPress image tag | v0.3.1 |
wordpress.image.pullPolicy | WordPress image pull policy | IfNotPresent |
wordpress.php.customIni | Additional PHP ini settings mounted as /usr/local/etc/php/conf.d/zz-custom.ini | "" |
wordpress.extraEnv | Extra environment variables for WordPress pod | [] |
wordpress.resources | WordPress container resource requests and limits | CPU request 100m, memory request 256Mi, CPU limit 2000m, memory limit 2Gi |
wordpress.persistence.storageClass | Storage class for WordPress data | standard |
wordpress.persistence.size | Size of WordPress persistent volume | 10Gi |
wordpress.persistence.accessMode | Access mode for WordPress PVC | ReadWriteMany |
wordpress.autoscaling.enabled | Enable autoscaling for WordPress | false |
wordpress.autoscaling.minReplicas | Minimum number of WordPress replicas | 1 |
wordpress.autoscaling.maxReplicas | Maximum number of WordPress replicas | 10 |
wordpress.autoscaling.targetCPUUtilizationPercentage | Target CPU utilization | 80 |
wordpress.autoscaling.targetMemoryUtilizationPercentage | Optional target memory utilization | null |
wordpress.autoscaling.scaleUpStabilizationWindowSeconds | Scale-up stabilization window | 30 |
wordpress.autoscaling.scaleDownStabilizationWindowSeconds | Scale-down stabilization window | 300 |
| Parameter | Description | Default |
|---|---|---|
mysql.replicaCount | Number of MySQL pods | 1 |
mysql.image.repository | MySQL image repository | mysql |
mysql.image.tag | MySQL image tag | 8 |
mysql.image.pullPolicy | MySQL image pull policy | IfNotPresent |
mysql.resources | MySQL container resource requests and limits | CPU request 50m, memory request 256Mi, CPU limit 1000m, memory limit 768Mi |
mysql.persistence.storageClass | Storage class for MySQL data | standard |
mysql.persistence.size | Size of MySQL persistent volume | 10Gi |
mysql.persistence.accessMode | Access mode for MySQL PVC | ReadWriteMany |
mysql.config.performanceSchema | Enable/disable performance schema | "OFF" |
mysql.config.innodbBufferPoolSize | InnoDB buffer pool size | 64M |
mysql.config.innodbRedoLogCapacity | InnoDB redo log capacity | 32M |
mysql.config.innodbFilePerTable | Use file-per-table for InnoDB | 1 |
mysql.config.maxConnections | Maximum DB connections | 20 |
mysql.config.keyBufferSize | MyISAM key buffer size | 8M |
mysql.config.tmpTableSize | Temporary table size | 16M |
mysql.config.maxHeapTableSize | Max heap table size | 16M |
mysql.config.binlogExpireLogsSeconds | Binary log retention in seconds | 259200 |
mysql.config.generalLog | Enable MySQL general log | 0 |
mysql.config.slowQueryLog | Enable MySQL slow query log | 0 |
| Parameter | Description | Default |
|---|---|---|
varnish.replicaCount | Number of Varnish pods | 1 |
varnish.image.repository | Varnish image repository | varnish |
varnish.image.tag | Varnish image tag | alpine |
varnish.image.pullPolicy | Varnish image pull policy | IfNotPresent |
varnish.backendDiscovery.ttl | DNS refresh interval for WordPress pod backends | 5s |
varnish.backendDiscovery.retryAfter | Retry delay after backend DNS lookup failures | 2s |
varnish.backendDiscovery.firstLookupTimeout | Initial backend DNS lookup timeout | 2s |
varnish.backendDiscovery.keep | Number of refreshes to keep vanished backend IPs | 1 |
varnish.resources | Varnish container resource requests and limits | CPU request 50m, memory request 64Mi, CPU limit 500m, memory limit 2Gi |
memcached.replicaCount | Number of Memcached pods | 1 |
memcached.image.repository | Memcached image repository | memcached |
memcached.image.tag | Memcached image tag | alpine |
memcached.image.pullPolicy | Memcached image pull policy | IfNotPresent |
memcached.resources | Memcached container resource requests and limits | CPU request 10m, memory request 32Mi, CPU limit 250m, memory limit 128Mi |
| Parameter | Description | Default |
|---|---|---|
database.username | WordPress database username | wp-user |
database.database | WordPress database name | wp-db |
database.password | WordPress database password | "" (auto-generated on first install, then preserved) |
| Parameter | Description | Default |
|---|---|---|
backupStorage.s3.bucket | S3 bucket for backups | "" |
backupStorage.s3.prefix | Prefix inside the S3 bucket | wordpress |
backupStorage.s3.endpoint | Optional S3-compatible endpoint (MinIO, etc.) | "" |
backupStorage.s3.region | S3 region | eu-west-1 |
backupStorage.s3.existingSecret | Existing secret with S3 credentials | "" |
backupStorage.s3.accessKey | S3 access key, used when no existing secret is set | "" |
backupStorage.s3.secretKey | S3 secret key, used when no existing secret is set | "" |
| Parameter | Description | Default |
|---|---|---|
backup.enabled | Enable scheduled backups | false |
backup.schedule | Backup CronJob schedule | 0 2 * * * |
backup.successfulJobsHistoryLimit | Successful backup Jobs to keep | 3 |
backup.failedJobsHistoryLimit | Failed backup Jobs to keep | 1 |
backup.ttlSecondsAfterFinished | Cleanup TTL for completed backup Jobs | 86400 |
backup.retentionDays | Legacy age-based cleanup. Used only when all backup.retention.* rules are disabled. Empty, null, or 0 disables cleanup | 0 |
backup.retention.keepLast | Keep the N most recent backups regardless of timing | 3 |
backup.retention.keepDaily | Keep the newest backup from each of the last N UTC calendar days with backups | 7 |
backup.retention.keepWeekly | Keep the newest backup from each of the last N UTC ISO calendar weeks with backups | 4 |
backup.retention.keepMonthly | Keep the newest backup from each of the last N UTC calendar months with backups | 2 |
backup.retention.keepYearly | Keep the newest backup from each of the last N UTC calendar years with backups | 0 |
backup.image | Backup worker image | alpine:3.20 |
backup.mysql.host | Override MySQL host | "" (release MySQL service) |
backup.mysql.port | MySQL port | 3306 |
backup.mysql.existingSecret | Override DB credential secret | "" (release DB secret) |
backup.wordpress.volumeClaimName | Override WordPress PVC name | "" (release WordPress PVC) |
| Parameter | Description | Default |
|---|---|---|
restore.enabled | Enable destructive restore Job | false |
restore.confirmation | Required restore confirmation string | "" |
restore.backupId | Specific backup ID to restore; empty restores latest.txt | "" |
restore.autoRestoreOnEmpty | Restore automatically on empty installs | false |
restore.image | Restore worker image | alpine:3.20 |
restore.mysql.host | Override MySQL host | "" (release MySQL service) |
restore.mysql.port | MySQL port | 3306 |
restore.mysql.existingSecret | Override DB credential secret | "" (release DB secret) |
restore.mysql.waitAttempts | MySQL wait attempts before failing | 60 |
restore.wordpress.volumeClaimName | Override WordPress PVC name | "" (release WordPress PVC) |
The chart includes optional S3-compatible backup and restore support. It is disabled by default.
Backups use standard tools:
mysqldump --single-transaction --quick --routines --triggers, compressed as database.sql.gztar.gz archive of the full WordPress document root, default wordpress.dataPath /var/wwwThe files backup contains the complete WordPress tree under wordpress.dataPath, including WordPress core, plugins, themes, uploads, and config files stored on the WordPress PVC. The archive extracts directly into the configured data path, so it is usable outside these containers as a standalone WordPress filesystem backup.
Backup layout:
s3://<bucket>/<prefix>/latest.txt
s3://<bucket>/<prefix>/<backup-id>/database.sql.gz
s3://<bucket>/<prefix>/<backup-id>/wordpress-files.tar.gz
The backup ID is a UTC timestamp in YYYYMMDD-HHMMSS format.
For production, prefer an existing Kubernetes Secret:
kubectl create secret generic wordpress-backup-s3 \
--from-literal=AWS_ACCESS_KEY_ID='<access-key>' \
--from-literal=AWS_SECRET_ACCESS_KEY='<secret-key>'
Then reference it:
backupStorage:
s3:
bucket: my-wordpress-backups
prefix: production/example-site
region: eu-west-1
existingSecret: wordpress-backup-s3
For S3-compatible storage such as MinIO, also set backupStorage.s3.endpoint.
Kubernetes Secrets are namespace-scoped. A backup CronJob can only read a Secret from its own namespace. If many WordPress releases share the same backup credentials, use Emberstack Reflector to copy one source Secret to every namespace.
The chart does not need special configuration for this. Every namespace should end up with a local Secret named wordpress-backup-s3, and the chart should reference it with:
backupStorage:
s3:
existingSecret: wordpress-backup-s3
Install Reflector in the cluster:
helm repo add emberstack https://emberstack.github.io/helm-charts
helm repo update
helm upgrade --install reflector emberstack/reflector \
--namespace reflector \
--create-namespace
Create one namespace for the source Secret:
kubectl create namespace backup-secrets
Create the source Secret in that namespace:
kubectl -n backup-secrets create secret generic wordpress-backup-s3 \
--from-literal=AWS_ACCESS_KEY_ID='<access-key>' \
--from-literal=AWS_SECRET_ACCESS_KEY='<secret-key>'
Annotate the source Secret so Reflector copies it to every namespace:
kubectl -n backup-secrets annotate secret wordpress-backup-s3 \
reflector.v1.k8s.emberstack.com/reflection-allowed="true" \
reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces=".*" \
reflector.v1.k8s.emberstack.com/reflection-auto-enabled="true" \
reflector.v1.k8s.emberstack.com/reflection-auto-namespaces=".*" \
--overwrite
Use a narrower namespace pattern instead of .* if only some namespaces should receive the backup credentials, for example wordpress-.*|frankenphp-test.
Check that the Secret was copied:
kubectl get secret wordpress-backup-s3 -A
The wp-frankenphp image ships default PHP settings in /usr/local/etc/php/conf.d/99-custom.ini. To override or add PHP ini settings without rebuilding the image, set wordpress.php.customIni. The chart mounts the content as /usr/local/etc/php/conf.d/zz-custom.ini, so it is loaded after the image defaults.
For example, to raise the PHP memory limit for large wp-admin/editor requests:
wordpress:
php:
customIni: |
memory_limit=512M
You can include multiple settings:
wordpress:
php:
customIni: |
memory_limit=512M
upload_max_filesize=256M
post_max_size=256M
max_execution_time=300
Changing wordpress.php.customIni updates a pod template checksum, so Helm upgrades roll the WordPress pods automatically.
Enable backups with:
backup:
enabled: true
schedule: "0 2 * * *"
retention:
keepLast: 3
keepDaily: 7
keepWeekly: 4
keepMonthly: 2
backupStorage:
s3:
bucket: my-wordpress-backups
existingSecret: wordpress-backup-s3
Retention cleanup runs after a successful upload. It only deletes timestamped backup directories, never deletes latest.txt, always keeps the backup referenced by latest.txt, and stops before deleting the last remaining backup.
The recommended retention policy uses Proxmox Backup Server-style buckets:
keepLast: keep the N most recent backups regardless of timing.keepDaily: keep the newest backup from each of the last N UTC calendar days with backups.keepWeekly: keep the newest backup from each of the last N UTC ISO calendar weeks with backups.keepMonthly: keep the newest backup from each of the last N UTC calendar months with backups.keepYearly: keep the newest backup from each of the last N UTC calendar years with backups.Rules are evaluated in this order: keepLast, keepDaily, keepWeekly, keepMonthly, keepYearly. Kept backups are de-duplicated, so overlap is intentional. For example, if the newest three backups are also the newest backups for three daily buckets, they count toward both keepLast and keepDaily and are stored only once.
Retention policy examples, assuming one backup per day:
| Policy | Use case | keepLast | keepDaily | keepWeekly | keepMonthly | keepYearly | Approx. retained |
|---|---|---|---|---|---|---|---|
| Homelab | Small setup with a short but practical history | 3 | 7 | 4 | 2 | 0 | ~14 |
| Production | Business-critical sites with longer detection windows | 14 | 14 | 8 | 12 | 0 | ~30 |
| Compliance | Long-term archival needs | 7 | 30 | 12 | 24 | 3 | ~55 |
Approximate retained counts account for overlap between rules. Actual counts vary depending on backup frequency and timing.
Set all backup.retention.* values to empty, null, or 0 to disable rule-based cleanup. If all rule-based retention values are disabled, the older backup.retentionDays setting is used for age-based cleanup. Set backup.retentionDays to empty, null, or 0 as well to keep backups forever.
To run a backup immediately:
kubectl create job --from=cronjob/my-wordpress-backup my-wordpress-backup-manual
Restore is destructive: it overwrites WordPress files under wordpress.dataPath and imports the database dump.
You must set the confirmation string explicitly:
helm upgrade my-wordpress oci://registry-1.docker.io/aapjeisbaas/wordpress-helm \
--version 0.2.11 \
--namespace my-wordpress \
--reset-then-reuse-values \
--set restore.enabled=true \
--set restore.confirmation=I_UNDERSTAND_RESTORE_OVERWRITES_DATA \
--set restore.backupId=20260101-020000
Leave restore.backupId empty to restore the backup referenced by latest.txt.
Set restore.enabled=false again after the restore Job completes, otherwise the restore hook will be created again on later upgrades.
Auto-restore is intended for disaster recovery or creating a fresh install from the latest backup:
restore:
autoRestoreOnEmpty: true
backupStorage:
s3:
bucket: my-wordpress-backups
existingSecret: wordpress-backup-s3
The initContainer restores only when both the database and wordpress.dataPath are empty. If both contain data, it skips restore. If only one side contains data, it fails loudly to avoid overwriting a partial state silently.
Do not enable automatic restore in production until manual restore has been tested successfully. A backup that has not been restored at least once is not a backup.
This chart uses PVC to persist data for WordPress and MySQL deployments. You can configure the storage class and size in the values.yaml file.
If you are upgrading an existing release, keep the existing PVC access mode values. Kubernetes does not allow changing access mode on bound PVCs.
helm upgrade my-wordpress /path/to/wordpress-helm \
--reset-then-reuse-values
For existing RWO volumes:
helm upgrade my-wordpress /path/to/wordpress-helm \
--reset-then-reuse-values \
--set wordpress.persistence.accessMode=ReadWriteOnce \
--set mysql.persistence.accessMode=ReadWriteOnce
Back up the site first. Upgrade using the existing release name and namespace:
helm upgrade my-wordpress oci://registry-1.docker.io/aapjeisbaas/wordpress-helm \
--version 0.2.11 \
--namespace my-wordpress \
--reset-then-reuse-values \
--wait --timeout 10m
--reset-then-reuse-values loads the new chart defaults and reapplies your saved custom values. Avoid --reuse-values alone: it can keep old defaults, including the old WordPress image. Explicit image overrides remain overrides; remove or update them if you want to follow the chart default. Keep existing storage classes and PVC access modes unchanged.
For automatic rollback on failure, add --rollback-on-failure with Helm 4 or --atomic with Helm 3. A Helm rollback does not undo database or filesystem changes.
helm uninstall my-wordpress --namespace my-wordpress
Uninstalling removes the chart-managed PVCs as well as the workloads. Depending on the storage class reclaim policy, the underlying volumes and site data may also be deleted. Make and verify a backup before uninstalling.
Content type
Helm
Digest
sha256:78f20a482…
Size
71.3 kB
Last updated
20 days ago
helm pull oci://registry-1.docker.io/aapjeisbaas/wordpress-helm --version 0.2.11