Docker
In this section, we'll create a Dockerfile
to build our Docker image and deploy it.
The instructions below will be using the official PHP Docker image as a base image. You may need to adapt it to your needs, depending on your project requirements, such as PHP version, extensions, etc.
Since Magento 2.4.7 is compatible with PHP 8.3, we'll be using this version.
Choice of base image
When choosing a PHP base image, we'd generally go with 8.3-fpm
, which seems to be the most popular choice.
However, you should consider the following:
- This image is based on Debian, which may not be the best choice for production, as it's not as lightweight as Alpine
- The underlying Debian version may be upgraded, which may break your build:
8.3-fpm
is available onbookworm
(8.3-fpm-bookworm
) andbullseye
(8.3-fpm-bullseye
) - Although using a minor version of PHP is generally safe, you may want to use a specific version, such as
8.3.7-fpm
to avoid any surprises - If you need to have reproducible builds, you may use the (short or full)
sha256
digest of the image, such asphp@sha256:606222f6366a
instead ofphp:8.3.7-fpm-bookworm
TIP
A general rule of thumb is to use the most specific version of the base image, such as 8.3.7-fpm-bookworm
, to avoid any surprises, but still be able to benefit from security updates.
At this point, your Dockerfile
should look like this:
FROM php:8.3.7-fpm-bookworm
Let's also define /app
as our base working directory, where we'll be copying our Magento / Adobe Commerce project files:
WORKDIR /app
We'll also define the MAGE_MODE
environment variable, which is required by Magento / Adobe Commerce to know we're building for production:
ENV MAGE_MODE=production
PHP extensions
Now that we have our base image, we need to install the necessary PHP extensions and their dependencies.
Required PHP extensions are listed in the official system requirements, under Commerce on-premises
tab.
NOTE
This guide targets PHP 8.3, which already includes some of the required extensions. You may need to install additional extensions, run docker run --rm php:<tag> php -m
to list the already installed extensions in the image you're planning to use.
For our PHP 8.3 base image, the missing extensions we need to install are:
bcmath
gd
intl
pdo_mysql
soap
sockets
xsl
zip
Although not listed explicitly, opcache
and pcntl
extensions are also required for optimal performance.
Let's add the necessary intrusctions in our Dockerfile
:
# Install required PHP extensions system dependencies
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y \
libfreetype6-dev libicu-dev libjpeg62-turbo-dev libpng-dev libxslt1-dev libzip-dev libwebp-dev
# Configure PHP extensions
RUN docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp
# Install required PHP extensions
RUN docker-php-ext-install -j$(nproc) bcmath gd intl opcache pcntl pdo_mysql soap sockets xsl zip
Composer
Next, we need to install Composer, which is a dependency manager for PHP.
NOTE
Magento / Adobe Commerce 2.4.7 is compatible with Composer 2.7. Similar to PHP base image, you may want to use a specific version of Composer to avoid any surprises. However, in such case, you may need to update the Dockerfile
when a new version of Composer is released.
To install Composer 2.x latest version, add the following instructions to your Dockerfile
:
RUN curl -sSL https://getcomposer.org/download/latest-2.x/composer.phar -o /usr/local/bin/composer \
&& chmod +x /usr/local/bin/composer
To benefit from Docker layer cache when rebuilding without change to composer.json
or composer.lock
, you may want to copy composer.json
and composer.lock
files separately, along with auth.json
(required to authenticate against repo.magento.com
):
COPY composer.json composer.lock auth.json /app/
Then, run composer install
to install the dependencies:
RUN composer install \
--no-interaction \
--no-dev \
--optimize-autoloader\
--no-progress\
--no-suggest
At this point, the /app
directory in your Docker image looks like this:
.
|-- auth.json
|-- composer.json
|-- composer.lock
`-- vendor
|-- 2tvenom
|-- astock
|-- autoload.php
|-- aws
|-- bacon
|-- bin
|-- braintree
|-- brick
|-- christian-riesen
|-- colinmollenhour
|-- composer
|-- dasprid
|-- elasticsearch
|-- endroid
|-- ezimuel
|-- ezyang
|-- firebase
|-- google
|-- guzzlehttp
|-- justinrainbow
|-- laminas
|-- league
|-- magento
|-- monolog
|-- ... (and many more)
|-- webmozart
|-- webonyx
`-- wikimedia
The auth.json
file can be safely removed from the image after running composer install
:
RUN rm -f auth.json
Magento / Adobe Commerce
Now that we have Composer installed and our dependencies ready, we can copy our Magento / Adobe Commerce project files to the image.
COPY . /app/
You may want to add a .dockerignore
file to exclude unnecessary files and directories from being copied to the image.
For example, you may want to exclude node_modules
, vendor
, .git
, etc.
The following directories are generally excluded:
pub/media
pub/static
var
NOTE
As the image should be built from a raw clone of the repository, you may not need to exclude anything.
DI compilation
The setup:di:compile
command generates the generated
directory, which contains generated code and classes.
To avoid running into memory issues, you may want to increase the memory limit for the PHP process:
RUN php -d memory_limit=2G bin/magento setup:di:compile
Static content
This part is one of the trickiest, as it generates static content for all locales and themes, and needs to be aware of the websites structure.
Therefore, you should dump the necessary database configuration into app/etc/config.php
:
bin/magento app:config:dump scopes themes i18n
IMPORTANT
This command should be run outside the Docker image, as it requires a database connection. The resulting config.php
should be added to the VCS, so it's available when building the Docker image.
NOTE
This command will need to be run every time you change the configuration, such as changing the websites structure, adding new themes, etc.
Additionally, the app/etc/env.php
needs to be moved temporarily during the static content deployment, to avoid database lookup issues:
RUN mv app/etc/env.php app/etc/env.php.bak
Then, you can run the setup:static-content:deploy
command:
RUN php -d memory_limit=2G bin/magento setup:static-content:deploy \
--max-execution-time=3600 \
--jobs=$(nproc)
To make this process more efficient, you may want to run the setup:static-content:deploy
command for a specific locale and theme only. The following options are available:
--area
(repeatable):frontend
oradminhtml
, defaults toall
--theme
(repeatable): themes to build, i.e.Magento/luma
,Magento/backend
, defaults toall
--language
(repeatable): locales to build, i.e.en_US
,de_DE
, defaults toall
IMPORTANT
Note that even in headless setups, the frontend
area is required, notably to generate emails and PDFs.
Once the static content is deployed, we can move back the env.php
file:
RUN mv app/etc/env.php.bak app/etc/env.php
We can now do some cleanup:
RUN rm -rf var/*
Although the PHP configuration is generally good enough for most cases, you may want to tweak it to your needs.
Here is an example of additional PHP configuration you may define:
date.timezone = Etc/UTC
; Recommended value by Magento / Adobe Commerce
memory_limit = 756M
log_errors = On
display_errors = Off
display_startup_errors = Off
expose_php = Off
opcache.enable = 1
opcache.enable_cli = 1
opcache.save_comments = 1
opcache.memory_consumption = 1024
opcache.interned_strings_buffer = 32
opcache.max_accelerated_files = 130987
opcache.enable_file_override = 1
; When set to 0, you'll need to restart the PHP process to apply changes to PHP files, which should never happen in production
opcache.consistency_checks = 0
opcache.validate_timestamps = 0
realpath_cache_size = 10M
realpath_cache_ttl = 7200
; Increase the maximum file upload size ; those should be set according to your needs (i.e. for image uploads from admin)
upload_max_filesize = 20M
post_max_size = 20M
TIP
PHP is able to read environment variables, so you may want to define some of the configuration options as environment variables, such as memory_limit
, etc. Syntax is memory_limit = ${PHP_MEMORY_LIMIT}
, which can be useful to have a different memory limit between CLI and FPM Pods.
The content of the above ini file should be saved in a file, such as custom.ini
, along yout Dockerfile
, and copied to the image:
COPY custom.ini /usr/local/etc/php/conf.d/custom.ini
FPM configuration
Here is a recommended pool configuration:
[www]
user = www-data
group = www-data
listen = 127.0.0.1:9000
pm = static
; Or use an environment variable, such as ${PHP_FPM_MAX_CHILDREN}
pm.max_children = 10
pm.status_path = /status
; Useful to avoid memory leaks ; processes will be restarted after handling 10 requests
pm.max_requests = 10
; Useful to send logs from all workers to the main process, which will then send them to stdout
; Note that those are usually already defined in /usr/local/etc/php-fpm.d/docker.conf shipped with PHP FPM docker image
catch_workers_output = yes
decorate_workers_output = no
The content of the above file should be saved in a file, such as www.conf
, along yout Dockerfile
, and copied to the image:
COPY www.conf /usr/local/etc/php-fpm.d/www.conf
IMPORTANT
As you may have noticed, we're using pm = static
instead of pm = dynamic
.
When working in a Kubernetes environment, we need to have control about how much resources our pods are using. Using pm = static
allows us to set a fixed number of workers, which is easier to manage in a Kubernetes environment.
We'll dig deeper into this in the Kubernetes resources allocation section.
nginx
Now that we have our PHP-FPM image ready, we need to add a nginx server to serve our Magento / Adobe Commerce project, communicating with PHP-FPM using fastcgi.
To serve static assets in an efficient way, we'll also copy the pub/static
directory to the nginx image.
INFO
The same way we did for the PHP image, we'll be using the official nginx image as a base image.
We recommend using the alpine
version, which is lightweight and secure.
As with PHP image, you may want to use a specific version of the image to avoid any surprises.
Note that Adobe recommends using version 1.24 of nginx for Magento 2.4.7.
A sample nginx.conf.sample
file is provided in the Magento / Adobe Commerce repository, which you can use as a base for your configuration.
Create a vhost.conf
file:
upstream fastcgi_backend {
server 127.0.0.1:9000;
}
server {
listen 80;
# server_name directive is not mandatory, as we're using the default server block
set $MAGE_ROOT /app;
set $MAGE_DEBUG_SHOW_ARGS 0;
# Include the content of `nginx.conf.sample` here
}
Additionnaly, you may change the following in the resulting vhost.conf
file:
- Uncomment the
# expires max;
line to enable caching of static assets, inlocation /static/
block - Change the access log format to JSON, to be able to parse it easily in a log aggregator, such as ELK stack, CloudWatch, etc.:
log_format nginxlog_json escape=json
'{ "timestamp": "$time_iso8601", '
'"remote_addr": "$remote_addr", '
'"body_bytes_sent": $body_bytes_sent, '
'"request_time": $request_time, '
'"response_status": $status, '
'"request": "$request", '
'"request_method": "$request_method", '
'"host": "$host",'
'"remote_user": "$remote_user",'
'"request_uri": "$request_uri",'
'"query_string": "$query_string",'
'"upstream_addr": "$upstream_addr",'
'"http_x_forwarded_for": "$http_x_forwarded_for",'
'"http_x_real_ip": "$http_x_real_ip",'
'"http_referrer": "$http_referer", '
'"http_user_agent": "$http_user_agent", '
'"http_version": "$server_protocol" }';
server {
...
access_log /dev/stdout nginxlog_json;
...
}
Remove unnecessary blocks:
location ~* ^/setup($|/) { ... }
location ~* ^/update($|/) { ... }
Add a health check endpoint, to be able to monitor the nginx server:
server {
...
location /nginx-health {
add_header Content-Type text/plain;
return 200 'OK';
}
}
- Increase the buffers size in the main entry point:
# PHP entry point for main application
location ~ ^/(index|get|static|errors/report|errors/404|errors/503|health_check)\.php$ {
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
fastcgi_buffers 16 32k;
fastcgi_buffer_size 64k;
fastcgi_busy_buffers_size 64k;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
...
}
INFO
The complete vhost.conf
file can be found here.
The resulting Dockefile
should look like this:
FROM nginx:alpine
WORKDIR /app
COPY vhost.conf /etc/nginx/conf.d/default.conf
But hey, we don't have the pub/static
directory yet! Let's discuss this in the next section.
Multi-stage build
A Docker multi-stage build is a method that allows for the creation of smaller, more efficient container images by using multiple intermediate stages in a single Dockerfile
, thereby isolating and discarding unnecessary build artifacts.
It also allows copying files from one stage to another, which is useful in our case, as we need to copy the pub/static
directory from the PHP image to the nginx image.
Our Dockerfile
will eventually look like this:
# PHP-FPM image
FROM php:8.3.7-fpm-bookworm AS php
...
# Nginx image
FROM nginx:alpine
...
COPY --from=php /app/pub/static /app/pub/static
Wrapping up
At this point, you should have a working Dockerfile
that builds a Docker image containing your Magento / Adobe Commerce project files, ready to be deployed.
To build the resulting PHP-FPM and nginx images, run the following commands:
docker build --target php -t my-namespace/magento-php:<tag> .
docker build --target nginx -t my-namespace/magento-nginx:<tag> .
Then push the images to your container registry, using docker push
.
TIP
When cross-building images for different architectures, you may want to use the --platform
flag to specify the target architecture, such as linux/amd64
, linux/arm64
, etc.
INFO
The complete Dockerfile
can be found here.
Tagging strategy
It's a good practice to tag your images with the Git commit (short) hash, to be able to trace back the image to the code it was built from.
To make sure your deployments are consistent between environments, you should use the same image for all environments, and use environment variables to configure the image.
As Docker images can have multiple tags, here are some additional tags you may want to use:
latest
: the latest version of the image<tag>
: the Git tag the image was built from<branch>
: the Git branch the image was built from