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
--linkmode to handle worktree hash changes automatically - Git-sync: create symlinks in both init and postStart (OOMKill safety)
- Git-sync: consider
--exechookfor auto cache flush on code changes - Core setup belongs in init containers (blocking), customizations in postStart (async)
- Bug #35751:
design/theme/theme_idcan’t be set via CLI for store scope - Run
app:config:importbeforeconfig:setto 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) orsetup: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:
Use --link mode to handle worktree changes
Link to heading
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).
Create symlinks in both init and postStart Link to heading
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
- Part 1: Deploying Magento to Kubernetes - the deployment journey
- brtkwr/magento-helm - the Helm chart
- git-sync documentation - official git-sync docs
- Magento Bug #35751 - the theme config issue