Deploy Magento 2.4.8 on Kubernetes with git-sync support for live plugin development.
- Magento 2.4.8 Community Edition with sample data (2000+ products)
- Single-pod deployment with MariaDB and OpenSearch sidecars
- Git-sync sidecars for live code updates during development
- Configurable via Helm values
# Using OCI registry
helm install magento oci://ghcr.io/brtkwr/charts/magento \
--set magento.host=magento.example.com \
--set adminUsers[0].password=YourSecurePassword123 \
--set database.password=YourDBPassword123
# Or clone locally
git clone https://github.com/brtkwr/magento-helm.git
cd magento-helm
helm install magento ./charts/magento \
--set magento.host=magento.example.com \
--set adminUsers[0].password=YourSecurePassword123 \
--set database.password=YourDBPassword123- Kubernetes 1.24+
- Helm 3.x
- PersistentVolume provisioner
- Ingress controller (nginx, traefik, etc.)
See values.yaml for all options.
| Parameter | Description | Default |
|---|---|---|
magento.host |
Store URL hostname | magento.example.com |
magento.currency |
Store currency | GBP |
magento.language |
Store language | en_GB |
existingSecret |
Name of existing secret for passwords | "" |
extraEnv |
Extra environment variables for containers | [] |
adminUsers |
Array of admin users | (see below) |
database.password |
MariaDB password (ignored if existingSecret set) | (generated) |
gitSync.plugins |
Array of git repos to sync | [] |
The first user in the array is the primary admin (used for setup:install). All users are synced on every pod restart.
adminUsers:
- username: admin
email: admin@example.com
firstname: Admin
lastname: User
password: securepassword123
- username: developer
email: dev@example.com
firstname: Dev
lastname: User
password: anotherpassword456Note: Roles must be assigned via the Magento admin panel - the CLI doesn't support role assignment.
Instead of passing passwords via --set or values files, you can reference a pre-existing Kubernetes secret:
existingSecret: my-magento-secretsWhen existingSecret is set, the chart will not create its own secret and will reference the existing one instead.
Required secret keys:
| Key | Description |
|---|---|
database-password |
MariaDB root and user password |
<passwordKey> |
Password for each admin user (key name specified in values) |
Creating the secret:
kubectl create secret generic my-magento-secrets \
--namespace magento \
--from-literal=database-password=MyDBPassword123 \
--from-literal=admin-password-admin=AdminPass123 \
--from-literal=admin-password-developer=DevPass456Example values file with existingSecret:
existingSecret: my-magento-secrets
adminUsers:
- username: admin
email: admin@example.com
firstname: Admin
lastname: User
passwordKey: admin-password-admin # References key in secret
- username: developer
email: dev@example.com
firstname: Dev
lastname: User
passwordKey: admin-password-developer
database:
name: magento
user: magento
# password not needed - read from secretYou can inject additional environment variables into the Magento containers using extraEnv. This is useful for API keys or other secrets needed in lifecycle hooks.
extraEnv:
- name: TWO_API_KEY
valueFrom:
secretKeyRef:
name: my-magento-secrets
key: api-key
- name: SOME_OTHER_VAR
value: "static-value"These variables are available in both the init-setup container and the main magento container, so you can reference them in your hooks.postSetup scripts:
hooks:
postSetup: |
php -r "
\$key = getenv('TWO_API_KEY');
// use the key...
"# values-production.yaml
magento:
host: shop.example.com
currency: USD
timezone: America/New_York
adminUsers:
- username: admin
email: admin@example.com
firstname: Admin
lastname: User
password: "" # Set via --set
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
tls:
enabled: true
resources:
magento:
requests:
memory: "2Gi"
cpu: "1000m"
limits:
memory: "4Gi"
cpu: "4000m"helm install magento oci://ghcr.io/brtkwr/charts/magento \
-f values-production.yaml \
--set adminUsers[0].password=$ADMIN_PASSWORD \
--set database.password=$DB_PASSWORD┌─────────────────────────────────────────────────────────┐
│ Pod: magento │
├─────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Magento │ │ MariaDB │ │ OpenSearch │ │
│ │ PHP+Apache │ │ 10.6 │ │ 2.19 │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ git-sync (optional) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────┐
│ PersistentVolumeClaim │
│ /data/mysql │ /data/opensearch │ /data/magento │
└─────────────────────────────────────────────────────────┘
The chart supports git-sync sidecars for live plugin development. Changes pushed to your repo are reflected within 60 seconds.
- Init container clones the repo with
--one-timeto ensure code exists before setup - Sidecar container continuously syncs with
--period=60s --link=codecreates a stable symlink that git-sync maintains (handles worktree hash changes automatically)- Setup scripts create symlinks from
app/code/Vendor/Module→git-sync/plugin/code
gitSync:
plugins:
- name: my-plugin
repo: https://github.com/your-org/your-magento-plugin.git
branch: main
path: app/code/YourVendor/ModulegitSync:
plugins:
- name: custom-shipping
repo: https://github.com/your-org/magento-shipping.git
branch: main
path: app/code/YourVendor/Shipping
- name: custom-payment
repo: https://github.com/your-org/magento-payment.git
branch: develop
path: app/code/YourVendor/PaymentFor private repositories, provide credentials:
gitSync:
credentials:
existingSecret: git-credentials # Secret with 'token' key containing PAT
plugins:
- name: my-plugin
repo: https://github.com/your-org/private-plugin.git
branch: main
path: app/code/YourVendor/ModuleFor production, disable git-sync and bake plugins into your image:
gitSync:
plugins: []Currently, you must manually flush Magento's cache after code changes:
kubectl exec deploy/magento -c magento -- bin/magento cache:flushFuture enhancement: The chart may add --exechook support to automatically flush cache on every sync.
Public images are available at:
| Image | Description |
|---|---|
ghcr.io/brtkwr/magento-base:php8.2 |
PHP 8.2 + Apache + extensions |
ghcr.io/brtkwr/magento:2.4.8 |
Magento + sample data |
To add custom modules or themes:
cd docker/
# Create auth.json with repo.magento.com credentials
cat > auth.json << 'EOF'
{
"http-basic": {
"repo.magento.com": {
"username": "YOUR_PUBLIC_KEY",
"password": "YOUR_PRIVATE_KEY"
}
}
}
EOF
# Build base image
docker buildx build -f Dockerfile.base \
-t your-registry/magento-base:php8.2 --push .
# Build Magento image
docker buildx build \
--secret id=composer_auth,src=auth.json \
-t your-registry/magento:2.4.8 --push .Then update your Helm values:
image:
repository: your-registry/magento
tag: "2.4.8"After deployment:
| URL | Description |
|---|---|
https://your-host/ |
Storefront |
https://your-host/admin |
Admin Panel |
https://your-host/health_check.php |
Health Check |
Default admin credentials are set via adminUsers[0].username and adminUsers[0].password.
The chart persists data across pod restarts using a PVC with the following mounts:
| Mount Path | PVC Subpath | Purpose |
|---|---|---|
/var/www/html/var |
magento/var |
Logs, cache, sessions |
/var/www/html/pub/media |
magento/media |
Uploaded media files |
/var/www/html/app/etc |
magento/etc |
env.php, config.php |
-
init-etccontainer - On first boot, copies defaultapp/etcfrom the Docker image to the PVC. On subsequent boots, preserves existing files (includingenv.php). -
Setup script - Detects if Magento is already installed by checking for
app/etc/env.php:- If exists: Runs
setup:upgrade+di:compile(fast upgrade path) - If missing: Runs full
setup:install+ copies sample data media + runscatalog:images:resize
- If exists: Runs
-
Lifecycle hooks - Custom commands run at various stages (configure stores, rebuild CSS, etc.)
This ensures pod restarts are idempotent - they complete without manual intervention.
The chart provides hooks to inject custom scripts at different stages:
| Hook | When it runs |
|---|---|
hooks.preSetup |
Before any setup (both upgrade and install paths) |
hooks.postUpgrade |
After setup:upgrade on existing installs only |
hooks.postInstall |
After setup:install on fresh installs only |
hooks.postSetup |
After all setup completes (both paths) |
Lifecycle flow:
Fresh install: Existing install:
───────────── ─────────────────
preSetup preSetup
↓ ↓
setup:install setup:upgrade
↓ ↓
postInstall postUpgrade
↓ ↓
postSetup postSetup
Example:
hooks:
postSetup: |
# Create store views
bin/magento store:create 'Hyva Checkout' hyva || true
# Configure payment gateway
bin/magento config:set payment/my_method/active 1 || true
# Rebuild CSS
npm --prefix vendor/hyva-themes/magento2-default-theme/web/tailwind/ run build-prod
bin/magento cache:flushUse || true for commands that may fail during initial setup (e.g., config paths that don't exist yet).
kubectl exec deploy/magento -c magento -- tail -f /var/www/html/var/log/setup.logkubectl exec deploy/magento -c magento -- \
chown -R www-data:www-data /var/www/html/var /var/www/html/generatedkubectl exec deploy/magento -c magento -- bin/magento cache:flushkubectl exec deploy/magento -c magento -- bin/magento indexer:reindexkubectl exec deploy/magento -c magento -- bin/magento module:statusSymptom: Pod stuck in Init:CrashLoopBackOff. OpenSearch init container logs show java.io.IOException: No space left on device. The magento init-setup container hangs on "Waiting for OpenSearch..." until it times out.
Root cause: Magento's file-based full-page cache (var/page_cache) grows unbounded. Crawler bots — especially meta-webindexer from Facebook — crawl all layered navigation filter combinations, creating a unique cache entry per combination. With multiple store views and combinatorial filter params, the cache can grow to 8 GB+ with 70k+ files and fill even a generously-sized PVC.
Diagnosis: the pod is pending so you can't kubectl exec into it. Launch a one-shot debug pod mounted on the same PVC instead:
kubectl run -n <namespace> pvc-debug --restart=Never --image=busybox \
--overrides='{"spec":{"containers":[{"name":"pvc-debug","image":"busybox","command":["sh","-c","du -sh /data/* /data/magento/* /data/magento/var/* 2>/dev/null | sort -hr"],"volumeMounts":[{"name":"data","mountPath":"/data"}]}],"volumes":[{"name":"data","persistentVolumeClaim":{"claimName":"<release-name>"}}]}}' \
&& sleep 5 \
&& kubectl logs -n <namespace> pvc-debug \
&& kubectl delete pod -n <namespace> pvc-debug --wait=falseFix (clear page cache):
kubectl run -n <namespace> pvc-clear --restart=Never --image=busybox \
--overrides='{"spec":{"containers":[{"name":"pvc-clear","image":"busybox","command":["sh","-c","rm -rf /data/magento/var/page_cache/* && echo CLEARED"],"volumeMounts":[{"name":"data","mountPath":"/data"}]}],"volumes":[{"name":"data","persistentVolumeClaim":{"claimName":"<release-name>"}}]}}' \
&& sleep 5 \
&& kubectl logs -n <namespace> pvc-clear \
&& kubectl delete pod -n <namespace> pvc-clear --wait=false \
&& kubectl rollout restart deployment/<release-name> -n <namespace>Prevention: block crawlers at your ingress. robots.txt is advisory only — meta-webindexer ignores it. Example nginx-ingress annotation:
ingress:
annotations:
nginx.ingress.kubernetes.io/server-snippet: |
if ($http_user_agent ~* (bot|crawler|spider|meta-webindexer|googlebot|bingbot|facebookexternalhit)) {
return 403;
}Longer-term: switch Magento's cache backend to Redis (cache.frontend.default.backend) so page cache no longer lives on the PVC, and/or schedule a bin/magento cache:clean full_page cron.
Symptom: Luma homepage renders as a flat stack of text/images instead of the styled hero banner + promo grid. Broken <img> placeholders for /media/wysiwyg/home/*.jpg and/or console errors like:
Refused to apply style from '.../media/styles.css' because its MIME type
('text/html') is not a supported stylesheet MIME type
Root cause: Magento's sample data lives in two separate vendor packages and populates the PVC in different places:
| Package | Populates | Chart-level restore on upgrade |
|---|---|---|
vendor/magento/sample-data-media |
pub/media/catalog/product/**, pub/media/wysiwyg/** |
Yes (chart ≥ 0.11.1, copied via cp -rn in init-setup) |
vendor/magento/module-cms-sample-data/fixtures/styles.css |
pub/media/styles.css — the actual stylesheet that styles .block-promo.home-main, .block-promo-wrapper, etc. |
No — fresh install only, via fixture loader |
The design/head/includes core_config_data row injects <link rel="stylesheet" href="{{MEDIA_URL}}styles.css"> into every page. When the PVC loses pub/media/styles.css (e.g. reprovisioned or restored from a backup that didn't include media), the link resolves to a 404 HTML page, browsers reject it for wrong MIME type, and the Luma CMS grid renders unstyled.
Diagnosis:
# Does the page reference the stylesheet?
curl -s https://your-host/ | grep '/media/styles.css'
# Does the stylesheet actually serve as CSS?
curl -sI https://your-host/media/styles.css
# Expect: 200 + content-type: text/css. If 404 or text/html → broken.
# Is the file on disk?
kubectl exec deploy/magento -c magento -- ls -la /var/www/html/pub/media/styles.css
# Is the head-includes config present?
kubectl exec deploy/magento -c magento -- bash -c \
"mysql -h 127.0.0.1 -u magento -p\"\$MYSQL_PASSWORD\" magento --skip-ssl -e \
'SELECT path, value FROM core_config_data WHERE path=\"design/head/includes\"'"Fix:
POD=$(kubectl get pod -l app.kubernetes.io/name=magento -o name | head -1)
# 1. Restore the stylesheet from the vendor fixture
kubectl exec -c magento $POD -- bash -c \
"cp /var/www/html/vendor/magento/module-cms-sample-data/fixtures/styles.css \
/var/www/html/pub/media/styles.css && \
chown www-data:www-data /var/www/html/pub/media/styles.css"
# 2. Restore the head-include config if missing
kubectl exec -c magento $POD -- bash -c \
"mysql -h 127.0.0.1 -u magento -p\"\$MYSQL_PASSWORD\" magento --skip-ssl -e \
'INSERT INTO core_config_data (scope, scope_id, path, value)
VALUES (\"default\", 0, \"design/head/includes\",
\"<link rel=\\\"stylesheet\\\" type=\\\"text/css\\\" media=\\\"all\\\" href=\\\"{{MEDIA_URL}}styles.css\\\" />\")
ON DUPLICATE KEY UPDATE value=VALUES(value)'"
# 3. Flush config + page cache
kubectl exec -c magento $POD -- \
runuser -u www-data -- bin/magento cache:clean config full_pageHard reload the page (Cmd/Ctrl+Shift+R) to bypass the browser's cached broken-MIME response.
For the same PVC also missing wysiwyg/ or catalog/product/ contents (product thumbnails broken, not just the CMS grid): chart ≥ 0.11.1 handles this automatically via idempotent cp -rn of sample-data-media on init-setup. On older chart versions, restore manually:
kubectl exec -c magento $POD -- bash -c \
"cp -rn /var/www/html/vendor/magento/sample-data-media/* /var/www/html/pub/media/ && \
chown -R www-data:www-data /var/www/html/pub/media && \
runuser -u www-data -- bin/magento catalog:images:resize"- Fork the repository
- Create a feature branch
- Make your changes
- Submit a pull request
MIT License - see LICENSE for details.