Sign inSign up

aapjeisbaas/wordpress-helm

By aapjeisbaas

•Updated 20 days ago

WordPress Helm chart with FrankenPHP, MySQL, Varnish, Memcached, and S3 backups

Helm
0

2.2K

aapjeisbaas/wordpress-helm repository overview

⁠WordPress Helm Chart

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⁠

⁠Overview

This chart deploys a complete WordPress environment with the following components:

  • WordPress with FrankenPHP
  • MySQL database
  • Varnish cache
  • Memcached for object caching
  • Network policies for securing communications
  • Optional scheduled S3-compatible backups and restore support

⁠Prerequisites

  • A Kubernetes cluster and kubectl access
  • Helm 3.14+ or Helm 4 (the upgrade examples use --reset-then-reuse-values)
  • A storage class supporting the configured persistent volume access modes

⁠Installing the Chart

⁠From DockerHub OCI Registry

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
⁠Access WordPress

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.

⁠From Local Directory

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

⁠Upgrade Notes

  • Chart 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.
  • Chart 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.
  • In-place upgrades are supported with helm upgrade.
  • Database password is generated once (if empty) and preserved on upgrades.
  • Services and network policies are release-scoped for multi-release support.
  • Deployment selectors remain app-only for upgrade compatibility because Kubernetes does not allow changing them in-place.
  • PVC access mode is immutable in Kubernetes. If an existing release uses 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 with helm upgrade --reset-then-reuse-values and the backup.*, backupStorage.s3.*, and optional restore.* settings.

⁠Configuration

The following table lists the configurable parameters of the WordPress chart and their default values.

⁠Global Parameters
ParameterDescriptionDefault
networkPolicies.enabledEnable network policiestrue
⁠WordPress Parameters
ParameterDescriptionDefault
wordpress.replicaCountNumber of WordPress pods1
wordpress.containerPortWordPress container listen port8080
wordpress.dataPathWordPress document root mounted from the WordPress PVC and archived/restored as files backup/var/www
wordpress.image.repositoryWordPress image repositoryaapjeisbaas/wp-frankenphp
wordpress.image.tagWordPress image tagv0.3.1
wordpress.image.pullPolicyWordPress image pull policyIfNotPresent
wordpress.php.customIniAdditional PHP ini settings mounted as /usr/local/etc/php/conf.d/zz-custom.ini""
wordpress.extraEnvExtra environment variables for WordPress pod[]
wordpress.resourcesWordPress container resource requests and limitsCPU request 100m, memory request 256Mi, CPU limit 2000m, memory limit 2Gi
wordpress.persistence.storageClassStorage class for WordPress datastandard
wordpress.persistence.sizeSize of WordPress persistent volume10Gi
wordpress.persistence.accessModeAccess mode for WordPress PVCReadWriteMany
wordpress.autoscaling.enabledEnable autoscaling for WordPressfalse
wordpress.autoscaling.minReplicasMinimum number of WordPress replicas1
wordpress.autoscaling.maxReplicasMaximum number of WordPress replicas10
wordpress.autoscaling.targetCPUUtilizationPercentageTarget CPU utilization80
wordpress.autoscaling.targetMemoryUtilizationPercentageOptional target memory utilizationnull
wordpress.autoscaling.scaleUpStabilizationWindowSecondsScale-up stabilization window30
wordpress.autoscaling.scaleDownStabilizationWindowSecondsScale-down stabilization window300
⁠MySQL Parameters
ParameterDescriptionDefault
mysql.replicaCountNumber of MySQL pods1
mysql.image.repositoryMySQL image repositorymysql
mysql.image.tagMySQL image tag8
mysql.image.pullPolicyMySQL image pull policyIfNotPresent
mysql.resourcesMySQL container resource requests and limitsCPU request 50m, memory request 256Mi, CPU limit 1000m, memory limit 768Mi
mysql.persistence.storageClassStorage class for MySQL datastandard
mysql.persistence.sizeSize of MySQL persistent volume10Gi
mysql.persistence.accessModeAccess mode for MySQL PVCReadWriteMany
mysql.config.performanceSchemaEnable/disable performance schema"OFF"
mysql.config.innodbBufferPoolSizeInnoDB buffer pool size64M
mysql.config.innodbRedoLogCapacityInnoDB redo log capacity32M
mysql.config.innodbFilePerTableUse file-per-table for InnoDB1
mysql.config.maxConnectionsMaximum DB connections20
mysql.config.keyBufferSizeMyISAM key buffer size8M
mysql.config.tmpTableSizeTemporary table size16M
mysql.config.maxHeapTableSizeMax heap table size16M
mysql.config.binlogExpireLogsSecondsBinary log retention in seconds259200
mysql.config.generalLogEnable MySQL general log0
mysql.config.slowQueryLogEnable MySQL slow query log0
⁠Varnish and Memcached Parameters
ParameterDescriptionDefault
varnish.replicaCountNumber of Varnish pods1
varnish.image.repositoryVarnish image repositoryvarnish
varnish.image.tagVarnish image tagalpine
varnish.image.pullPolicyVarnish image pull policyIfNotPresent
varnish.backendDiscovery.ttlDNS refresh interval for WordPress pod backends5s
varnish.backendDiscovery.retryAfterRetry delay after backend DNS lookup failures2s
varnish.backendDiscovery.firstLookupTimeoutInitial backend DNS lookup timeout2s
varnish.backendDiscovery.keepNumber of refreshes to keep vanished backend IPs1
varnish.resourcesVarnish container resource requests and limitsCPU request 50m, memory request 64Mi, CPU limit 500m, memory limit 2Gi
memcached.replicaCountNumber of Memcached pods1
memcached.image.repositoryMemcached image repositorymemcached
memcached.image.tagMemcached image tagalpine
memcached.image.pullPolicyMemcached image pull policyIfNotPresent
memcached.resourcesMemcached container resource requests and limitsCPU request 10m, memory request 32Mi, CPU limit 250m, memory limit 128Mi
⁠Database Secrets
ParameterDescriptionDefault
database.usernameWordPress database usernamewp-user
database.databaseWordPress database namewp-db
database.passwordWordPress database password"" (auto-generated on first install, then preserved)
⁠Backup Storage Parameters
ParameterDescriptionDefault
backupStorage.s3.bucketS3 bucket for backups""
backupStorage.s3.prefixPrefix inside the S3 bucketwordpress
backupStorage.s3.endpointOptional S3-compatible endpoint (MinIO, etc.)""
backupStorage.s3.regionS3 regioneu-west-1
backupStorage.s3.existingSecretExisting secret with S3 credentials""
backupStorage.s3.accessKeyS3 access key, used when no existing secret is set""
backupStorage.s3.secretKeyS3 secret key, used when no existing secret is set""
⁠Backup Parameters
ParameterDescriptionDefault
backup.enabledEnable scheduled backupsfalse
backup.scheduleBackup CronJob schedule0 2 * * *
backup.successfulJobsHistoryLimitSuccessful backup Jobs to keep3
backup.failedJobsHistoryLimitFailed backup Jobs to keep1
backup.ttlSecondsAfterFinishedCleanup TTL for completed backup Jobs86400
backup.retentionDaysLegacy age-based cleanup. Used only when all backup.retention.* rules are disabled. Empty, null, or 0 disables cleanup0
backup.retention.keepLastKeep the N most recent backups regardless of timing3
backup.retention.keepDailyKeep the newest backup from each of the last N UTC calendar days with backups7
backup.retention.keepWeeklyKeep the newest backup from each of the last N UTC ISO calendar weeks with backups4
backup.retention.keepMonthlyKeep the newest backup from each of the last N UTC calendar months with backups2
backup.retention.keepYearlyKeep the newest backup from each of the last N UTC calendar years with backups0
backup.imageBackup worker imagealpine:3.20
backup.mysql.hostOverride MySQL host"" (release MySQL service)
backup.mysql.portMySQL port3306
backup.mysql.existingSecretOverride DB credential secret"" (release DB secret)
backup.wordpress.volumeClaimNameOverride WordPress PVC name"" (release WordPress PVC)
⁠Restore Parameters
ParameterDescriptionDefault
restore.enabledEnable destructive restore Jobfalse
restore.confirmationRequired restore confirmation string""
restore.backupIdSpecific backup ID to restore; empty restores latest.txt""
restore.autoRestoreOnEmptyRestore automatically on empty installsfalse
restore.imageRestore worker imagealpine:3.20
restore.mysql.hostOverride MySQL host"" (release MySQL service)
restore.mysql.portMySQL port3306
restore.mysql.existingSecretOverride DB credential secret"" (release DB secret)
restore.mysql.waitAttemptsMySQL wait attempts before failing60
restore.wordpress.volumeClaimNameOverride WordPress PVC name"" (release WordPress PVC)

⁠Backups And Restores

The chart includes optional S3-compatible backup and restore support. It is disabled by default.

Backups use standard tools:

  • Database: mysqldump --single-transaction --quick --routines --triggers, compressed as database.sql.gz
  • Files: tar.gz archive of the full WordPress document root, default wordpress.dataPath /var/www
  • Storage: AWS CLI to S3-compatible storage

The 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.

⁠S3 Credentials

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.

⁠Shared S3 Credentials Across Namespaces

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
⁠PHP Configuration

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.

⁠Scheduled Backups

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:

PolicyUse casekeepLastkeepDailykeepWeeklykeepMonthlykeepYearlyApprox. retained
HomelabSmall setup with a short but practical history37420~14
ProductionBusiness-critical sites with longer detection windows14148120~30
ComplianceLong-term archival needs73012243~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
⁠Manual Restore

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 On Empty Installs

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.

⁠Persistence

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.

⁠Upgrading

⁠From Local Directory
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
⁠From DockerHub OCI Registry

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.

⁠Uninstalling the Chart

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.

Tag summary

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