Skip to main content

How to Run Magento 2 on Kubernetes Without Overpaying

6 min read
Review my Magento cluster

The review sizes each tier and says whether one well-sized server would serve you better.

Most Magento clusters cost more than they need to because every tier gets replicated as if every tier were the bottleneck, when only PHP-FPM ever is. This is the shape that keeps the bill honest, with sessions and cache moved to Valkey or Redis, media moved to object storage instead of a shared filesystem, and cron running on exactly one scheduler because Adobe documents that it can only run on one node. It also covers the case for not doing this at all, since a single well-sized server with a warm standby reaches the same uptime for a single steady store.

Most Magento clusters cost more than they need to because every tier gets replicated as if every tier were the bottleneck. Only one of them ever is. Get the shape right and you pay for the capacity you actually use, with rolling deploys and honest capacity numbers on top. Get it wrong and you are paying for three copies of a search cluster nobody queries, plus a shared filesystem that makes the storefront slower than the single box you left behind.

Only one tier actually scales

A Magento request spends nearly all of its wall clock time inside PHP. nginx accepts the connection, hands the request to PHP-FPM over FastCGI, and waits. The thing you scale is therefore PHP-FPM worker processes, and every other component is sized by data volume rather than by traffic.

PHP-FPM's own ceiling is pm.max_children. The PHP manual is direct about what it means, "This option sets the limit on the number of simultaneous requests that will be served." Multiply that by your replica count and the product is the real concurrency of your storefront. Size the pod memory limit around a measured worker rather than a guess, because a pod that gets OOM killed under load costs you more than one that queues.

The web tier behaves differently. nginx serving static files and proxying FastCGI is cheap and close to constant. Two replicas for availability is usually the entire story, and adding a third buys nothing while requests are still queueing on PHP.

Sessions and cache have to leave the pod

Any pod can be replaced at any moment, so nothing a customer depends on can live on the pod's own disk. Magento supports Redis and Valkey for session storage, configured from the CLI rather than by hand-editing app/etc/env.php.

bin/magento setup:config:set --session-save=redis \
  --session-save-redis-host=valkey-session \
  --session-save-redis-port=6379 \
  --session-save-redis-db=2 \
  --session-save-redis-log-level=4

Adobe recommends keeping the databases apart, with the default cache on database 0, the page cache on 1 and sessions on 2. The session backend also does locking, and those defaults matter once you run many PHP pods. max_concurrency defaults to 6 processes waiting on a lock for a single session, break_after_frontend to 5 seconds and break_after_adminhtml to 30. A storefront that fires several parallel AJAX calls per page view will meet that concurrency number long before it meets your CPU limit.

Media is the part that decides your architecture

Two directories are the whole problem. pub/static is generated output and pub/media is customer data, and they want opposite treatment.

Static content stops being a problem the moment you stop treating it as runtime state. In production mode Magento does not generate static files on demand, so you run the deploy step during the image build and ship the result inside the image.

# Dockerfile, build stage
RUN bin/magento setup:static-content:deploy -f -j 4 en_US

The -j option enables parallel processing with the given number of jobs, and its default is 0, which means no parallelism at all. The -f option is there because by default the tool refuses to run outside production mode.

pub/media is the one that pushes teams into a shared filesystem they did not want. Magento's remote storage module moves it to object storage instead, and it has been available since 2.4.2.

bin/magento setup:config:set --remote-storage-driver="aws-s3" \
  --remote-storage-bucket="my-bucket" \
  --remote-storage-region="eu-central-1"

Read the constraints before committing to it. Remote storage and Magento's database media storage cannot both be enabled, and file operations have to go through the Commerce framework rather than raw PHP file functions, which some third party modules do not do.

If remote storage is not an option you are buying a ReadWriteMany volume, which Kubernetes defines as a volume that "can be mounted as read-write by many nodes". That is a real line item, usually the largest one, and it is also the change that most often makes a cluster slower than the server it replaced.

Cron runs on exactly one node, and Kubernetes has to be told

Adobe states the constraint plainly for multi-server installs, "crontab can run on only one node". Magento's own scheduler is a crontab entry wrapped in #~ MAGENTO START and #~ MAGENTO END markers, installed with bin/magento cron:install and removed with bin/magento cron:remove. On Kubernetes you want neither, because the CronJob object is the scheduler.

# k8s/magento-cron.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: magento-cron
spec:
  schedule: "* * * * *"
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 1
  failedJobsHistoryLimit: 3
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: cron
              image: registry.example.com/magento:2026.08.25
              command: ["php", "bin/magento", "cron:run"]

The concurrencyPolicy: Forbid line is the important one. Kubernetes documents it as skipping the new run when the previous one has not finished, which is what stops a minutely schedule from stacking indexer runs on top of each other. The same page warns that scheduling is only approximate, that two Jobs or none may be created, and that the work itself therefore has to be idempotent.

Message queue consumers are a separate decision. They are driven by the cron_consumers_runner block in app/etc/env.php, where cron_run defaults to true, max_messages defaults to 10000 messages before a consumer terminates, and an empty consumers array runs all of them. Setting cron_run to false and running the consumers as their own Deployment gives you a tier you can scale and restart without touching the storefront.

When one larger server is the honest answer

Kubernetes adds line items a single server does not have. A control plane, a load balancer, object storage or a shared filesystem, and the engineer time to keep all of it patched and upgraded.

Work out your own number rather than trusting anyone's rule of thumb. Take the PHP-FPM concurrency you actually measured at peak, multiply it by the memory a real worker uses on your codebase, and that gives you the node capacity you need. Compare that against your current bill using your provider's own cost report, then add the shared storage and the load balancer, because those two are what surprise people.

The cluster earns its keep when traffic is genuinely spiky, when several stores share the platform, or when a team already runs Kubernetes for everything else. A single store with steady traffic, one region, and nobody on call for the cluster is better served by one well-sized server, a warm standby and a boring deploy script. That is not a lesser answer, it is the cheaper route to the same uptime.

If you want the trade-off analysed rather than assumed, our write-up on whether Magento belongs on Kubernetes at all argues the other side of this question. Once the decision is made and the cluster has to be built and kept alive, that is Kubernetes management, and the storefront tuning that belongs beside it is Magento 2 speed optimization.

Frequently asked questions

Which Magento tier should scale on Kubernetes?

PHP-FPM is the only Magento tier that scales with traffic, because a request spends nearly all of its wall clock time in PHP while nginx waits on FastCGI. Every other component is sized by data volume. The real concurrency of the storefront is pm.max_children multiplied by the replica count, and nginx usually needs just two replicas for availability, since a third buys nothing while requests are queueing on PHP.

How do I store Magento sessions in Valkey or Redis on Kubernetes?

Configure the session backend from the CLI with bin/magento setup:config:set --session-save=redis plus the --session-save-redis-host, --session-save-redis-port and --session-save-redis-db options, for example host valkey-session, port 6379 and database 2, instead of hand-editing app/etc/env.php. Adobe recommends keeping the databases apart, with the default cache on 0, the page cache on 1 and sessions on 2. Sessions have to leave the pod because any pod can be replaced at any moment.

What are the default Magento Redis session locking settings?

Magento's session backend does locking, and by default max_concurrency allows 6 processes to wait on a lock for a single session, break_after_frontend is 5 seconds and break_after_adminhtml is 30. A storefront that fires several parallel AJAX calls per page view meets that concurrency number long before it meets its CPU limit, so the defaults matter once many PHP pods are running.

Should Magento generate static content at runtime on Kubernetes?

No. In production mode Magento does not generate static files on demand, so run the deploy step during the image build and ship the result inside the image, for example with bin/magento setup:static-content:deploy -f -j 4 en_US in a build stage. The -j option sets the number of parallel jobs and defaults to 0, meaning no parallelism, and -f is needed because by default the tool refuses to run outside production mode.

How do I store Magento pub/media without a shared filesystem on Kubernetes?

Use Magento's remote storage module, available since 2.4.2, which moves pub/media to object storage, for example with bin/magento setup:config:set --remote-storage-driver="aws-s3" plus a bucket and region. Remote storage and database media storage cannot both be enabled, and file operations must go through the Commerce framework instead of raw PHP file functions, which some third party modules do not do. The alternative is a ReadWriteMany volume, usually the largest line item and a common reason a cluster ends up slower than the server it replaced.

How do I run Magento cron on Kubernetes?

Adobe documents that crontab can run on only one node, so on Kubernetes the CronJob object becomes the scheduler and the crontab from bin/magento cron:install is left out. Run php bin/magento cron:run every minute with concurrencyPolicy set to Forbid, which skips a new run while the previous one is unfinished and stops indexer runs stacking up. Scheduling is only approximate and two Jobs or none may be created, so the work has to be idempotent.

Should Magento message queue consumers run from cron on Kubernetes?

Consumers are a separate decision, driven by the cron_consumers_runner block in app/etc/env.php. There cron_run defaults to true, max_messages defaults to 10000 messages before a consumer terminates, and an empty consumers array runs all of them. Setting cron_run to false and running the consumers as their own Deployment gives a tier that can be scaled and restarted without touching the storefront.

Is Kubernetes worth it for a single Magento store?

A single Magento store with steady traffic, one region and nobody on call for the cluster is better served by one well-sized server, a warm standby and a boring deploy script, the cheaper route to the same uptime. Kubernetes earns its keep when traffic is genuinely spiky, when several stores share the platform or when a team already runs Kubernetes for everything else. To compare, multiply measured peak PHP-FPM concurrency by real worker memory for node capacity, then add shared storage and the load balancer, the two costs that surprise people.

Read how we handle it in Cloud & Server Management.