Pimcore
Production-oriented deployment of Pimcore, the PHP/Symfony platform for PIM, MDM, DAM, CDP, DXP/CMS, and digital commerce.
This chart targets Pimcore 2026.2.5 and pins the official multi-architecture
PHP 8.5 runtime pimcore/pimcore:php8.5.9-max-v5.2-hardened. It models the
upstream topology instead of treating the runtime image as a complete
application:
- nginx and PHP-FPM in one web pod with independent health checks;
- Symfony Messenger workers with the official Pimcore transport set;
pimcore:maintenanceCronJob;- HelmForge MariaDB, RabbitMQ, and optional Redis dependencies;
- Mercure for Pimcore Studio real-time updates;
- project and public-asset persistence;
- explicit installation and product-registration workflow;
- Ingress, Gateway API, dual-stack Service, NetworkPolicy, PDB, and ESO.
Important application-image boundary
The official pimcore/pimcore image is a PHP runtime. It does not contain a
Pimcore project. A production deployment must build project code, locked
Composer dependencies, configuration, and generated classes into an immutable
image based on that runtime.
For evaluation and first installation, the default bootstrap downloads the
exact pimcore/skeleton:2026.2.0 Composer project into a persistent project
volume. This mode is intentionally single-replica and requires outbound HTTPS.
It is not a replacement for an immutable production image.
Pimcore 2026 requires product registration before installation or use. The
chart never invents or reuses registration data. Installation runs only when
install.enabled=true and a matching product key, instance identifier, and
encryption secret are supplied.
Install
helm install pimcore oci://ghcr.io/helmforgedev/helm/pimcore \
--namespace pimcore \
--create-namespace
Wait for the runtime:
kubectl wait -n pimcore \
--for=condition=available deployment/pimcore \
--timeout=300s
kubectl port-forward -n pimcore service/pimcore 8080:80
curl -fsS http://127.0.0.1:8080/healthz
curl -fsS http://127.0.0.1:8080/readyz
The two endpoints verify nginx and PHP-FPM. The Pimcore UI is available only after the registered installer completes.
Registered installation
Create a Secret containing one matching registration set:
apiVersion: v1
kind: Secret
metadata:
name: pimcore-registration
type: Opaque
stringData:
application-secret: replace-with-a-long-random-value
admin-password: replace-with-a-strong-password
product-key: replace-with-the-registered-product-key
instance-identifier: replace-with-the-registered-uuid
encryption-secret: replace-with-the-matching-defuse-key
mercure-jwt-key: replace-with-a-long-random-value
Run the installer:
helm upgrade pimcore oci://ghcr.io/helmforgedev/helm/pimcore \
--namespace pimcore \
--reuse-values \
--set auth.existingSecret=pimcore-registration \
--set install.enabled=true
Inspect the hook Job and disable the one-time installer after success:
kubectl logs -n pimcore job/pimcore-install
helm upgrade pimcore oci://ghcr.io/helmforgedev/helm/pimcore \
--namespace pimcore \
--reuse-values \
--set install.enabled=false \
--set worker.enabled=true \
--set maintenance.enabled=true
Production image
Build a project image outside Kubernetes:
FROM pimcore/pimcore:php8.5.9-max-v5.2-hardened
WORKDIR /var/www/html
COPY --chown=www-data:www-data . .
RUN composer install \
--no-dev \
--no-interaction \
--no-progress \
--prefer-dist \
--optimize-autoloader \
&& test -f bin/console
Deploy it with bootstrap and project persistence disabled:
image:
repository: registry.example.com/platform/pimcore-project
tag: '2026.2.5-1'
project:
bootstrap:
enabled: false
persistence:
enabled: false
auth:
existingSecret: pimcore-registration
worker:
enabled: true
maintenance:
enabled: true
With project persistence disabled, nginx, PHP-FPM, workers, and maintenance use the project code directly from the immutable image. Only public assets remain mounted from their dedicated persistent volume.
Database
MariaDB is enabled by default with the upstream-required utf8mb4 character
set and utf8mb4_unicode_520_ci collation:
mariadb:
standalone:
persistence:
size: 20Gi
Use a managed MariaDB service:
mariadb:
enabled: false
database:
mode: external
serverVersion: mariadb-11.4.7
external:
host: mariadb.database.svc
name: pimcore
username: pimcore
existingSecret: pimcore-database
existingSecretPasswordKey: password
The chart constructs DATABASE_URL at process start so passwords remain
Secret-backed and do not appear in rendered manifests. Set
database.serverVersion to the exact Doctrine-compatible external engine
version; the default matches the bundled MariaDB 12.3.2 dependency.
RabbitMQ and workers
The default RabbitMQ dependency uses quorum queues and persistent storage. Workers consume the transports recommended by the current Pimcore skeleton:
pimcore_generic_execution_enginepimcore_generic_data_index_queuescheduler_generic_data_indexpimcore_corepimcore_maintenancepimcore_scheduled_taskspimcore_image_optimizepimcore_asset_update
Use an external broker:
rabbitmq:
enabled: false
queue:
mode: external
external:
host: rabbitmq.messaging.svc
username: pimcore
vhost: pimcore
existingSecret: pimcore-rabbitmq
The chart constructs the AMQP DSN in the container. URL-encode a custom vhost.
Redis
The upstream skeleton starts Redis but does not activate a Redis-backed Symfony cache or session handler. The chart therefore keeps Redis optional and exposes connection values only when requested:
cache:
enabled: true
redis:
enabled: true
Your project configuration must consume REDIS_HOST, REDIS_PORT,
REDIS_PASSWORD, and REDIS_TLS.
Mercure
Mercure is enabled for Pimcore Studio. The internal hub is exposed through the
main nginx Service at /hub, avoiding a separate public origin. Anonymous
subscriptions match the skeleton development topology and should be disabled
after the project configures subscriber authorization:
mercure:
anonymous: false
Persistence and scaling
Bootstrap mode uses a project PVC. Production immutable-image mode should disable it. Public assets have a separate PVC:
assets:
persistence:
enabled: true
storageClass: nfs-rwx
accessModes:
- ReadWriteMany
size: 100Gi
The chart rejects multiple web replicas with a chart-created RWO project or asset PVC. For HA:
- use an immutable project image;
- disable project persistence and bootstrap;
- provide RWX assets or configure project-specific object storage;
- use external HA MariaDB, RabbitMQ, and Redis services;
- configure pod topology and a PDB.
OpenSearch
Generic Data Index installations can require OpenSearch. It is deliberately not bundled because supported versions and credentials belong to the application project:
pimcore:
opensearchDSN: opensearch://user:[email protected]:9200
Prefer pimcore.extraEnv plus a Secret reference when the DSN contains
credentials.
External Secrets
The chart implements the canonical externalSecrets.items[] contract:
auth:
existingSecret: pimcore-registration
externalSecrets:
enabled: true
items:
- name: registration
spec:
secretStoreRef:
name: production
kind: ClusterSecretStore
target:
name: pimcore-registration
creationPolicy: Owner
dataFrom:
- extract:
key: pimcore/production
The target Secret must contain every key configured under auth.
Routing
Ingress:
ingress:
enabled: true
ingressClassName: nginx
hosts:
- host: pimcore.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: pimcore-tls
hosts:
- pimcore.example.com
Gateway API:
gatewayAPI:
enabled: true
httpRoutes:
- name: public
parentRefs:
- name: public-gateway
namespace: gateways
hostnames:
- pimcore.example.com
Network policy
NetworkPolicy is opt-in. When egress enforcement is enabled, the chart permits DNS only to the configured cluster DNS namespace/pod selectors, plus chart-managed MariaDB, RabbitMQ, Redis, and Mercure traffic. Bootstrap also requires outbound HTTPS:
networkPolicy:
enabled: true
egress:
enabled: true
extraEgress:
- ports:
- protocol: TCP
port: 443
Production deployments should restrict that rule to known Composer or service CIDRs, or remove it when using an immutable project image.
Backup and restore
A complete recovery point includes:
- MariaDB;
- public assets or the external object store;
- the immutable application image and its configuration;
- registration Secret values;
- RabbitMQ only when durable in-flight jobs must survive the recovery point.
Do not back up cache directories as authoritative data. Test restore and
pimcore:deployment:classes-rebuild procedures against the application
project.
Security Scan
Security Scan: pimcore
| Framework | Score |
|---|---|
| MITRE + NSA + SOC2 | 89.43% |
Security posture acceptable.
Local details:
- Tool: Kubescape v4.0.9
- Command:
kubescape scan framework mitre,nsa,soc2 .tmp/pimcore-render.yaml - Result: 0 critical and 0 high failed resources, resource summary score 89.43%.
Runtime images are official, exact, multi-architecture tags. The default PHP image is the upstream hardened variant. Containers drop all Linux capabilities, disable privilege escalation, run as non-root users, and do not automount ServiceAccount tokens. nginx and the Helm test use read-only root filesystems. PHP workloads retain a writable root because upstream Pimcore and Composer write project/runtime paths without a complete relocation contract; immutable project images should minimize those paths and keep application data on explicit volumes.
Validation
Run the full HelmForge gate:
make validate-chart CHART=pimcore
This covers dependencies, strict lint, default and CI rendering, unit tests, kubeconform with real CRD schemas, Artifact Hub lint, and behavioral k3d deployment.
Configuration reference
Naming
| Value | Default | Description |
|---|---|---|
nameOverride |
"" |
Override the chart name used by resource names. |
fullnameOverride |
"" |
Override the complete release resource prefix. |
namespaceOverride |
"" |
Render namespaced resources in another namespace. |
clusterDomain |
cluster.local |
Kubernetes DNS suffix. |
commonLabels |
{} |
Labels added to every chart-owned resource. |
Application and nginx images
| Value | Default | Description |
|---|---|---|
image.repository |
docker.io/pimcore/pimcore |
Runtime or custom project image repository. |
image.tag |
php8.5.9-max-v5.2-hardened |
Exact official hardened runtime tag. |
image.pullPolicy |
IfNotPresent |
Application image pull policy. |
imagePullSecrets |
[] |
Registry pull Secrets. |
waitForDependencies.image.repository |
docker.io/library/busybox |
Dependency-check image repository. |
waitForDependencies.image.tag |
1.37.0 |
Exact dependency-check image tag. |
nginx.image.repository |
docker.io/library/nginx |
Official nginx repository. |
nginx.image.tag |
1.30.4-alpine3.24 |
Exact nginx tag. |
nginx.clientMaxBodySize |
100m |
Maximum HTTP request body. |
nginx.resources |
See values | nginx requests and limits. |
Project lifecycle
| Value | Default | Description |
|---|---|---|
project.runtimeImage.repository |
docker.io/pimcore/pimcore |
Immutable project base-image repository. |
project.runtimeImage.tag |
php8.5.9-max-v5.2-hardened |
Immutable project base-image tag. |
project.bootstrap.enabled |
true |
Bootstrap the pinned skeleton when the image has no project. |
project.bootstrap.skeletonVersion |
2026.2.0 |
Exact skeleton release. |
project.persistence.enabled |
true |
Persist bootstrapped project code. |
project.persistence.storageClass |
"" |
Project PVC storage class. |
project.persistence.accessModes |
[ReadWriteOnce] |
Project PVC access modes. |
project.persistence.size |
4Gi |
Project PVC request. |
project.persistence.existingClaim |
"" |
Reuse an existing project claim. |
project.persistence.annotations |
{} |
Project PVC annotations. |
Web and workers
| Value | Default | Description |
|---|---|---|
web.replicaCount |
1 |
nginx/PHP-FPM web pod replicas. |
web.resources |
See values | PHP-FPM requests and limits. |
worker.enabled |
false |
Enable installed-project Messenger consumers. |
worker.replicaCount |
1 |
Worker replicas. |
worker.transports |
Eight official queues | Messenger transports consumed by each worker. |
worker.memoryLimit |
250M |
Symfony Messenger recycle memory threshold. |
worker.timeLimit |
3600 |
Symfony Messenger recycle interval. |
worker.resources |
See values | Worker requests and limits. |
Maintenance and installation
| Value | Default | Description |
|---|---|---|
maintenance.enabled |
false |
Enable pimcore:maintenance. |
maintenance.schedule |
*/5 * * * * |
Maintenance Cron schedule. |
maintenance.concurrencyPolicy |
Forbid |
Prevent overlapping maintenance jobs. |
maintenance.successfulJobsHistoryLimit |
1 |
Successful Job retention. |
maintenance.failedJobsHistoryLimit |
3 |
Failed Job retention. |
install.enabled |
false |
Run the one-time registered installer hook. |
install.profile |
App\Installer\SkeletonProfile |
Installer profile class. |
install.hookDeletePolicy |
before-hook-creation |
Helm hook cleanup policy. |
install.backoffLimit |
1 |
Installer retry limit. |
install.activeDeadlineSeconds |
1800 |
Installer hard timeout. |
Pimcore environment
| Value | Default | Description |
|---|---|---|
pimcore.environment |
prod |
Symfony environment. |
pimcore.debug |
false |
Symfony debug flag. |
pimcore.trustedProxies |
127.0.0.1,REMOTE_ADDR |
Symfony trusted proxies. |
pimcore.trustedHosts |
"" |
Trusted-host regular expression. |
pimcore.mercureURL |
"" |
Public Mercure URL; empty derives the chart route. |
pimcore.opensearchDSN |
"" |
Optional Generic Data Index OpenSearch DSN. |
pimcore.extraEnv |
[] |
Additional environment variables. |
pimcore.extraEnvFrom |
[] |
Additional Secret/ConfigMap environment sources. |
Registration and application Secret
| Value | Default | Description |
|---|---|---|
auth.existingSecret |
"" |
Existing complete application Secret. |
auth.applicationSecretKey |
application-secret |
Application Secret key name. |
auth.adminPasswordKey |
admin-password |
Administrator password key name. |
auth.productKeyKey |
product-key |
Product key key name. |
auth.instanceIdentifierKey |
instance-identifier |
Instance UUID key name. |
auth.encryptionSecretKey |
encryption-secret |
Defuse key key name. |
auth.mercureJWTKey |
mercure-jwt-key |
Mercure JWT key name. |
auth.applicationSecret |
"" |
Inline application secret; generated when empty. |
auth.adminUser |
admin |
Initial administrator username. |
auth.adminPassword |
"" |
Inline password; generated when empty. |
auth.productKey |
"" |
Registered product key. |
auth.instanceIdentifier |
"" |
Registered instance UUID. |
auth.encryptionSecret |
"" |
Matching encryption secret. |
auth.mercureJWT |
"" |
Inline Mercure key; generated when empty. |
Database
| Value | Default | Description |
|---|---|---|
database.mode |
mariadb |
mariadb or external. |
database.serverVersion |
mariadb-12.3.2 |
Doctrine-compatible engine version. |
database.external.host |
"" |
External MariaDB host. |
database.external.port |
3306 |
External MariaDB port. |
database.external.name |
pimcore |
Database name. |
database.external.username |
pimcore |
Database user. |
database.external.password |
"" |
Inline external password. |
database.external.existingSecret |
"" |
External password Secret. |
mariadb.enabled |
true |
Deploy HelmForge MariaDB. |
mariadb.standalone.persistence.size |
8Gi |
MariaDB volume request. |
Queue and cache
| Value | Default | Description |
|---|---|---|
queue.mode |
rabbitmq |
rabbitmq or external. |
queue.external.host |
"" |
External RabbitMQ host. |
queue.external.port |
5672 |
External AMQP port. |
queue.external.username |
pimcore |
External broker user. |
queue.external.vhost |
%2f |
URL-encoded broker vhost. |
queue.external.existingSecret |
"" |
Broker password Secret. |
rabbitmq.enabled |
true |
Deploy HelmForge RabbitMQ. |
rabbitmq.singleNode.persistence.size |
4Gi |
Broker storage request. |
cache.enabled |
false |
Expose Redis connection values. |
cache.mode |
redis |
redis or external. |
cache.external.host |
"" |
External Redis host. |
cache.external.port |
6379 |
External Redis port. |
cache.external.tls |
false |
Indicate TLS to project configuration. |
redis.enabled |
false |
Deploy HelmForge Redis. |
Mercure and assets
| Value | Default | Description |
|---|---|---|
mercure.enabled |
true |
Deploy the internal Mercure hub. |
mercure.image.tag |
v0.24.2 |
Exact official Mercure tag. |
mercure.replicaCount |
1 |
Fixed at one for the bundled local transport. |
mercure.anonymous |
true |
Allow anonymous subscribers. |
assets.persistence.enabled |
true |
Persist public assets. |
assets.persistence.accessModes |
[ReadWriteOnce] |
Asset PVC access modes. |
assets.persistence.size |
20Gi |
Asset PVC request. |
assets.persistence.existingClaim |
"" |
Reuse an existing asset claim. |
Routing, policy, and scheduling
| Value | Default | Description |
|---|---|---|
service.type |
ClusterIP |
Kubernetes Service type. |
service.port |
80 |
Public Service port. |
service.ipFamilyPolicy |
"" |
Optional dual-stack policy. |
service.ipFamilies |
[] |
Optional IP families. |
ingress.enabled |
false |
Create an Ingress. |
ingress.ingressClassName |
"" |
Ingress class. |
gatewayAPI.enabled |
false |
Create HTTPRoutes. |
externalSecrets.enabled |
false |
Create ExternalSecret resources. |
externalSecrets.items |
[] |
Canonical ExternalSecret definitions. |
networkPolicy.enabled |
false |
Create workload policy. |
networkPolicy.egress.enabled |
false |
Enforce egress rules. |
networkPolicy.egress.dns.namespaceSelector |
kube-system label selector |
Cluster DNS namespace selector. |
networkPolicy.egress.dns.podSelector |
k8s-app: kube-dns |
Cluster DNS pod selector. |
networkPolicy.egress.extraEgress |
[] |
Complete extra egress rules. |
podDisruptionBudget.enabled |
true |
Create PDBs for scaled workloads. |
serviceAccount.automountServiceAccountToken |
false |
Disable API credentials. |
nodeSelector |
{} |
Node selection constraints. |
tolerations |
[] |
Pod tolerations. |
affinity |
{} |
Pod affinity rules. |
topologySpreadConstraints |
[] |
Pod distribution rules. |
Troubleshooting guide
The web pod remains in Init:0/2
Inspect prepare-project. Bootstrap downloads more than 200 locked Composer
packages and needs outbound HTTPS, writable project storage, and sufficient
time. Production images avoid this dependency.
The installer rejects the product key
The product key is cryptographically tied to the instance identifier and encryption secret. Supply the exact three values generated by one registration flow. Do not generate or rotate only one member of the set.
/healthz works but the UI returns 404
This is expected before database installation. /healthz proves nginx and
/readyz proves PHP-FPM; neither claims that Pimcore is registered or installed.
PHP-FPM starts but reports database errors
Verify the MariaDB Service, Secret key, database name, username, and collation. Inspect the in-container environment without printing secret values, then test TCP connectivity to port 3306.
Workers crash immediately
Workers require a fully installed database and an application image containing
the same project code as web. Keep worker.enabled=false during initial
registration and installation.
Jobs accumulate in RabbitMQ
Confirm the eight configured transports match the project, inspect worker logs, and verify that workers can read the broker Secret. Scale workers based on queue depth and task cost.
Pimcore Studio does not receive real-time updates
Check the Mercure pod, /hub nginx proxy, and JWT key consistency. Disable
anonymous subscriptions only after project authorization is configured.
Multiple web replicas are rejected
Disable bootstrap/project persistence and use an immutable application image. Provide RWX assets or external object storage. The guard prevents unsafe RWO mount assumptions.
Assets disappear after a rollout
Verify assets.persistence.enabled, the bound PVC, mount events, and storage
access mode. Do not store authoritative assets only in pod-local storage.
Redis is healthy but unused
That is expected unless the project configures Symfony cache/session providers. The chart supplies connection variables but does not rewrite application configuration.
Generic Data Index fails
Configure a compatible OpenSearch endpoint and credentials for the installed bundle version. OpenSearch is deliberately external to this chart.
NetworkPolicy blocks bootstrap
Add explicit HTTPS egress for Composer or deploy an immutable image and remove runtime package downloads. Keep the rule as narrow as the environment allows.
Upgrades time out
Inspect the installer/migration Job before rolling web and workers. Back up MariaDB and assets, run schema transitions once, and never allow all replicas to race migrations.