This guide covers deploying Magento 2.4.8 to Kubernetes with MariaDB and OpenSearch as sidecars.
The key benefit: disposable environments. Database corrupted? Config in a weird state? Just delete the PVC and redeploy - you’re back to a clean Magento install with sample data in under 5 minutes. No more debugging broken dev environments.
Part 2 covers developing Magento plugins using git-sync for live code updates.
TLDR Link to heading
- Don’t wait for sidecars in postStart hooks (deadlock)
- Magento 2.4.8 dropped Elasticsearch 7 support entirely
- Use startup probes with generous timeouts
- Run
bin/magentocommands as www-data from the start, not just chown at the end - Configure SSL offloader header or you’ll get redirect loops
- Enable store codes for multi-store URL paths
- Reload Apache after DI compile to clear opcache
- Sample data needs
catalog:images:resizeafter copying media files - Or just use my Helm chart and skip the pain
The architecture Link to heading
This setup uses a Magento image with a couple of proprietary extensions. The deployment is a single pod with five containers plus two init containers.
Init containers:
- init-data: Creates PVC directories for MySQL, OpenSearch, and Magento
- init-etc: Copies
app/etcfrom image to PVC on first boot (preservesenv.phpon restarts)
Containers:
- magento: PHP 8.2 + Apache serving Magento 2.4.8
- database: MariaDB 10.6 sidecar
- opensearch: OpenSearch 2.19.0 sidecar
- git-sync-gateway: Syncs a custom payment plugin for live development
- git-sync-hyva: Syncs a Hyva extension for live development
Images:
| Image | Contents | Built by |
|---|---|---|
ghcr.io/brtkwr/magento-base:php8.2 | PHP 8.2 + Apache + extensions | Public repo CI |
ghcr.io/brtkwr/magento:2.4.8 | Vanilla Magento + sample data | Public repo CI |
your-registry/magento:2.4.8 | Vanilla Magento + proprietary extensions | Private CI |
PVC mounts (persisted across restarts):
/var/www/html/var- logs, cache, sessions/var/www/html/pub/media- uploaded media files/var/www/html/app/etc-env.php,config.php(Magento config)
All containers share a single PVC with subPaths. This keeps things simple for a staging environment where you don’t need the complexity of separate StatefulSets.
Building the image Link to heading
Two-layer Docker architecture Link to heading
Magento Docker builds are slow. Really slow. A full build with composer install can take 10+ minutes.
I’d recommend splitting into two images:
Base image (rarely changes, ~5 min to build):
- PHP 8.2 + Apache with all required extensions
- System packages (libfreetype, libjpeg, mysql-client, git)
- Apache modules and Magento-specific config
- PHP config (
memory_limit=2G,max_execution_time=1800) - Composer binary
App image (changes frequently, ~3 min with cache):
- Based on your base image
composer.json+composer install(use BuildKit cache mounts)- Any additional plugins or themes
# Base image - Dockerfile.base
FROM php:8.2-apache
RUN apt-get update && apt-get install -y \
libfreetype6-dev libjpeg62-turbo-dev libpng-dev \
libzip-dev libicu-dev libxslt-dev mysql-client git
RUN docker-php-ext-install pdo_mysql gd intl xsl zip bcmath sockets soap
# ... more extensions and config
# App image - Dockerfile
FROM your-registry/magento-base:php8.2
COPY composer.json composer.lock ./
RUN --mount=type=cache,target=/root/.composer/cache \
--mount=type=ssh \
composer install --no-dev --prefer-dist
COPY . .
The cache mount for Composer is crucial - it means subsequent builds only download changed dependencies. Combined with the base image, rebuilds after code changes take under 3 minutes instead of 10+.
Sample data authentication nightmare Link to heading
Running bin/magento sampledata:deploy at runtime kept failing. The problem? Composer tries to authenticate with all repositories in composer.json - including third-party GitLab repos that require SSH keys. Those keys are only available during docker build (via --ssh default), not in the running container.
My first attempt was creating test products via PHP scripts. It worked but was tedious.
The proper solution: add sample data modules directly to composer.json:
{
"require": {
"magento/sample-data-media": "100.*",
"magento/module-bundle-sample-data": "100.*",
"magento/module-catalog-sample-data": "100.*",
"magento/module-catalog-rule-sample-data": "100.*",
"magento/module-cms-sample-data": "100.*",
"magento/module-configurable-sample-data": "100.*",
"magento/module-customer-sample-data": "100.*",
"magento/module-downloadable-sample-data": "100.*",
"magento/module-grouped-product-sample-data": "100.*",
"magento/module-msrp-sample-data": "100.*",
"magento/module-offline-shipping-sample-data": "100.*",
"magento/module-product-links-sample-data": "100.*",
"magento/module-review-sample-data": "100.*",
"magento/module-sales-rule-sample-data": "100.*",
"magento/module-sales-sample-data": "100.*",
"magento/module-swatches-sample-data": "100.*",
"magento/module-tax-sample-data": "100.*",
"magento/module-theme-sample-data": "100.*",
"magento/module-widget-sample-data": "100.*",
"magento/module-wishlist-sample-data": "100.*"
}
}
This way, sample data modules are installed during docker build when SSH authentication is available. At runtime, setup:upgrade detects and installs the sample data automatically. No runtime Composer calls needed.
Sample data media files Link to heading
Got the sample data installed, but all product images were 404s. Turns out the sample data modules install product data but not the media files.
You need to copy them from vendor and regenerate the image cache:
cp -r vendor/magento/sample-data-media/* pub/media/
bin/magento catalog:images:resize
Note: The Helm chart handles this automatically - it copies sample data media and runs catalog:images:resize during init-setup.
Getting pods running Link to heading
The postStart deadlock Link to heading
This one cost me hours. I had a postStart hook that waited for the git-sync sidecars to populate before continuing:
lifecycle:
postStart:
exec:
command:
- /bin/bash
- -c
- |
while [ ! -L /var/www/html/git-sync/gateway/code ]; do
sleep 2
done
This deadlocks the entire pod. PostStart hooks block other containers from starting. So the Magento container waits for git-sync, but git-sync waits for Magento’s postStart to complete. The pod sits in PodInitializing forever.
The fix: Move all waits to a background setup script that runs after the container starts. PostStart should only do synchronous, quick operations.
The container startup race Link to heading
All sidecar containers start simultaneously, but MariaDB takes a few seconds to initialise. Without handling this, setup:install fails because there’s no database.
Use startup probes with generous timeouts:
startupProbe:
httpGet:
path: /health_check.php
port: http
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 30 # 150 seconds total
And wait in your setup script:
# Wait for database
until mysql -h 127.0.0.1 -u magento -p"$MYSQL_PASSWORD" --skip-ssl -e "SELECT 1" &>/dev/null; do
echo "Waiting for MariaDB..."
sleep 5
done
# Wait for OpenSearch
until curl -s http://127.0.0.1:9200/_cluster/health &>/dev/null; do
echo "Waiting for OpenSearch..."
sleep 5
done
Note the --skip-ssl flag - the MariaDB sidecar doesn’t have SSL configured, and the MySQL client defaults to requiring it.
Making restarts idempotent Link to heading
Pod restarts happen. Nodes get preempted, deployments get upgraded, things crash. Your setup script needs to handle restarts gracefully without manual intervention.
Note: If you’re using my Helm chart, all of this is handled automatically. This section explains what’s happening under the hood.
Persist config with init containers:
Magento stores its install state in app/etc/env.php. Without persistence, every restart is a fresh install. The Helm chart uses an init container to copy config to a PVC on first boot:
initContainers:
- name: init-etc
image: your-magento-image
command: ['sh', '-c', 'if [ ! -f /pvc/etc/env.php ]; then cp -r /var/www/html/app/etc/* /pvc/etc/; fi']
volumeMounts:
- name: magento-etc
mountPath: /pvc/etc
Then mount that PVC to app/etc in the main container. On subsequent boots, env.php exists and the setup script can run setup:upgrade instead of setup:install.
Other things the chart handles:
- Clean up stale generated code -
rm -rf generated/code generated/metadataat the start - Use
|| truefor commands that may fail - store creation fails if store exists, that’s fine - Handle git-sync worktree path changes - the worktree hash changes on restart
Git-sync creates paths like /var/www/html/git-sync/plugin/.worktrees/{commit-sha}/. After a restart, the old path is gone. Any generated code referencing it will break. The chart cleans it up and regenerates.
Permission chaos Link to heading
The setup script runs as root, but Apache runs as www-data. After setup completes, you’ll get 500 errors because Apache can’t write to var/, generated/, or pub/static/.
The fix isn’t just to chown at the end - run all bin/magento commands as www-data from the start:
mage() {
runuser -u www-data -- bin/magento "$@"
}
export -f mage
# Now use mage instead of bin/magento
mage setup:upgrade
mage cache:flush
This prevents files from being created with root ownership in the first place. If you run as root and chown afterwards, you’ll still hit race conditions where Apache tries to write before the chown completes.
Also, disable maintenance mode at the end of setup:
mage maintenance:disable
Magento enables it during setup:install and setup:upgrade, and it’s easy to forget this when you’re scripting everything.
Product images still broken after sample data Link to heading
Even after copying the sample data media files, product images may still show as broken. The media files exist, but Magento serves resized/cached versions that don’t exist yet.
You must run the image resize command:
bin/magento catalog:images:resize
This generates all the thumbnail and gallery variants. It takes about 10 seconds for the sample data (~44 images). Without this step, you’ll see the product data but broken image placeholders everywhere.
Note: The Helm chart now runs this automatically during fresh installs when sample data is detected.
Getting the app working Link to heading
Search engine roulette Link to heading
My setup:install command kept failing with:
Search engine 'elasticsearch7' is not an available search engine.
Turns out Magento 2.4.8 completely removed Elasticsearch 7 support. Your options are now:
opensearch(OpenSearch 2.x)elasticsearch8(Elasticsearch 8.x)
I’d recommend OpenSearch - it’s what Adobe seems to be pushing towards. Just update your setup command:
bin/magento setup:install \
--search-engine=opensearch \
--opensearch-host=127.0.0.1 \
--opensearch-port=9200
SSL offloader redirect loop Link to heading
Once I got everything running, the site kept returning:
ERR_TOO_MANY_REDIRECTS
Behind a load balancer that terminates SSL, Magento can’t detect that the original request was HTTPS. It keeps redirecting HTTP → HTTPS → HTTP → forever.
The fix is to tell Magento to read the protocol from the proxy header:
bin/magento config:set web/secure/offloader_header X-Forwarded-Proto
This is a common gotcha with any PHP app behind a reverse proxy, but Magento’s error message gives you zero hints about what’s wrong.
Store codes in URLs Link to heading
For multi-store setups with URLs like /hyva/, /amasty/, /fire/, you need to enable store codes:
bin/magento config:set web/url/use_store 1
Without this, those paths return 404 instead of the store view. Another config that’s easy to miss.
DI compile and opcache Link to heading
After running setup:di:compile, the site broke with “Application is not installed” errors. The compiled code was there, but Apache’s opcache was still serving the old files.
You need to reload Apache after DI compile:
bin/magento setup:di:compile
apachectl -k graceful # Clear opcache
bin/magento cache:flush
The graceful restart clears the opcache without dropping active connections. Add this to your setup scripts or you’ll be scratching your head wondering why changes aren’t taking effect.
The Helm chart Link to heading
After all this pain, I packaged everything into a public Helm chart so you don’t have to:
helm install magento oci://ghcr.io/brtkwr/charts/magento \
--set ingress.host=magento.example.com \
--set admin.password=secretpassword
The chart includes MariaDB and OpenSearch as sidecars, handles the startup race conditions, and configures the SSL offloader automatically. It’s opinionated but saves you from most of the gotchas I’ve documented here.
I’ve also published public Docker images:
ghcr.io/brtkwr/magento-base:php8.2- PHP 8.2 + Apache + all extensionsghcr.io/brtkwr/magento:2.4.8- Vanilla Magento with sample data
Check out the repo: brtkwr/magento-helm
When to delete the PVC Link to heading
One thing that confused me initially: when do you need a fresh PVC vs just upgrading?
| Change | Action |
|---|---|
| Docker image changes (new modules) | helm upgrade - pulls new image |
| Helm values changes (config, postScript) | helm upgrade - reruns setup |
| Database schema changes | helm upgrade - runs setup:upgrade |
| Storage class change | Delete PVC, then upgrade |
| Fresh install from scratch | Delete PVC, then install |
You don’t need to delete the PVC for:
- Adding new Composer packages to the Docker image
- Changing post-install scripts (store creation, config)
- Updating git-sync repos
You do need to delete the PVC for:
- Changing storage class (e.g., standard → premium SSD)
- Corrupted database or need fresh sample data
- Minor version upgrades (2.4.6 → 2.4.8) - despite what the docs suggest,
setup:upgradefails with DI errors due to stale generated code
Next steps Link to heading
That covers getting Magento deployed and running. In Part 2, I cover:
- Magento CLI quirks and workarounds (Bug #35751, “command unavailable” errors)
- Init container vs postStart: the right architecture split
- Git-sync for live development (–link mode, exechook, OOMKill safety)
- Debugging tips
Further reading Link to heading
- Magento 2.4.8 Release Notes - includes the search engine changes
- Kubernetes postStart and preStop hooks - understanding the lifecycle
- OpenSearch documentation - if you’re migrating from Elasticsearch