Cloud: Google Cloud, Azure and Alternatives

This content was generated with the assistance of AI and should be verified against the official documentation of each platform before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

This page was written against PrestaShop 9.2.0 (2026-09-30) on PHP 8.5, Kubernetes 1.36 and 1.37, MySQL 8.4 on Cloud SQL and on Azure Database for MySQL Flexible Server, Memorystore for Valkey 9.x and Azure Managed Redis, and was checked on 2026-10-29. It names no prices: sizes are starting points to measure on your own traffic. The Kubernetes manifests were validated against the Kubernetes 1.36 schemas with kubeconform (custom resources were checked for syntax only), the shell with shellcheck and the JSON and YAML with a parser. No cloud account was used: the gcloud, az and aws commands are composed from the Google Cloud, Azure and AWS pages of this site and from the vendors' documentation and have not been run, so verify each flag with --help on the CLI version you install. The statements about PrestaShop’s own behaviour come from reading the 9.2.0 source and were not reproduced on every cloud, so each one says what to test.

The other pages of this section build one architecture on AWS: Pocket Coffee on EKS in eu-west-1, with the database in RDS, the shared files on EFS, the SQL cache and sessions in ElastiCache and the secrets in Secrets Manager (start with Cloud: Reference Architecture and Cloud: EKS Infrastructure and Helm Deployment). Nothing in that design belongs to AWS except the services that fill each tier. This page maps the tiers to Google Cloud and Azure, shows the commands and manifests that differ, and then compares the options that do not use Kubernetes. The shared fundamentals are on the cloud pages of this site and are linked where they apply: GKE Clusters, Cloud SQL, AKS Clusters and PostgreSQL and MySQL on Azure.

What stays the same, what changes

The container image of Cloud: Production Container Image (PrestaShop 9.2.0 code, nginx and PHP-FPM in two containers, the admin folder renamed to admin-pocketcoffee at build), the Helm chart pocketcoffee-prestashop (Deployment pocketcoffee-web, CronJob pocketcoffee-cron, Jobs pocketcoffee-install and pocketcoffee-migrate), the parameters.php that reads its settings from the environment, bin/console and the release pipeline of Cloud: Releases and Upgrades are the same on every conformant Kubernetes. What changes is in five places:

  • How pods get cloud credentials: EKS Pod Identity on AWS, Workload Identity Federation for GKE on Google Cloud, Microsoft Entra Workload ID on Azure.

  • How PrestaShop reaches the database, and whether the connection can be encrypted (see the next section).

  • Which service backs each of the two caches: the SQL cache needs Memcached, the sessions need a Redis-protocol server, and the clouds differ in what they sell.

  • Which shared file system holds img/, upload/ and download/: EFS, Filestore or Azure Files, because PrestaShop has no core object-storage option for media.

  • The HTTP entry: AWS Load Balancer Controller, GKE Gateway, or an Azure gateway.

Decision tree for running PrestaShop: a hosting offer, Kubernetes, ECS on Fargate, Cloud Run, Azure Container Apps or an EC2 Auto Scaling group

Three PrestaShop facts that shape every cloud

Three properties of PrestaShop 9.2.0, each read from the source, decide more than the choice of cloud does.

The database client has no TLS settings. DbPDO::getPDO() in classes/db/DbPDO.php builds a DSN from host, port or unix_socket, dbname and the charset, and passes only a timeout, the buffered-query, the SET NAMES and the multi-statement attributes; DbMySQLi calls real_connect() with host, user, password and database only; and the Doctrine connection of the Symfony part (app/config/doctrine.yml) lists the driver, the credentials and two driver options. None of them sets a CA file or an SSL mode. The PHP MySQL native driver does not switch TLS on by itself, so against a server that refuses unencrypted connections (the default on Azure Database for MySQL, an option on Cloud SQL and on RDS) the connection fails: the database page ran the same options against a MySQL 8.4.11 server that required TLS and got error 3159, "Connections using insecure transport are prohibited". Repeat the test in staging for your own service before you pick the database settings. The ways out are a local proxy that speaks TLS to the server (the Cloud SQL Auth Proxy does exactly this), a server setting that accepts connections without TLS on a private network, or a class override and a Doctrine option that add the driver’s SSL attributes, which couples the shop to core internals and has to be re-tested on every upgrade. The Cloud: Managed Database (RDS and Aurora) page covers the same question for RDS.

The SQL cache backends are APCu, Memcache, Memcached and XCache. classes/cache/ has CacheApc, CacheMemcache, CacheMemcached and CacheXcache and nothing for Redis or Valkey. The only backend that is shared between pods and servers is Memcached (the memcached PHP extension, servers stored in the table ps_memcached_servers with the columns ip, port and weight). APCu is private to each PHP pool (see Performance and Caching), so with several pods each one would keep its own copy of the cached query results and invalidate only its own. The SQL cache is optional: with it off, the database’s own buffer pool still serves repeated reads. Google Cloud and Azure differ in what they offer for Memcached, covered below.

Sessions and media are on the file system and in the PHP session handler. The back office uses PHP’s native session handler (framework.session.handler_id: ~ in app/config/config.yml), so a Redis-protocol server configured in php.ini or the FPM pool is the way to share it; front and back office login state also sits in the encrypted cookie, so cookie_key and cookie_iv must be identical on all pods (see Cloud: Cache, Sessions and the Back Office in a Cluster). There is no S3 or Cloud Storage option in core for img/, upload/ and download/, so each cloud’s file service is the default (see Cloud: Shared Media and Writable Paths); the media-server settings only let a CDN host serve the files.

Tier AWS (the reference) Google Cloud Azure

Cluster

EKS Auto Mode, pocketcoffee-prod

GKE Autopilot or Standard, regional

AKS Standard (or Automatic) with zones

Database

RDS for MySQL 8.4, Multi-AZ

Cloud SQL for MySQL 8.4, regional availability

Azure Database for MySQL Flexible Server 8.4, zone-redundant HA

SQL cache (Memcached)

ElastiCache for Memcached

Memorystore for Memcached is deprecated: Memcached on GKE, or no SQL cache

No managed Memcached service: Memcached on AKS, or no SQL cache

Sessions

ElastiCache for Valkey

Memorystore for Valkey

Azure Managed Redis

Shared files

EFS (pocketcoffee-shared)

Filestore

Azure Files over NFS (or SMB)

Secrets

Secrets Manager + external-secrets

Secret Manager + external-secrets

Key Vault + external-secrets or the CSI driver

Registry

ECR pocketcoffee/prestashop

Artifact Registry

Azure Container Registry

Entry

ALB (AWS Load Balancer Controller)

GKE Gateway

Application routing Gateway API, Application Gateway for Containers

CDN for media

CloudFront

Cloud CDN

Front Door

Pod credentials

EKS Pod Identity

Workload Identity Federation

Entra Workload ID

Two database facts limit the choice. Cloud SQL offers MySQL, PostgreSQL and SQL Server, so there is no MariaDB on Google Cloud. Azure Database for MariaDB was retired on 2025-09-19 (creation of new servers had stopped in March 2024); it is not an option, and Microsoft’s migration path is Azure Database for MySQL Flexible Server. A shop whose source database is MariaDB therefore moves to MySQL 8.4 on both clouds, and the dump must be checked for MariaDB-only features first (see Cloud: Migrating from a Single Server). PrestaShop 9.2.0 documents MySQL 5.7 or MariaDB 10.2 as minimums and was tested here on MySQL 8.4 (see Versions and Support), so MySQL 8.4 is the version to choose on both clouds.

Google Cloud: GKE, Cloud SQL, Filestore and Memorystore

The cluster

An Autopilot cluster removes node management, enables the Filestore CSI driver and Workload Identity Federation for GKE by default, and is the closest equivalent of EKS Auto Mode; a Standard cluster keeps node pools under your control and needs the Filestore driver enabled (gcloud container clusters update <cluster> --update-addons=GcpFilestoreCsiDriver=ENABLED). The cluster commands are on GKE Clusters. The PrestaShop-specific point is that PHP-FPM pods need CPU and memory requests equal to what the autoscaler measures against, which Autopilot also requires.

Cluster and registry (Google Cloud)
#!/usr/bin/env bash
set -euo pipefail

REGION=europe-west1

gcloud services enable container.googleapis.com artifactregistry.googleapis.com \
  sqladmin.googleapis.com secretmanager.googleapis.com file.googleapis.com \
  memorystore.googleapis.com

gcloud artifacts repositories create pocketcoffee \
  --repository-format=docker --location="$REGION"

gcloud container clusters create-auto pocketcoffee-prod \
  --location="$REGION" \
  --release-channel=regular

gcloud container clusters get-credentials pocketcoffee-prod --location="$REGION"
kubectl create namespace pocketcoffee

The image is pushed to europe-west1-docker.pkg.dev/<project-id>/pocketcoffee/prestashop:<git-sha>, and the release workflow changes in two lines: the OIDC step uses google-github-actions/auth with Workload Identity Federation (see Identity and Access) and the cluster login is gcloud container clusters get-credentials. The helm upgrade step is identical.

Cloud SQL for MySQL and the Auth Proxy

Cloud SQL runs MySQL 8.4 (the default version at the check date) with a primary and a standby in two zones when --availability-type=REGIONAL, a private address through private services access, point-in-time recovery from the binary log and automatic storage growth. The Auth Proxy is the natural connection for PrestaShop precisely because of the first fact above: the proxy authenticates with the pod’s Google identity, encrypts the traffic itself and listens on a local port, so PrestaShop connects to 127.0.0.1 and never needs a TLS setting.

Cloud SQL instance, database and users
#!/usr/bin/env bash
set -euo pipefail

PROJECT_ID="$(gcloud config get-value project)"
REGION=europe-west1
NETWORK=default

gcloud sql instances create pocketcoffee-prod \
  --database-version=MYSQL_8_4 \
  --edition=ENTERPRISE \
  --tier=db-custom-2-7680 \
  --region="$REGION" \
  --availability-type=REGIONAL \
  --network="projects/$PROJECT_ID/global/networks/$NETWORK" \
  --no-assign-ip \
  --ssl-mode=ENCRYPTED_ONLY \
  --storage-type=SSD --storage-size=100GB --storage-auto-increase \
  --enable-bin-log \
  --retained-backups-count=14 --retained-transaction-log-days=7 \
  --backup-start-time=03:00 \
  --deletion-protection

gcloud sql databases create pocketcoffee \
  --instance=pocketcoffee-prod \
  --charset=utf8mb4 --collation=utf8mb4_unicode_ci

# The passwords come from the operator's environment, never from a file in Git.
gcloud sql users create ps_app --instance=pocketcoffee-prod --host=% --password="$PS_APP_PASSWORD"
gcloud sql users create ps_migrate --instance=pocketcoffee-prod --host=% --password="$PS_MIGRATE_PASSWORD"

The users are plain MySQL users: give ps_app the privileges PrestaShop needs at runtime and ps_migrate the schema privileges that module and core upgrades need, with SQL GRANT (the lists are on the database page of this section). ENCRYPTED_ONLY is compatible with the proxy, which encrypts on its own; it would reject a direct connection from PrestaShop, which is the reason the proxy is not optional with this setting. If you want a direct connection on the private address, create the instance with --ssl-mode=ALLOW_UNENCRYPTED_AND_ENCRYPTED and accept that the traffic inside the VPC is unencrypted.

The proxy runs as a sidecar. In the Jobs of the chart it has a lifecycle problem: a sidecar that never exits keeps pocketcoffee-migrate, pocketcoffee-install and the cron Job from completing. The remedy is a native sidecar, an init container with restartPolicy: Always, which Kubernetes stops when the main container finishes. The Job below runs the module upgrade command of bin/console against 127.0.0.1:3306; in the chart the same fragment is a conditional block of the pod templates of the web Deployment and the Jobs (the final chart layout is on the Helm page).

Pod spec of the migration Job with the Auth Proxy as a native sidecar
apiVersion: batch/v1
kind: Job
metadata:
  name: pocketcoffee-migrate
  namespace: pocketcoffee
spec:
  backoffLimit: 0
  template:
    spec:
      restartPolicy: Never
      serviceAccountName: pocketcoffee-prestashop
      initContainers:
        - name: cloud-sql-proxy
          image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:<pinned-2.x-version>
          restartPolicy: Always
          args:
            - "--private-ip"
            - "--structured-logs"
            - "--port=3306"
            - "<project-id>:europe-west1:pocketcoffee-prod"
          securityContext:
            runAsNonRoot: true
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
          resources:
            requests: {cpu: 50m, memory: 64Mi}
      containers:
        - name: console
          image: europe-west1-docker.pkg.dev/<project-id>/pocketcoffee/prestashop:<git-sha>
          workingDir: /var/www/html
          command: ["php", "bin/console", "prestashop:module", "upgrade", "pocketcoffee_roast", "--env=prod"]
          envFrom:
            - configMapRef:
                name: pocketcoffee-env
            - secretRef:
                name: pocketcoffee-secrets
          env:
            - { name: DB_USER, value: ps_migrate }
            - name: DB_PASSWD
              valueFrom:
                secretKeyRef:
                  name: pocketcoffee-secrets
                  key: DB_PASSWORD_MIGRATE

In the Helm values for this cluster the difference is a few lines of the keys defined on the Helm page (secrets.storeName and secrets.valkeyName change in the same way if the store has another name, and shop.cacheEnabled stays false until the cache page’s measurement says otherwise):

values-gke.yaml (excerpt)
image:
  repository: europe-west1-docker.pkg.dev/<project-id>/pocketcoffee/prestashop
  tag: "<git-sha>"
shop:
  domain: www.pocketcoffee.example
  adminDir: admin-pocketcoffee
web:
  replicas: 3
database:
  host: 127.0.0.1
shared:
  claimName: pocketcoffee-shared
secrets:
  name: pocketcoffee-prestashop                 # the remote keys are the names in Secret Manager
  dbAppName: pocketcoffee-prestashop-db-app
  dbMigrateName: pocketcoffee-prestashop-db-migrate

The Kubernetes ServiceAccount is bound to roles/cloudsql.client with a principal identifier that names the namespace and the ServiceAccount (the exact binding is on Cloud SQL). One proxy per pod multiplies the connections Cloud SQL sees, so the PHP-FPM pm.max_children times the replicas must stay below the instance’s max_connections: size the ceiling with the autoscaler’s maximum, not its minimum. The legacy mechanism for read replicas (config/db_slave_server.inc.php, described on the database page) has no TLS keys either, so it would need the same proxy per replica; most shops should size the primary instead.

Filestore for the shared paths

Filestore is a managed NFS server, and the GKE driver exposes it as ReadWriteMany volumes. The built-in classes are zonal-rwx, enterprise-rwx, enterprise-multishare-rwx, standard-rwx and premium-rwx; the Enterprise tier is the one replicated across zones (the site page on Block and File Storage on Google Cloud explains the tiers and the minimum instance sizes, which are large: 1 TiB for the zonal tier at the check date, so verify the minimum for the tier and region you choose). A zonal Filestore is a single-zone dependency under a multi-zone cluster, which is a decision to take knowingly.

The shared claim and the pod security context
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pocketcoffee-shared
  namespace: pocketcoffee
spec:
  accessModes: ["ReadWriteMany"]
  storageClassName: enterprise-rwx
  resources:
    requests:
      storage: 1Ti
---
apiVersion: v1
kind: Pod
metadata:
  name: shared-permissions-check
  namespace: pocketcoffee
spec:
  restartPolicy: Never
  securityContext:
    runAsUser: 33
    runAsGroup: 33
    fsGroup: 33
  containers:
    - name: check
      image: busybox:1.37
      command: ["sh", "-c", "touch /shared/img/.write-test && ls -ln /shared/img && rm /shared/img/.write-test"]
      volumeMounts:
        - name: shared
          mountPath: /shared
  volumes:
    - name: shared
      persistentVolumeClaim:
        claimName: pocketcoffee-shared

The check Pod is a test, not a part of the release: it proves that user 33 (www-data in the official image) can write to the share before the web pods depend on it. The Google documentation recommends fsGroup to manage group ownership of shared volumes and warns that combining fsGroup with the ROOT_SQUASH export option gives an "Access denied" error, and a new instance is owned by root, so create the img/, upload/ and download/ directories once with an admin Pod before the web pods start (the exact list of shared paths is on the shared-files page of this section). The rule of the EFS case applies unchanged: shared files on the volume, never PHP code, and keep var/cache per pod.

Media for shoppers goes through Cloud CDN in front of the global load balancer, with media.pocketcoffee.example as a second host name served by the same backend and set in the media-server settings (see Cloud CDN and Media CDN).

Memorystore for Valkey, and Memcached

Memorystore for Valkey holds the sessions. It is reached through a Private Service Connect endpoint that a service connection policy creates once per network and region (the commands are on Memorystore and Partner Databases). PHP reads the session settings from the configuration, and the pool file of the chart takes them from the variables VALKEY_HOST, VALKEY_PORT and VALKEY_AUTH; the Memorystore endpoint and credential go into those keys of the secret that the chart’s ExternalSecret reads (secrets.valkeyName). Where the service needs something the pool file does not express (another port, or an authentication that is not a token), generate an ini file from a Secret that holds the endpoint and the credential instead:

zz-sessions.ini, mounted into the PHP configuration directory from a Secret
session.save_handler = redis
session.save_path = "tls://<valkey-endpoint>:6379?auth=<password-from-secret>"
redis.session.locking_enabled = 1
redis.session.lock_expire = 60
redis.session.lock_wait_time = 20000

The handler needs the redis PHP extension, which is not part of the official PrestaShop image (the image page shows how it is installed). Session locking matters in the back office, where several Ajax requests of one page run at once. Memorystore’s IAM authentication uses short-lived access tokens, and a static auth value cannot follow them: either run the instance with transit encryption and authentication that suits a static password, or put a token refresher in front, and check the authorization modes that gcloud memorystore instances create --help offers on your version. A lost session store only logs people out of the back office and empties carts that were not yet written to the database, so it does not need persistence.

Memorystore for Memcached is deprecated: Google announced that it is no longer a recommended Memorystore service, that new projects cannot create instances after 2027-02-01 and that the service shuts down on 2029-01-31, recommending Memorystore for Valkey instead. PrestaShop’s SQL cache cannot use Valkey, so a new deployment has two honest choices: leave the SQL cache off and give the database a larger buffer pool, or run Memcached on the cluster:

Memcached on GKE for the PrestaShop SQL cache
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pocketcoffee-memcached
  namespace: pocketcoffee
spec:
  replicas: 2
  selector:
    matchLabels:
      app.kubernetes.io/name: pocketcoffee-memcached
  template:
    metadata:
      labels:
        app.kubernetes.io/name: pocketcoffee-memcached
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 11211
      containers:
        - name: memcached
          image: memcached:1.6-alpine
          args: ["-m", "256", "-c", "1024"]
          ports:
            - containerPort: 11211
          resources:
            requests: {cpu: 100m, memory: 320Mi}
            limits: {memory: 320Mi}
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
---
apiVersion: v1
kind: Service
metadata:
  name: pocketcoffee-memcached
  namespace: pocketcoffee
spec:
  clusterIP: None
  selector:
    app.kubernetes.io/name: pocketcoffee-memcached
  ports:
    - port: 11211

The headless Service name resolves to the pod addresses, which you enter as servers in Advanced Parameters > Performance > Caching (or as rows of ps_memcached_servers) with the caching system CacheMemcached. PrestaShop’s server list is a static table of addresses and weights, so it does not follow pods that are replaced: use a fixed set of replicas behind a StatefulSet with stable names if you go this way, or accept that a rescheduled pod changes an address and the table needs updating. The cache holds disposable data, so losing a node only costs a few slow requests. Pin the image by digest in your own registry, as for every image in this section.

Secrets, entry and health checks

Secrets come from Secret Manager. The External Secrets Operator reads them with a store that authenticates through Workload Identity, and the chart’s ExternalSecret is unchanged except for the store name and the remote keys (the store manifest is on the WordPress page of this section; the keys are PS_COOKIE_KEY, PS_COOKIE_IV, PS_NEW_COOKIE_KEY and PS_SECRET of PrestaShop, and the credentials of the two database users in their own secrets, as on AWS). The GKE add-on for Secret Manager mounts secrets as files instead (see Secret Manager and Cloud KMS).

The HTTP entry is a Gateway of class gke-l7-global-external-managed, with the certificate for www.pocketcoffee.example in Certificate Manager and Cloud Armor attached to the backend (see Load Balancing and Cloud Armor). The Gateway, HTTPRoute and HealthCheckPolicy objects are the ones of the WordPress page, with this PrestaShop difference: the load balancer’s probe sends a request to the pod without the shop’s host name, and PrestaShop redirects requests whose host differs from the one in ps_shop_url to the shop domain, so a probe on / can see a redirect and mark the backend unhealthy. Point the health check at the static location that the nginx configuration of the container image page answers with a plain 200. The back office stays at /admin-pocketcoffee/ and is restricted at the gateway or with Cloud Armor (see Security).

Azure: AKS, Azure Database for MySQL, Azure Files and Azure Managed Redis

Cluster and database (Azure)
#!/usr/bin/env bash
set -euo pipefail

RG=rg-pocketcoffee
LOCATION=westeurope

az group create --name "$RG" --location "$LOCATION"

az acr create --resource-group "$RG" --name acrpocketcoffee --sku Standard

az aks create \
  --resource-group "$RG" \
  --name pocketcoffee-prod \
  --tier standard \
  --node-count 3 \
  --zones 1 2 3 \
  --network-plugin azure \
  --network-plugin-mode overlay \
  --network-dataplane cilium \
  --enable-oidc-issuer \
  --enable-workload-identity \
  --enable-addons azure-keyvault-secrets-provider \
  --attach-acr acrpocketcoffee \
  --generate-ssh-keys

az aks get-credentials --resource-group "$RG" --name pocketcoffee-prod
kubectl create namespace pocketcoffee

# The server lives in a delegated subnet with a private DNS zone (VNet integration).
az mysql flexible-server create \
  --resource-group "$RG" \
  --name mysql-pocketcoffee-prod \
  --location "$LOCATION" \
  --tier GeneralPurpose \
  --sku-name Standard_D2ds_v4 \
  --storage-size 128 \
  --version 8.4 \
  --high-availability ZoneRedundant \
  --zone 1 --standby-zone 2 \
  --vnet vnet-pocketcoffee --subnet snet-mysql \
  --private-dns-zone mysql-pocketcoffee.private.mysql.database.azure.com \
  --database-name pocketcoffee \
  --admin-user ps_admin \
  --admin-password "$MYSQL_ADMIN_PASSWORD"

The points that differ from RDS and Cloud SQL:

  • Networking is fixed at creation. Public access versus VNet integration cannot be changed later, and a VNet-integrated server needs a subnet delegated to Microsoft.DBforMySQL/flexibleServers and a private DNS zone. Choose private access for production and keep a bastion or a debug Pod in the VNet for imports.

  • Versions and support. MySQL 8.4 is generally available; 8.0 reaches the end of standard support on 2027-01-31 and then moves to automatically charged extended support, and the 9.x innovation versions are preview only without HA, replicas or backups. New shops start on 8.4. Check the version string your CLI accepts with az mysql flexible-server list-skus.

  • TLS is on by default, and PrestaShop cannot ask for it. The server parameter require_secure_transport is ON by default. With the database client of the first section, a connection that sets no SSL attribute is refused (error 3159, as measured on the database page). Decide before the first import: either set require_secure_transport to OFF on a server that is reachable only from the cluster’s private subnet (a security decision to record, not a default), or place a TLS-terminating proxy next to each pod (Azure has no equivalent of the Cloud SQL Auth Proxy, so this is a general-purpose proxy that you build, run and monitor), or add the SSL attributes to the database class and the Doctrine connection by override. Test whichever you choose with the back office and the front office, because they use different connections.

  • No built-in connection pooler for MySQL: keep pm.max_children times the replicas under max_connections of the SKU.

  • Authentication. The server supports Microsoft Entra authentication with short-lived tokens, but PrestaShop’s connection takes a password from parameters.php, so use password authentication with the secret in Key Vault, and keep Entra for operators. MySQL 8.4 defaults to caching_sha2_password, which the PHP native driver supports.

Azure Managed Redis, and Memcached

Azure Managed Redis is the current first-party service (Azure Cache for Redis is retiring in two waves, on 2027-03-31 for the Enterprise tiers and 2028-09-30 for Basic, Standard and Premium). It speaks the Redis protocol, so the session ini file of the Google Cloud section applies with the endpoint’s host and port. It is not Valkey, and its endpoint listens on port 10000 with TLS, so the port is not 6379. The clustering policy is fixed at creation (OSSCluster needs a cluster-aware client; EnterpriseCluster lets a client that does not follow cluster redirects connect to a single endpoint, which is the simpler fit for session storage), and a new cache defaults to Microsoft Entra authentication with access keys off, so enable access keys on a cache reachable only from the cluster’s private network, or run a token refresher. The commands and the choices are on Redis and Partner Databases on Azure.

The Azure pages of this site list no managed Memcached service, so verify the current Azure offer before relying on that statement; if there is none, the SQL cache has the same two choices as on GKE: off, or the Memcached manifest of the Google Cloud section on the cluster (the manifest is portable). Do not use APCu for the SQL cache across several pods.

Azure Files: NFS and SMB

Azure Files is the shared file system. It offers two protocols, and the choice has consequences for PrestaShop, which writes thumbnails and uploads as many small files:

NFS (version 4.1) SMB

Storage account

SSD (Premium) file shares in a virtual-network-enabled account

Standard or Premium

Authentication

None: network-based access control, so only the virtual network can reach it

Storage account key (the AKS CSI driver mounts SMB with key-based NTLMv2 authentication)

Encryption in transit

Off by default; the storage account’s Secure transfer required must be disabled for the mount to succeed, unless you use the driver’s encryptInTransit parameter (verify it for your driver version)

Yes

File ownership

POSIX: set the owner to UID/GID 33 with fsGroup or an init step

Set by mount options (uid=33, gid=33, dir_mode, file_mode), identical for every file

Symbolic links

Supported (full POSIX)

Need the mfsymlinks mount option

Fit for PrestaShop

Closer to the local file system the application expects; Microsoft describes it as optimised for random access with in-place updates

Works for img/ and uploads, slower for many small files, and every file has the ownership of the mount options

For img/, upload/ and download/ use NFS unless the organisation forbids unencrypted traffic inside the virtual network, in which case use SMB with explicit ownership. The two storage classes:

Shared paths on Azure Files, NFS and SMB variants
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: azurefile-nfs-pocketcoffee
provisioner: file.csi.azure.com
parameters:
  protocol: nfs
  skuName: Premium_LRS
reclaimPolicy: Retain
volumeBindingMode: Immediate
allowVolumeExpansion: true
mountOptions:
  - nconnect=4
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: azurefile-smb-pocketcoffee
provisioner: file.csi.azure.com
parameters:
  skuName: Premium_LRS
reclaimPolicy: Retain
volumeBindingMode: Immediate
allowVolumeExpansion: true
mountOptions:
  - dir_mode=0775
  - file_mode=0664
  - uid=33
  - gid=33
  - mfsymlinks
  - cache=strict
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pocketcoffee-shared
  namespace: pocketcoffee
spec:
  accessModes: ["ReadWriteMany"]
  storageClassName: azurefile-nfs-pocketcoffee
  resources:
    requests:
      storage: 100Gi

Use one class or the other, not both; they are shown together so the differences are visible. Retain keeps the share when the claim is deleted, which is what you want for product images. The NFS share’s reachability is the virtual network’s, so the node subnet must be allowed by the storage account’s network rules, and the PHP code never goes on the share, as on every cloud.

Key Vault, workload identity and the gateway

On AKS the ServiceAccount carries the identity of a user-assigned managed identity through a federated credential: the ServiceAccount has the annotation azure.workload.identity/client-id and the pod template the label azure.workload.identity/use: "true", and the identity needs the role Key Vault Secrets User on the vault (see Key Vault and Secrets and Identity and Access; the store manifest for the External Secrets Operator is on the WordPress page of this section). The HTTP entry is the application routing add-on’s Gateway API implementation, or Application Gateway for Containers with the Web Application Firewall, and Azure Front Door is the edge for media.pocketcoffee.example (see AKS Microservices, Gateways and Load Balancing and Front Door and CDN). The older managed NGINX mode rests on the retired ingress-nginx upstream, so none of the manifests of this section use it. The host-name redirect of the Google Cloud section applies to the health probe on Azure too.

Alternatives to Kubernetes

Kubernetes earns its operating cost when there are several workloads, a platform team, or the need to scale pods and nodes independently. For one shop the simpler services are real options. Everything of the tiered architecture still holds (managed database, shared sessions, shared files, secrets, one cron); only the compute changes, and each option changes what PrestaShop’s state looks like in a specific way.

ECS on Fargate

A task definition replaces the Deployment: nginx and PHP-FPM are two containers of one task and share localhost, an ALB target group replaces the Service and an ECS service with target-tracking scaling replaces the HPA (see ECS and Fargate and ECS Deployments, Scaling and Networking). What changes for PrestaShop: the shared paths are an EFS volume declared in the task definition (Fargate supports EFS) with an access point that fixes UID and GID 33; the secrets come from Secrets Manager through the task definition; there is no Helm, so the release is a new task definition revision, the pocketcoffee-migrate step is a one-off run-task of the same image before the service update, and the cron of pocketcoffee-cron is an EventBridge Scheduler rule that runs the task with the cron command.

task-definition.json (excerpt for Fargate)
{
  "family": "pocketcoffee-web",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "1024",
  "memory": "2048",
  "runtimePlatform": {"cpuArchitecture": "ARM64", "operatingSystemFamily": "LINUX"},
  "executionRoleArn": "arn:aws:iam::123456789012:role/pocketcoffee-ecs-execution",
  "taskRoleArn": "arn:aws:iam::123456789012:role/pocketcoffee-ecs-task",
  "volumes": [
    {
      "name": "shared",
      "efsVolumeConfiguration": {
        "fileSystemId": "fs-<id>",
        "transitEncryption": "ENABLED",
        "authorizationConfig": {"accessPointId": "fsap-<id>", "iam": "ENABLED"}
      }
    }
  ],
  "containerDefinitions": [
    {
      "name": "nginx",
      "image": "123456789012.dkr.ecr.eu-west-1.amazonaws.com/pocketcoffee/prestashop:<git-sha>-nginx",
      "essential": true,
      "portMappings": [{"containerPort": 80, "protocol": "tcp"}],
      "dependsOn": [{"containerName": "php-fpm", "condition": "START"}],
      "mountPoints": [{"sourceVolume": "shared", "containerPath": "/var/www/html/shared", "readOnly": true}]
    },
    {
      "name": "php-fpm",
      "image": "123456789012.dkr.ecr.eu-west-1.amazonaws.com/pocketcoffee/prestashop:<git-sha>",
      "essential": true,
      "environment": [
        {"name": "DB_SERVER", "value": "<rds-endpoint>"},
        {"name": "DB_NAME", "value": "pocketcoffee"},
        {"name": "DB_USER", "value": "ps_app"}
      ],
      "secrets": [
        {"name": "DB_PASSWD", "valueFrom": "arn:aws:secretsmanager:eu-west-1:123456789012:secret:pocketcoffee/prod/prestashop-db-app-<suffix>:password::"},
        {"name": "PS_COOKIE_KEY", "valueFrom": "arn:aws:secretsmanager:eu-west-1:123456789012:secret:pocketcoffee/prod/prestashop-<suffix>:PS_COOKIE_KEY::"}
      ],
      "mountPoints": [{"sourceVolume": "shared", "containerPath": "/var/www/html/shared"}],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/pocketcoffee-web",
          "awslogs-region": "eu-west-1",
          "awslogs-stream-prefix": "php-fpm"
        }
      }
    }
  ]
}

The variable names in the environment and secrets entries are those that the parameters.php of the Helm page reads, and the secret names are those of the Secrets Manager secrets of the Helm page; the mount path is an illustration: use the layout of the production image, where img/, upload/ and download/ are links into the shared mount. The excerpt shows two of the secrets; PS_COOKIE_IV, PS_SECRET and the others follow the same pattern. The nginx container needs the static files of the shop (theme and module assets) at the paths it serves, so build its image from the stage of the Dockerfile that holds the code. Fargate gives each task ephemeral storage and no persistent disk, which is the right default for var/cache (see the cache page of this section for the warm-up at build); only the shared paths go on EFS. On ECS the database and cache endpoints are the same RDS and ElastiCache of the reference architecture, so the database page applies unchanged, including the TLS question.

EC2 Auto Scaling groups

The oldest pattern: an AMI baked with Packer (or a launch template that pulls the container image), an Auto Scaling group over three zones behind an ALB, RDS, ElastiCache, EFS, and a rolling instance refresh as the release (see EC2 and Auto Scaling). It has the fewest moving parts and the highest operating burden. For PrestaShop, each instance is a small version of the old single server, and these are the places where that bites:

  • the shared paths are mounted from EFS at boot, and img/, upload/ and download/ are links or bind mounts into the code tree, the same layout as in the container image;

  • var/cache is per instance and is built when the AMI is baked or when the instance starts, never from the back office;

  • cron runs on exactly one instance: a systemd timer guarded by a lock in the database, or an EventBridge rule that calls SSM Run Command on one instance;

  • an instance refresh replaces instances in place, so the schema change of a release cannot run from "the new instance": run it as a separate step before the refresh.

User data of an instance: mount the shared paths and start the services
#!/usr/bin/env bash
set -euo pipefail

SHARED=/srv/pocketcoffee/shared
SHOP=/var/www/html

mkdir -p "$SHARED"
mount -t efs -o tls,accesspoint=fsap-0123456789abcdef0 fs-0123456789abcdef0:/ "$SHARED"

for dir in img upload download; do
  mkdir -p "$SHARED/$dir"
  rm -rf "${SHOP:?}/${dir:?}"
  ln -s "$SHARED/$dir" "$SHOP/$dir"
done

systemctl enable --now php-fpm nginx

The file system and access point identifiers are placeholders. This script replaces the packaged img/ directory of the AMI with a link, so the baked image must not contain files that exist nowhere else (the demo images of a fresh install, for example). The rest of the infrastructure is the OpenTofu of the Helm page without the cluster module, with an aws_autoscaling_group over the launch template (see the Auto Scaling resource in the OpenTofu registry).

Cloud Run

Cloud Run runs a container without a cluster: requests go to instances that scale between a minimum and a maximum, each serving a configurable number of concurrent requests (see Cloud Run). PrestaShop fits if every piece of state is external, and three details of its source decide how:

  • The database. The legacy Db class accepts a host of the form host:/path/to/socket and connects through the socket, which is how Cloud Run’s built-in Cloud SQL connection (a Unix socket under /cloudsql/) works. The Symfony part of the shop, which serves the back office pages written in Symfony, builds its Doctrine connection from database_host and database_port separately and has no socket parameter in app/config/doctrine.yml, so a socket in database_host is understood by one connection and not by the other (a reading of the source: test the back office pages in staging). The safer route is the Cloud SQL Auth Proxy as a second container of the service, listening on 127.0.0.1:3306, which both connections understand.

  • Shared files. A Cloud Run service can mount an NFS share such as Filestore as a volume, or a Cloud Storage bucket through Cloud Storage FUSE. A bucket mount is a file-system view of object storage without file locking and with object-store semantics, which is a poor fit for the thumbnail generation that writes under img/; use the NFS volume, and give the service Direct VPC egress so that it can reach Filestore, Memorystore and the private IP of Cloud SQL.

  • Sessions and cache. The same Memorystore for Valkey as on GKE for sessions (the ini file is part of the image or a mounted Secret) and no SQL cache, or Memcached on a VM or GKE reachable over the VPC.

Cloud Run service with nginx, PHP-FPM and the Auth Proxy (service.yaml)
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: pocketcoffee-web
  annotations:
    run.googleapis.com/ingress: all
spec:
  template:
    metadata:
      annotations:
        autoscaling.knative.dev/minScale: "2"
        autoscaling.knative.dev/maxScale: "20"
        run.googleapis.com/execution-environment: gen2
        run.googleapis.com/container-dependencies: '{"nginx":["php-fpm"],"php-fpm":["cloud-sql-proxy"]}'
        run.googleapis.com/network-interfaces: '[{"network":"default","subnetwork":"default"}]'
        run.googleapis.com/vpc-access-egress: private-ranges-only
    spec:
      containerConcurrency: 40
      serviceAccountName: pocketcoffee-run@<project-id>.iam.gserviceaccount.com
      containers:
        - name: nginx
          image: europe-west1-docker.pkg.dev/<project-id>/pocketcoffee/prestashop:<git-sha>-nginx
          ports:
            - containerPort: 80
          volumeMounts:
            - name: shared
              mountPath: /var/www/html/shared
        - name: php-fpm
          image: europe-west1-docker.pkg.dev/<project-id>/pocketcoffee/prestashop:<git-sha>
          env:
            - name: DB_SERVER
              value: 127.0.0.1
            - name: DB_PASSWD
              valueFrom:
                secretKeyRef:
                  name: pocketcoffee-db-password
                  key: latest
          volumeMounts:
            - name: shared
              mountPath: /var/www/html/shared
        - name: cloud-sql-proxy
          image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:<pinned-2.x-version>
          args: ["--private-ip", "--port=3306", "<project-id>:europe-west1:pocketcoffee-prod"]
      volumes:
        - name: shared
          nfs:
            server: <filestore-ip>
            path: /pocketcoffee

Deploy it with gcloud run services replace service.yaml --region=europe-west1, and run the migration and the cron as Cloud Run Jobs of the same image (gcloud run jobs create) triggered by Cloud Scheduler. The annotations and the volume syntax are composed from the reference above and have not been run; check them with the current reference. Limits to weigh: the request timeout (not a problem for page views, but long imports and thumbnail regenerations belong in Jobs), cold starts of a PHP application (minimum instances address them), and no persistent disk.

Azure Container Apps

Container Apps is the Azure analogue: an environment with a virtual network, revisions per deployment, HTTP scaling rules, Key Vault references for secrets, multiple containers per revision, and jobs for scheduled and manual work (see Container Apps and Container Apps: Scaling, Jobs and Dapr). What changes for PrestaShop is the shared file system, whose rules are stricter than on AKS:

  • Container Apps mounts only Azure Files: shares of Azure NetApp Files and Blob Storage are not supported, and only "classic" file shares inside a storage account work.

  • An NFS share needs an environment created with a custom virtual network, the storage account must allow that network, and the account must not require encryption in transit, because Container Apps does not support it for NFS (a mount against a account that requires it fails with an "access denied by server" error). Ports 445 and 2049 must be allowed in the subnet’s network security group.

  • An SMB share can use the environment’s managed identity (a preview feature at the check date) or the storage account key, and the volume’s ownership comes from mount options, as in the AKS table.

Storage definition and a NFS volume for the web app
#!/usr/bin/env bash
set -euo pipefail

RG=rg-pocketcoffee

az containerapp env storage set \
  --resource-group "$RG" --name cae-pocketcoffee \
  --storage-name shared \
  --storage-type NfsAzureFile \
  --server stpocketcoffee.file.core.windows.net \
  --azure-file-share-name /stpocketcoffee/shared \
  --access-mode ReadWrite

az containerapp show --resource-group "$RG" --name pocketcoffee-web -o yaml > app.yaml
# Edit app.yaml: under template.volumes add
#   - name: shared
#     storageType: NfsAzureFile
#     storageName: shared
# and under each container's volumeMounts add
#   - volumeName: shared
#     mountPath: /var/www/html/shared
az containerapp update --resource-group "$RG" --name pocketcoffee-web --yaml app.yaml

The database has the TLS question of the AKS section, without a proxy option inside Container Apps other than a second container in the same revision, so the choices are the server setting or a sidecar you operate. Sessions use Azure Managed Redis as on AKS; there is no SQL cache without a Memcached you run yourself. The migration and the cron are Container Apps jobs of the same image (a manual job for pocketcoffee-migrate before the revision switch and a scheduled job for pocketcoffee-cron), and the secrets are Key Vault references (see the Container Apps page above).

PrestaShop hosting offers

A managed host removes the infrastructure and sells PrestaShop as a service. The PrestaShop project itself describes several kinds of offer on prestashop.com (a hosted offer with the shop pre-installed, a managed PrestaShop hosting platform, and partner hosting companies), and the list, the plans and the partners change, so verify the current offers on prestashop.com/hosting and in each provider’s documentation before deciding. The questions that decide the choice are the same as for any managed platform:

  • Can you reach the shell and bin/console, deploy by Git and keep the shop as code, or is everything done from the back office and an FTP-like upload? The deployment model of this section assumes the first.

  • Which modules and themes are allowed, can you install your own (pocketcoffee_roast, pocketcoffee_pay, pocketcoffee_lot), and are core overrides, custom PHP settings and cron jobs available?

  • How fast do security releases arrive after the project publishes them, which PHP and MySQL versions run, and can you move to PrestaShop 9.2 when you want to (see Versions and Support)?

  • What does the platform include (CDN, WAF, staging, backups with a restore test), and what are the exit conditions: can you export a complete database dump and the img/, upload/ and download/ folders at any time?

When the answers are acceptable and nothing needs custom infrastructure (private networking to internal systems, custom nginx rules, a compliance regime), a managed host is the cheapest operation. The self-hosted requirements are on Installation and Local Environments, and the hardening you would otherwise do yourself is on Security.

Decision table

Option Operating effort Scaling What PrestaShop’s state looks like Choose it when

Single server (VPS)

Lowest

Vertical only

Everything in one place, no high availability

A small shop where an hour of downtime is acceptable

PrestaShop hosting offer

Very low

The provider’s

Out of your hands, with the provider’s module and PHP rules

The team wants no infrastructure and the restrictions are acceptable

EC2 Auto Scaling group

Medium (operating system, images)

Instance level, minutes

EFS links for img/ and uploads, one cron instance

A VM-based team already on AWS, a legacy module that needs a server

ECS on Fargate

Low to medium

Task level, fast

EFS volume in the task, secrets from Secrets Manager

One or a few services on AWS without Kubernetes

Cloud Run

Low

Instance level

Filestore NFS volume, Auth Proxy container, Memorystore

Google Cloud, spiky traffic, small team

Azure Container Apps

Low

Replica level, rules

Azure Files NFS in a custom network, no managed Memcached

Azure, small team

EKS

Higher (cluster, add-ons)

Pod and node

The reference design of this section

Several workloads, a platform team, AWS

GKE

Medium

Pod and node

Same chart: Cloud SQL proxy, Filestore, no managed Memcached

A Google Cloud organisation

AKS

Medium to higher

Pod and node

Same chart: Azure Files, TLS decision for the database, no managed Memcached

An Azure organisation

Two rules of thumb. First, choose the cloud by where the rest of the organisation’s data, identity and compliance already are, not by PrestaShop: a tiered shop moves between clouds with a database dump, a copy of img/, upload/ and download/, and new values for the same chart (see Cloud: Migrating from a Single Server), so it is not a reason to bring in a second cloud. Second, choose the compute by how many other workloads share it: a Kubernetes cluster for one shop is rarely justified, and ECS on Fargate or Cloud Run are the honest alternatives. The state, the release process, the autoscaling signal and the backups described in this section (see Cloud: Autoscaling, Observability, Backups and Security) are the same in all of them.

Common errors and pitfalls

  • SQLSTATE[HY000] [3159] Connections using insecure transport are prohibited while --require_secure_transport=ON: the database client sent no TLS request to a server that requires it. Use the Auth Proxy, a proxy you run, or relax the server setting on a private network.

  • The shop works but the back office pages written in Symfony fail while the front office works (or the reverse): the legacy Db connection and the Doctrine connection read the same settings differently, as with a Unix socket in database_host. Test both paths after any change to the connection.

  • A Job with a normal sidecar never completes (pocketcoffee-migrate stays Running forever): use a native sidecar as above.

  • The SQL cache set to APCu on several pods: each pod caches and invalidates alone, so a price change shows on some pods only. Use Memcached or switch the SQL cache off.

  • A new Memorystore for Memcached instance cannot be created in a project after 2027-02-01: plan the cache on the cluster from the start.

  • PHP sessions do not persist on Azure Managed Redis: the port is 10000 with TLS, not 6379, and a short-lived Entra token used as the password expires after about an hour.

  • Product images disappear or are not writable after a pod restart on Azure Files over SMB: the mount options did not set uid=33, gid=33 and mfsymlinks, or the NFS share was mounted with the root owner.

  • An Azure Files NFS mount fails with "access denied by server": Secure transfer required is enabled on the storage account, or the pod’s subnet is not allowed by the account’s network rules.

  • The gateway shows the backend as unhealthy: the probe on / receives a redirect to the shop domain; define the health check on the static location of the container image.

  • A Cloud Run or Container Apps service redirects in a loop: HTTPS detection behind the platform’s proxy is not configured in the shop’s SSL settings (see the reference architecture page and the domain settings of the Helm page).