This is Part 2 covering plugin development. Part 1 covers setting up Magento on Kubernetes.

With Magento running on Kubernetes, you can use git-sync sidecars for live plugin development - push to your repo and see changes within 60 seconds. This post covers the git-sync setup, Magento CLI quirks you’ll encounter, and debugging tips.

TLDR Link to heading

  • Git-sync: use --link mode to handle worktree hash changes automatically
  • Git-sync: create symlinks in both init and postStart (OOMKill safety)
  • Git-sync: consider --exechook for auto cache flush on code changes
  • Core setup belongs in init containers (blocking), customizations in postStart (async)
  • Bug #35751: design/theme/theme_id can’t be set via CLI for store scope
  • Run app:config:import before config:set to avoid “command unavailable” errors

Magento CLI quirks Link to heading

Bug #35751: Store-level theme config fails Link to heading

Setting a theme for a specific store view should be simple:

bin/magento config:set --scope=stores --scope-code=hyva design/theme/theme_id 5

But this fails with:

The "design/theme/theme_id" path doesn't exist.

This is Magento bug #35751. The CLI validates config paths against system.xml, but design/theme/theme_id isn’t defined there (it uses a different backend model).

The workaround is to use Magento’s WriterInterface directly in a helper function:

magento_config_set_store() {
  local store_code="$1"
  local config_path="$2"
  local config_value="$3"
  runuser -u www-data -- php -r "
    require 'app/bootstrap.php';
    \$bootstrap = \Magento\Framework\App\Bootstrap::create(BP, \$_SERVER);
    \$obj = \$bootstrap->getObjectManager();
    \$storeManager = \$obj->get(\Magento\Store\Model\StoreManagerInterface::class);
    \$configWriter = \$obj->get(\Magento\Framework\App\Config\Storage\WriterInterface::class);
    \$store = \$storeManager->getStore('$store_code');
    \$configWriter->save('$config_path', '$config_value', \Magento\Store\Model\ScopeInterface::SCOPE_STORES, \$store->getId());
  "
}

# Usage
magento_config_set_store hyva design/theme/theme_id 5

Other store-scoped config paths (like amasty_checkout/general/enabled) work fine with the normal config:set command. It’s specifically design/theme/theme_id that needs this workaround.

“Command unavailable” error Link to heading

Sometimes config:set commands fail with:

This command is unavailable right now. To continue working with it please run app:config:import or setup:upgrade command before.

This happens when Magento’s config state is out of sync. Run app:config:import first:

bin/magento app:config:import --no-interaction
bin/magento config:set payment/my_payment/active 1

This is especially common after setup:upgrade if you’re running config commands in a postStart hook.

Amasty and Magento 2.4.8 Link to heading

After upgrading to Magento 2.4.8, the frontend and admin panel started breaking in weird ways. Turns out Amasty modules need a compatibility fix:

composer require amasty/module-mage-248-fix -W
bin/magento setup:upgrade

If you’re using any Amasty extensions, add this to your composer.json before you waste hours debugging.

Two-factor authentication Link to heading

Magento’s 2FA is enabled by default. For staging environments, you’ll want to disable it:

bin/magento module:disable Magento_AdminAdobeImsTwoFactorAuth Magento_TwoFactorAuth

Otherwise you’ll need to set up TOTP for every test admin account.

Third-party Composer plugins blocking installs Link to heading

Some vendors (like Swissup) ship Composer plugins that do version security checks - they block installation if packagist has a newer version than their private repo. This breaks automated installs.

The fix is --no-plugins:

composer require vendor/module --no-plugins

Also watch out for interactive prompts during install. If a module needs domain activation or license keys, you can’t run it in a Dockerfile. Either bake the credentials into auth.json or install manually and commit the result.

Init container vs postStart: The right split Link to heading

After much trial and error, I’ve found the optimal split between init containers and postStart hooks:

Init container (init-setup) - Runs BEFORE Apache starts, blocking:

  • Wait for database/opensearch sidecars
  • Create git-sync symlinks (modules must exist for setup:upgrade)
  • setup:install (fresh) or setup:upgrade (existing)
  • setup:di:compile
  • Create admin users
  • Set deploy mode

postStart hook - Runs AFTER Apache starts, async:

  • Create git-sync symlinks again (safety net for OOMKill restarts)
  • User hooks (Tailwind builds, custom config)
  • Store view creation
  • Payment gateway config
  • app:config:import, indexer:reindex, cache:flush

The key insight: core Magento setup must be blocking (init container), while customizations can be async (postStart). If you put setup:upgrade in postStart, Apache starts serving requests before the schema is migrated, causing 500 errors.

The git-sync symlinks are created in both places intentionally. The init container needs them for setup:upgrade to detect custom modules. The postStart creates them again as a safety net - if the pod restarts due to OOMKill (which skips init containers), the symlinks would be missing otherwise.

Git-sync caveats Link to heading

If you’re using git-sync sidecars for live development (syncing plugins from git repos), there are several gotchas:

Git-sync internally creates worktree directories with commit hashes (.worktrees/{sha}/). If you link directly to these, your symlinks break on every sync.

The solution is git-sync’s --link flag. It creates a stable symlink that git-sync maintains:

args:
  - --repo={{ .repo }}
  - --root=/git
  - --link=code      # Creates /git/code -> .worktrees/{current-sha}/
  - --period=60s

Then your setup script just links to the stable path:

ln -sf /var/www/html/git-sync/gateway/code /var/www/html/app/code/Vendor/Gateway

Git-sync handles updating the internal symlink when commits change - your symlink stays stable.

Auto-flush cache with exechook (optional) Link to heading

Git-sync v4 supports --exechook to run a script after every sync. You can use this to automatically flush Magento’s cache when code changes:

args:
  - --exechook=/scripts/on-sync.sh
#!/bin/bash
# /scripts/on-sync.sh
cd /var/www/html
bin/magento cache:flush

This is optional but saves you from manually flushing cache during development.

Paths must be inside docroot Link to heading

Magento’s filesystem security validator rejects any path outside /var/www/html/. Mount git-sync volumes inside the docroot:

volumeMounts:
  - mountPath: /var/www/html/git-sync/my-plugin  # Inside docroot
    name: plugin-repo

Not /git-sync/my-plugin (outside docroot).

Init containers run once on pod startup. But if your pod restarts due to OOMKill (not a full pod restart), init containers are skipped - only the killed container restarts. Your symlinks would be missing.

The solution: create symlinks in both the init container (for fresh starts) and postStart (as a safety net for OOMKill scenarios).

Private repos need credentials Link to heading

For private git repositories, you need to provide SSH keys or HTTPS credentials:

gitSync:
  credentials:
    existingSecret: git-credentials  # Contains SSH private key

The secret should contain your deploy key or personal access token. The git-sync container mounts this automatically.

Don’t wait for git-sync in postStart Link to heading

Never put a blocking wait for git-sync in your postStart hook:

# DON'T DO THIS - causes deadlock
while [ ! -d /var/www/html/git-sync/plugin ]; do
  sleep 2
done

PostStart hooks block other containers from starting. If git-sync hasn’t populated yet, you’ll deadlock the entire pod. Instead, use init containers for any operations that need git-sync content to exist.

When does this make sense? Link to heading

Magento on Kubernetes is a good fit when you need:

  • Multiple environments (staging, production, sandbox) with consistent deployment
  • Live development via git-sync for rapid iteration on plugins
  • Infrastructure consistency with other Kubernetes services
  • Ingress/cert management through existing cluster infrastructure
  • GitOps deployments via ArgoCD or Flux

For local development only, Docker Compose is simpler. But for anything beyond a single developer’s machine, the investment in Kubernetes pays off through reproducibility and easier scaling.

Debugging tips Link to heading

When health checks fail:

kubectl exec deploy/magento -c magento -- tail -50 /var/www/html/var/log/exception.log

Watch the setup script progress:

kubectl exec deploy/magento -c magento -- tail -f /var/www/html/var/log/setup.log

Force rebuild after changes:

kubectl exec deploy/magento -c magento -- bash -c 'cd /var/www/html && bin/magento setup:di:compile && apachectl -k graceful && bin/magento cache:flush'

Check module status:

kubectl exec deploy/magento -c magento -- bin/magento module:status

Flush cache after git-sync updates:

kubectl exec deploy/magento -c magento -- bin/magento cache:flush

Further reading Link to heading