Running It Locally

Why Magento belongs in containers, and a compose file with PHP 8.4, MySQL, OpenSearch and Valkey beside the code.

Syntaxdocker compose exec php bin/magento <command>

Magento is not one program. A working install needs PHP with roughly a dozen extensions, a relational database, a search engine, a cache and session backend, a message broker for queues, and a cron process. Adobe supports only specific combinations of those versions, published in the system requirements table per release. For 2.4.9 that table lists MySQL 8.4 or MariaDB, OpenSearch 3, and Valkey — an open-source fork of Redis that speaks the same protocol — for cache and session storage. Check the table for your patch release before pinning image tags.

Why not directly on the laptop

A Homebrew or apt PHP is one version for the whole machine, and the next project will want a different one. OpenSearch and Valkey become background services you forget are running. Nothing records which versions you ended up with, so your install and your colleague's install differ in ways neither of you can see. Containers make the versions a file in the repository.

The shape of the stack

Five services: php (the only one that runs your code), nginx, db, opensearch and valkey. Each service name is the hostname the others use, which is what you pass to setup:install as --db-host=db and --opensearch-host=opensearch.

What trips people up

Every bin/magento command must run inside the PHP container: docker compose exec php bin/magento cache:flush. Run on the host it uses the host's PHP and cannot resolve db at all. Adobe ships Cloud Docker for Commerce for projects on cloud infrastructure; for an on-premise Open Source install you write the compose file or use a community stack.

Example

Example · yaml
# docker-compose.yml - project root, beside app/, pub/ and composer.json
services:
  php:
    build: ./docker/php          # PHP 8.4 FPM plus Magento's extensions
    volumes:
      - ./:/var/www/html
    depends_on:
      - db
      - opensearch
      - valkey
  nginx:
    image: nginx:stable
    volumes:
      - ./:/var/www/html
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
    ports:
      - "8080:80"                # short syntax
      # the same mapping in long syntax - these are identical:
      # - target: 80
      #   published: "8080"
      #   protocol: tcp
    depends_on:
      - php
  db:
    image: mysql:8.4
    environment:
      MYSQL_ROOT_PASSWORD: magento
      MYSQL_DATABASE: magento
    volumes:
      - dbdata:/var/lib/mysql
  opensearch:
    image: opensearchproject/opensearch:3
    environment:
      - discovery.type=single-node
      - plugins.security.disabled=true
      - OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m
  valkey:
    image: valkey/valkey:9-alpine
volumes:
  dbdata:

When to use it

  • Stand up a local Magento that matches the versions a client's production stack runs.
  • Keep two client projects on different PHP and OpenSearch versions on one machine.
  • Give a new developer a working environment from a clone and one compose command.
  • Reproduce a search bug that only appears with OpenSearch rather than a bare install.

More examples

Installing through the container

The host values are compose service names, not localhost, because the PHP container resolves them on the compose network. Reach for this whenever you rebuild an environment from scratch; keep it in the repository as a script so the next person does not have to reconstruct the flags. The storefront then answers on http://localhost:8080/ and the Admin on the URI setup:install reports.

Example · bash
docker compose up -d
docker compose exec php composer install

docker compose exec php bin/magento setup:install \
  --base-url=http://localhost:8080/ \
  --db-host=db --db-name=magento --db-user=root --db-password=magento \
  --search-engine=opensearch \
  --opensearch-host=opensearch --opensearch-port=9200 \
  --admin-firstname=Dev --admin-lastname=User \
  [email protected] \
  --admin-user=admin --admin-password=Password123 \
  --language=en_US --currency=USD --timezone=UTC

docker compose exec php bin/magento deploy:mode:set developer

The PHP image

Magento checks its required extensions during setup:install and refuses to continue when one is missing, so the extension list belongs in the image rather than in a wiki page. Copying a conf.d directory in gives you one place to set memory_limit and to drop an Xdebug ini later. Rebuild with docker compose build php after any change here.

Example · dockerfile
# docker/php/Dockerfile
FROM php:8.4-fpm

RUN apt-get update && apt-get install -y --no-install-recommends \
      libicu-dev libxml2-dev libxslt1-dev libzip-dev \
      libpng-dev libjpeg-dev libfreetype6-dev unzip git \
    && docker-php-ext-configure gd --with-jpeg --with-freetype \
    && docker-php-ext-install -j"$(nproc)" \
      bcmath gd intl pdo_mysql soap sockets xsl zip \
    && rm -rf /var/lib/apt/lists/*

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
COPY conf.d/ /usr/local/etc/php/conf.d/

WORKDIR /var/www/html

The commands you run every day

Shows the single habit that prevents most local confusion. The ownership line matters on Linux hosts, where a root-owned var/cache written by a container command then blocks the next command run as your own user. On macOS the bind mount hides the problem until you copy the setup to a Linux CI runner.

Example · bash
# always through the container, never on the host
docker compose exec php bin/magento cache:flush
docker compose exec php bin/magento setup:upgrade

# run scheduled work on demand instead of waiting for cron
docker compose exec php bin/magento cron:run

# php:8.4-fpm runs as root, so files written by these commands are root-owned;
# match the host user to keep var/ and generated/ writable from your editor
docker compose exec --user "$(id -u):$(id -g)" php bin/magento cache:flush

Discussion

  • Be the first to comment on this lesson.