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/magento commands 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:resize after 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/etc from image to PVC on first boot (preserves env.php on 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:

ImageContentsBuilt by
ghcr.io/brtkwr/magento-base:php8.2PHP 8.2 + Apache + extensionsPublic repo CI
ghcr.io/brtkwr/magento:2.4.8Vanilla Magento + sample dataPublic repo CI
your-registry/magento:2.4.8Vanilla Magento + proprietary extensionsPrivate 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/metadata at the start
  • Use || true for 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 extensions
  • ghcr.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?

ChangeAction
Docker image changes (new modules)helm upgrade - pulls new image
Helm values changes (config, postScript)helm upgrade - reruns setup
Database schema changeshelm upgrade - runs setup:upgrade
Storage class changeDelete PVC, then upgrade
Fresh install from scratchDelete 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:upgrade fails 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