Deploy Magento 2 to Kubernetes 📦 🐳

Magento 2 currently does not have a fully cloud native approach, and documentation lacks on this topic. We'll see below how to deploy immutable Docker images of Magento 2 on Kubernetes, make it possible to build once (ie. a tag) and deploy many times (ie. staging, preproduction, production).

This post is not a detailed how-to guide, but rather a compilation of solutions to known difficulties you'll face trying to deploy Magento to Kubernetes.

At the end of this post, you'll also find a list of many security/observability/availability related recommendations. 

Building Docker image

Once the code copied and dependencies installed via Composer, Magento requires two major commands to be ran during build, which will not work out-of-the-box in this context (no access to database or external service):

  • bin/magento setup:di:compile
  • bin/magento setup:static-content:deploy

See our clickandmortar/magento-kubernetes GitHub repository for a complete Dockerfile example.

setup:di:compile

When ran with an existing app/etc/env.php, this command will try to connect to the database and/or the cache. To get around this, move it to a temporary location:

RUN mv app/etc/env.php app/etc/env.php.orig \

    && bin/magento setup:di:compile

Keep your app/etc/config.php in place, it is required for Magento to know which modules are enabled.

setup:static-content:deploy

This command needs to know the websites structure to work properly, you'll need to dump your database config to app/etc/config.php before building the image:

bin/magento app:config:dump

This file containing the whole Magento config may be reverted to it's simpler version containing only modules once static content is deployed, otherwise you wont be able to change any setting on your deployed environment (except using environment variables, which is recommended).

I'd recomment to keep both files:

  • app/etc/config.php containing the modules list
  • app/etc/config.docker.php containing the whole config, only useful at build time

Then within your Dockerfile:

RUN mv app/etc/config.php app/etc/config.php.orig \
    && mv app/etc/config.docker.php app/etc/config.php \

    && bin/magento setup:static-content:deploy \
    --max-execution-time=3600 \
    -t my/theme -t Magento/backend \
    ja_JP de_DE es_ES ... \

    && mv app/etc/config.php.orig app/etc/config.php

 

For both commands above, you may need to increase the memory limit:

RUN php -d memory_limit=1024M bin/magento setup:...

 

Dynamic configuration

Magento allows to override any system configuration using environment variables, following the following patterns, replacing the slash character of config paths with double underscores:

  • CONFIG__DEFAULT__DEV__JS__MERGE_FILES: sets value of dev/js/merge_files on default scope
  • CONFIG__STORES__MYSTORECODE__WEB__UNSECURE__BASE_URL: sets value of web/unsecure/base_url for store with code mystorecode
  • CONFIG__WEBSITES__MYWEBSITECODE__WEB__SECURE__BASE_URL: sets value of web/secure/base_url for website with code mywebsitecode

Note that these environment values take precedence over values in config.php and the database.

⚠️  Note that you need to use encrypted values for sensitive configuration (ie. payment gateway credentials) environment variables.

However, Magento does not offer a similar mechanism for values of env.php. Easy way to be able to define database connection info and such using environment variables, is taking advantage of PHP's getenv() function:

<?php
    ...
    'db' => [
        'table_prefix' => '',
        'connection' => [
            'default' => [
                'host' => getenv('MAGENTO_DATABASE_HOST'),
                'dbname' => getenv('MAGENTO_DATABASE_NAME'),
                'username' => getenv('MAGENTO_DATABASE_USERNAME'),
                'password' => getenv('MAGENTO_DATABASE_PASSWORD'),
                'model' => 'mysql4',
                'engine' => 'innodb',
                'initStatements' => 'SET NAMES utf8;',
                'active' => '1'
            ]
        ]
    ],
    ...

 

Deploying to Kubernetes

Architecture

The schema below is an overview of the (simplified) target architecture of Magento running in Kubernetes:

Magento Kubernetes simplified architecture.
Usage of cloud managed services is recommended whenever possible.
 

 

And below is a detailed view of the Magento Deployment part:

As you can see, Magento requires volumes to be shared between running Pods:

  • media: contents of the pub/media directory, which holds product images for instance
  • static cache: contents of the pub/static/_cache directory, where the minified/compressed CSS/JS assets will be created

To achieve this, you'll need a StorageClass allowing mounting a PersistentVolume with the ReadWriteMany access mode. You may use Kubernetes NFS provisioner or a managed service offering a RWX StorageClass (Google Cloud Filestore, Alicloud NAS, etc.).

Deploying using Helm

Most practical way to deploy Magento to Kubernetes is using Helm. It offers advanced templating features, release management and hooks.

When deploying a new release, Magento requires the setup:upgrade command to be executed, before the application is effectively deployed.

For this purpose, usage of Helm chart hooks is quite handy, as shown using the annotations in this example:

# upgrade-job.yaml

apiVersion: batch/v1
kind: Job
metadata:
  name: "{{ include "magento.fullname" . }}-pre-upgrade-job"
  labels:
    {{- include "magento.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": pre-upgrade
    "helm.sh/hook-weight": "1"
    "helm.sh/hook-delete-policy": before-hook-creation
spec:
  template:
    metadata:
      labels:
        {{- include "magento.labels" . | nindent 8 }}
    spec:
      restartPolicy: Never
      containers:
        - name: php
          image: {{ .Values.image.name }}:{{ .Values.image.tag }}
          command: ["bin/magento", "setup:upgrade", "--keep-generated"]
          envFrom:
            - configMapRef:
                name: magento-cm
            - secretRef:
                name: magento-secret

You may also need to define a post-install,post-upgrade hook to clear the cache once the release is fully deployed.

The rest of the Helm chart required to deployed Magento is quite straightforward, a good start is using Helm create command.

Still a lot to do

The deployment architecture described above is quite basic, and requires additional work on the following topics:

Security

Monitoring & observability

  • Expose Prometheus metrics for all your applications (see https://github.com/hipages/php-fpm_exporter for php-fpm)
  • Monitor and analyse your Prometheus metrics using Grafana, AlertManager
  • Use Monolog features to format your logs as JSON and add necessary metadata for further analysis
  • Write your logs to standard output / error instead of files under var/log, and forward them to a centralized analysis tool (Elasticsearch + Kibana) using FluentD for instance
  • Use an OpenTracing implementation (Jaeger, Zipkin), especially when working in distributed / headless mode

Availability & fault tolerance

  • For long running processes, as queue consumers, you need them to be able to stop gracefuly when receiving a SIGTERM signal from the Docker daemon ; this can be achieved by catching signals (see https://github.com/magento/community-features/issues/280) ; for nginx, see https://pracucci.com/graceful-shutdown-of-kubernetes-pods.html
  • Define Pod anti-affinity rules to avoid having all your Pods deployed on the same Node
  • Always use a Pod Disruption Budget to ensure keeping enough Pods running at any time
  • Define readiness and liveness probes on your workloads:
    • Pod not ready: Kubernetes does not send trafic to the Pod (removed from Service's Endpoint)
    • Pod not alive: Kubernetes restarts the Pod
  • Fine-tune your rollingUpdate strategy to ensure keeping enough Pods during deployment of a new release
  • When possible, deploy your Kubernetes cluster across multiple zones within a region (beware of volumes being attached to region once created)
  • Use blue-green or canary pattern when deploying, using a service mesh and/or Flagger or any other tool
  • Use a Horizontal Pod Autoscaler policy to auto-scale your workload, and your cluster (the latter requires a cluster autoscaler controller)
  • Use managed services offered by your cloud provider whenever possible, especially for:
    • MySQL: Google CloudSQL, AWS RDS/Aurora, Alibaba Cloud ApsaraDB RDS, etc.
    • Elasticsearch: Elasticloud, AWS Elasticsearch Service, Alibaba Cloud Elasticsearch, etc.
    • Redis: Google MemoryStore, AWS ElastiCache, Alibaba Cloud ApsaraDB Redis, etc.
  • Frequently backup your data (using Velero for instance)