Magento on OxPHP

Magento is the heaviest recipe here. It mandates a search engine (OpenSearch), a long extension list, and a static-content deployment step. It runs in OxPHP's framework routing mode with pub/ as the document root, but its versioned static-asset URLs need one extra step, described below.

Stack at a glance

  • OxPHP image: ghcr.io/oxphp/oxphp:0.10.0-php8.4-alpine3.23 (PHP 8.4)
  • Routing mode: Framework (ENTRY_FILE=index.php; DOCUMENT_ROOT=/var/www/html/pub overrides the default …/public)
  • Extensions added: bcmath, gd, intl, pdo_mysql, soap, xsl, zip, mbstring, ftp, pcntl, sockets
  • Services: OxPHP + MySQL 8.0 + OpenSearch 2.x
  • URL: http://localhost:8093 · admin /admin · internal http://localhost:9094/health
PHP version

Magento 2.4.8 declares "php": "~8.2 || ~8.3 || ~8.4" and rejects PHP 8.5, so pin the PHP 8.4 image. ext-sockets is a transitive requirement (php-amqplib) and needs linux-headers to compile.

Project layout

Magento Open Source from the official distribution channel needs Adobe Marketplace auth keys. To install without keys, clone the open-source repository (its modules are provided in-tree via replace, so composer install pulls only Packagist dependencies):

bash
mkdir -p magento-oxphp git clone --branch 2.4.8 --depth 1 https://github.com/magento/magento2.git magento-oxphp/src # composer install runs later, inside the PHP 8.4 container (composer:2 is PHP 8.5 # and Magento rejects it): docker compose run --rm --no-deps -e COMPOSER_MEMORY_LIMIT=-1 app \ composer install --no-interaction --prefer-dist

Dockerfile and compose file

src/Dockerfile.oxphp is named to avoid colliding with Magento's own docker assets.

src/Dockerfile.oxphp
ARG OXPHP_VERSION=0.10.0 ARG PHP_VERSION=8.4 ARG ALPINE_VERSION=3.23 # ── PHP base: Magento runtime extensions ────────────────────── FROM php:${PHP_VERSION}-zts-alpine${ALPINE_VERSION} AS php-base RUN apk add --no-cache \ icu-libs libpng libjpeg-turbo freetype oniguruma libzip libxslt \ && apk add --no-cache --virtual .build-deps \ icu-dev libpng-dev libjpeg-turbo-dev freetype-dev oniguruma-dev \ libzip-dev libxslt-dev libxml2-dev linux-headers \ && docker-php-ext-configure gd --with-jpeg --with-freetype \ && docker-php-ext-install -j"$(nproc)" \ bcmath gd intl pdo_mysql soap xsl zip mbstring ftp pcntl sockets \ && apk del .build-deps # ── Composer ────────────────────────────────────────────────── FROM composer:2 AS composer # ── OxPHP artifacts (PHP 8.4 image) ─────────────────────────── FROM ghcr.io/oxphp/oxphp:${OXPHP_VERSION}-php${PHP_VERSION}-alpine${ALPINE_VERSION} AS oxphp # ── dev: OxPHP server + PHP CLI + Composer + Magento extensions ─ FROM php-base AS dev RUN apk add --no-cache libgcc git patch COPY --from=composer /usr/bin/composer /usr/local/bin/composer COPY --from=oxphp /usr/local/bin/oxphp /usr/local/bin/oxphp COPY --from=oxphp /usr/local/lib/liboxphp_bridge.so /usr/local/lib/ COPY --from=oxphp /usr/local/lib/php/extensions/ /tmp/oxphp-ext/ RUN cp /tmp/oxphp-ext/*/oxphp_sapi.so "$(php -r 'echo ini_get("extension_dir");')/" \ && rm -rf /tmp/oxphp-ext \ && echo "extension=oxphp_sapi.so" > /usr/local/etc/php/conf.d/oxphp-ext.ini RUN { \ echo "[opcache]"; echo "opcache.enable=1"; echo "opcache.enable_cli=1"; \ echo "opcache.validate_timestamps=1"; echo "opcache.revalidate_freq=0"; \ } > /usr/local/etc/php/conf.d/opcache-dev.ini RUN { \ echo "memory_limit=4G"; echo "max_execution_time=1800"; \ echo "realpath_cache_size=10M"; echo "realpath_cache_ttl=86400"; \ echo "upload_max_filesize=64M"; echo "post_max_size=64M"; \ } > /usr/local/etc/php/conf.d/magento.ini RUN getent passwd www-data >/dev/null \ || adduser -D -H -u 82 -G www-data -s /sbin/nologin www-data RUN mkdir -p /var/www/html/pub && chown -R www-data:www-data /var/www/html ENV LD_LIBRARY_PATH=/usr/local/lib WORKDIR /var/www/html EXPOSE 80 9090 CMD ["oxphp"]

Install and first run

bash
docker compose up -d db opensearch # composer install (see Project layout), then: docker compose run --rm app php bin/magento setup:install \ --base-url=http://localhost:8093/ \ --db-host=db --db-name=magento --db-user=magento --db-password=magento \ --admin-firstname=Admin --admin-lastname=User \ [email protected] --admin-user=admin --admin-password='Admin123!' \ --language=en_US --currency=USD --timezone=America/New_York \ --search-engine=opensearch --opensearch-host=opensearch --opensearch-port=9200 \ --opensearch-index-prefix=magento --opensearch-enable-auth=0 # production mode compiles DI and deploys static content docker compose run --rm app php bin/magento deploy:mode:set production # CRITICAL for OxPHP: make versioned static URLs resolve (see notes below) docker compose run --rm app sh -c \ 'ln -sfn . "pub/static/version$(cat pub/static/deployed_version.txt)"' docker compose up -d app

OxPHP notes

Note
  • Versioned static URLs need a symlink. Magento emits asset URLs like /static/version<timestamp>/frontend/…, while the files live at pub/static/frontend/…. nginx strips the version<N>/ segment with a rewrite rule; OxPHP's framework mode has no such rewrite, so every versioned asset would 404 and the storefront would render unstyled. The fix is a self-referential symlink that makes the versioned path resolve to the real files:

    bash
    ln -sfn . "pub/static/version$(cat pub/static/deployed_version.txt)"

    Because the symlink stays inside DOCUMENT_ROOT (pub/), no SYMLINK_ALLOW_PATHS entry is required.

  • Run production mode. OxPHP's worker pool is multi-threaded (PHP ZTS). Magento developer mode generates DI classes and static assets on the fly (via pub/static.php, which framework mode does not route), which risks races across worker threads. deploy:mode:set production pre-compiles DI and deploys static content up front, so workers never generate code at request time.

  • MySQL: --log_bin_trust_function_creators=1. Magento creates triggers and stored functions during install; with binary logging enabled (the MySQL 8.0 default) the non-SUPER magento user otherwise hits error 1419.

  • composer install runs in the PHP 8.4 container. The stock composer:2 image is PHP 8.5, which Magento rejects; run it through the built app image instead.

Verify

bash
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8093/ # 200 storefront curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8093/admin # 200 admin login curl -s http://localhost:8093/ | grep -oE '/static/version[0-9]+/[^"]+\.css' | head -1 | \ xargs -I{} curl -s -o /dev/null -w "%{http_code}\n" "http://localhost:8093{}" # 200 curl -s -o /dev/null -w "%{http_code}\n" http://localhost:9094/health # 200

See also