Setting Up a Local Multi-Store Development Environment for Magento 2
AI generated
M2
di.xml
Magento 2 · Developer Workflow · Docker · Multi-Store
Multi-Store Development Environment for Magento 2
Rebuilding websites, store views and domains realistically

Teams that only discover multi-store bugs in staging lose time and trust. A local multi-store development environment with its own domains, real store views and store-specific configuration values exposes bugs that a single-store instance would never surface.

18 min read Docker · Nginx · Store View · Website · Fixtures Magento 2.4.8 · Mark Shust Docker

1. Why a local multi-store environment matters

Many Magento teams develop for months against a single store view and then wonder why prices suddenly render incorrectly in staging, or why a layout update only takes effect on one of three domains. The reason is almost always the same: the local instance never had a real multi-store development environment, but ran with a single default store, while production has long operated multiple websites with their own domains, languages and price books. Bugs that only occur through the interplay of several store codes get systematically missed.

A properly built multi-store development environment replicates the complete website, store group and store view hierarchy locally, including a dedicated domain per store view. It costs some time upfront, but pays off for every feature that touches store-specific behavior: price rules, shipping methods, payment configuration, CMS blocks and layout updates. Teams that only test these cases in production are testing too late.

This article walks through the complete build of a multi-store development environment: from the Docker Compose setup with multiple domains, through Nginx vhosts, to store-specific fixtures. The goal is a local instance that behaves exactly like production, just on your own machine.

2. Docker Compose setup for multiple websites

The first building block of a multi-store development environment is the Docker configuration itself. With the Mark Shust setup, only a single Magento container runs regardless of how many websites are defined in the backend, because Magento resolves websites through domains and MAGE_RUN_CODE, not through separate processes. The real work happens not in additional containers but in the Nginx configuration and additional host entries that all point to the same PHP-FPM upstream.

Still, a dedicated Docker service for a second Nginx vhost is worthwhile if domains need different SSL certificates or ports. In most cases, though, a single Nginx container with multiple server blocks, one per store domain, is enough. What matters is that all domains land on the same Docker network bridge and are forwarded to PHP-FPM through the same fastcgi_pass, so sessions, cache and database stay consistent.


#!/usr/bin/env bash
# Extend the existing Mark Shust docker-compose with additional domains
# for a Magento 2 multi-store development environment

# 1. Add all store domains to /etc/hosts (or use dnsmasq for wildcard domains)
echo "127.0.0.1 shop-de.test shop-at.test shop-en.test" | sudo tee -a /etc/hosts

# 2. Generate a self-signed cert covering all local store domains
bin/generate-cert shop-de.test shop-at.test shop-en.test

# 3. Restart nginx so it picks up the additional server blocks
bin/restart nginx

# 4. Verify all three domains resolve to the same Magento container
for domain in shop-de.test shop-at.test shop-en.test; do
  curl -sk -o /dev/null -w "%{http_code} $domain\n" "https://$domain/"
done

3. Domains and hosts file for store views

Every store view in a clean multi-store development environment needs its own resolvable domain, because Magento's URL detection is primarily based on the requested host. Locally, the /etc/hosts file takes over this job: each store domain gets mapped to 127.0.0.1 so the browser sends the request to the local Docker container, while Magento identifies which store view is meant based on the hostname.

A common pitfall: developers use the same local domain for all stores with different paths, like localhost/de and localhost/at. That may work for simple language switching, but it does not cover bugs based on actual domain detection, such as cookie scoping, CORS headers on GraphQL requests, or store-specific redirects. For a resilient multi-store development environment, separate top-level domains like shop-de.test and shop-at.test are the only option that truly mirrors production behavior.

The .test suffix is not an accident: it is reserved for local testing under RFC 2606 and never collides with real public domains. Certificates for these domains can be generated with mkcert or the Docker setup's own generate-cert command, so HTTPS-dependent features like service workers or payment SDKs can also be tested correctly locally.

4. Nginx vhosts and MAGE_RUN_CODE

The actual core of domain resolution lies in the interplay between the Nginx vhost and the environment parameter MAGE_RUN_CODE. Each server block in the Nginx configuration corresponds to a store domain and sets fastcgi_param MAGE_RUN_CODE and MAGE_RUN_TYPE to the respective store or website code. Magento reads these two parameters at bootstrap and loads exactly the associated store configuration, regardless of what is in the URL itself.

In a multi-store development environment, this configuration must stay consistent with the store codes in the backend. A typo in MAGE_RUN_CODE does not raise an error but silently falls back to the default store, which is especially tricky because everything appears to work, just with the wrong data. That is why the store code in the Nginx configuration should be regularly cross-checked with bin/magento store:list.


# nginx vhost snippet for one store domain in a multi-store setup
server {
    listen 443 ssl;
    server_name shop-at.test;

    ssl_certificate     /etc/nginx/certs/shop-at.test.pem;
    ssl_certificate_key /etc/nginx/certs/shop-at.test.key;

    set $MAGE_ROOT /var/www/html;
    set $MAGE_MODE developer;

    include /etc/nginx/magento.conf;

    location ~ \.php$ {
        fastcgi_pass php-fpm:9000;
        fastcgi_param MAGE_RUN_CODE "store_at";
        fastcgi_param MAGE_RUN_TYPE "store";
        include fastcgi_params;
        include fastcgi.conf;
    }
}

5. app/etc/config.php and store-specific values

Declarative configuration management through app/etc/config.php is a central building block of any multi-store development environment, because it lets you version all store-specific system configuration values instead of leaving them only in the database. After bin/magento app:config:dump, values like payment methods, shipping configuration and base URLs per scope land in this file, and every developer gets identical store configurations at checkout without swapping database dumps.

The scope mechanism in config.php follows the default, website, store hierarchy, where more specific scopes override more general ones. For a multi-store development environment that means: base URLs are set per store, while shared settings like the product catalog cache stay in the default scope. This separation immediately shows which configuration is genuinely store-specific and which was accidentally duplicated.


<?php
// Excerpt from app/etc/config.php after app:config:dump
// Shows how store-specific base URLs are stored per scope
return [
    'system' => [
        'default' => [
            'web' => [
                'secure' => [
                    'base_url' => 'https://shop-de.test/',
                ],
            ],
        ],
        'websites' => [
            'website_at' => [
                'web' => [
                    'secure' => [
                        'base_url' => 'https://shop-at.test/',
                    ],
                ],
                'general' => [
                    'locale' => [
                        'code' => 'de_AT',
                    ],
                ],
            ],
        ],
        'stores' => [
            'store_en' => [
                'general' => [
                    'locale' => [
                        'code' => 'en_GB',
                    ],
                ],
            ],
        ],
    ],
];

A common mistake when working with config.php in a multi-store development environment: developers edit values directly in the database through the admin, without producing a new dump afterward. The file then drifts from the actual configuration, and the manual changes get overwritten on the next deployment. The correct workflow is always: change in admin, then app:config:dump, then commit the file.

6. Creating website, store group and store view

Magento's store hierarchy consists of three levels: website, store group and store view. The website defines the technical separation, such as different domains or payment providers. The store group bundles store views that share a product catalog. The store view itself is the smallest unit and controls language, theme and visibility. For a resilient multi-store development environment, all three levels must exist locally, not just a single store view with a switched language.

Creation happens via Stores > All Stores in the admin or through a CLI script using WebsiteRepositoryInterface, GroupRepositoryInterface and StoreRepositoryInterface. For local setups, a reproducible PHP script or a data patch is recommended that automatically builds the complete hierarchy when setting up the multi-store development environment, instead of recreating it manually in the backend. That makes the setup reproducible in minutes rather than hours for new team members.

Correct root category assignment per store view is important, because a misassigned root category leads to empty category trees in the frontend even though the products exist in the database. This mistake occurs particularly often in multi-store development environments when store views are created by copy-paste in the backend and the root category is accidentally inherited from the default store.

7. Languages, currencies and locale per store view

Once the hierarchy is in place, the actual store view configuration follows: locale code, base currency, allowed currencies and time zone. These values are set under General > Locale Options and General > Currency Setup per scope, and are one of the most common reasons teams need a multi-store development environment in the first place: without different locales, formatting bugs in prices, dates and numeric values can never be reproduced locally.

A concrete example: the German store uses de_DE with a comma as decimal separator, while the Austrian store shares the same language but needs its own price display with a different VAT class. Without separate store views with their own locale and tax configuration, this difference only becomes visible in production, usually through a customer complaint about an incorrect final price.

For translations themselves, the multi-store development environment should additionally have the matching i18n packages installed (bin/composer require magento/language-de_at, for example), so backend translations and system messages can be tested store-correctly as well, not just frontend text from CMS blocks.

8. Test data and fixtures per store

Realistic test data is the last missing building block. A multi-store development environment without store-specific product prices, category visibilities and stock levels tests the configuration but not the actual business behavior. Magento's testing framework provides fixtures that can be targeted at a specific store scope, such as #storeView store_at as an annotation in an MFTF test, or directly as a parameter during CSV import.

For daily development, a data patch that sets different prices, special offers and visibilities per store for the same test SKUs is worthwhile. This lets you switch between store views with a few clicks and immediately see whether a new price rule or layout update works consistently across stores.


<?php
declare(strict_types=1);

namespace Mironsoft\DevFixtures\Setup\Patch\Data;

use Magento\Framework\Setup\Patch\DataPatchInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Store\Api\StoreRepositoryInterface;

/**
 * Sets store-specific prices for the shared demo SKU across all
 * store views in the local multi-store development environment.
 */
class SetStoreSpecificPrices implements DataPatchInterface
{
    /**
     * @param ModuleDataSetupInterface $moduleDataSetup Module setup helper
     * @param ProductRepositoryInterface $productRepository Product repository
     * @param StoreRepositoryInterface $storeRepository Store repository
     */
    public function __construct(
        private readonly ModuleDataSetupInterface $moduleDataSetup,
        private readonly ProductRepositoryInterface $productRepository,
        private readonly StoreRepositoryInterface $storeRepository,
    ) {
    }

    /**
     * Applies the data patch.
     *
     * @return void
     */
    public function apply(): void
    {
        $prices = ['store_de' => 49.90, 'store_at' => 54.90, 'store_en' => 44.90];
        foreach ($prices as $storeCode => $price) {
            $store = $this->storeRepository->get($storeCode);
            $product = $this->productRepository->get('demo-sku', true, $store->getId());
            $product->setPrice($price);
            $this->productRepository->save($product);
        }
    }

    /**
     * @return array
     */
    public static function getDependencies(): array
    {
        return [];
    }

    /**
     * @return array
     */
    public function getAliases(): array
    {
        return [];
    }
}

9. Single-store vs. multi-store compared

The difference between a single-store setup and a real multi-store development environment shows up most clearly in the classes of bugs each one finds. The following table contrasts both approaches.

Aspect Single-Store Setup Multi-Store Development Environment
Domain detection Never tested Fully covered
Store-specific pricing Not reproducible Covered via fixtures
Locale and tax bugs Only visible in staging Reproducible locally
Setup effort Low Higher once, then amortized
Cache and session scoping Not tested Realistically covered

The one time extra effort of building the multi-store development environment pays for itself within a few sprints, as soon as the first store-specific bug ticket can be reproduced locally instead of in production. Teams that regularly manage multiple websites should firmly anchor this setup in the onboarding process for new developers.

Mironsoft

Magento 2 development, multi-store architecture and Hyvä themes

A multi-store setup that finds bugs before customers report them?

We build local multi-store development environments that precisely mirror your production websites, domains and store views, including Docker, Nginx vhosts and store-specific test data.

Environment audit

Assess your existing dev environment for multi-store readiness

Setup automation

Data patches and scripts for reproducible store hierarchies

Onboarding

Get new developers productive in hours instead of days

10. Summary

A local multi-store development environment is not a luxury reserved for large enterprise projects, but the only way to reliably test store-specific behavior before it affects customers in production. The build follows a clear pattern: dedicated domains per store view, Nginx vhosts with the correct MAGE_RUN_CODE, versioned configuration through app/etc/config.php, and store-specific fixtures for realistic test data.

The biggest misconception is treating multi-store as nothing more than language switching. Real websites differ in domains, price books, tax classes, payment providers and cache scoping, and only a complete multi-store development environment covers these differences locally. The one time setup effort pays off with the very first cross-store bug fix.

Multi-Store Development Environment for Magento 2 — Key Takeaways

Domains

Dedicated .test domain per store view in /etc/hosts, no path-based language switching.

Nginx and MAGE_RUN_CODE

One vhost per domain with the correct MAGE_RUN_CODE, otherwise a silent fallback to the default store.

Configuration

Re-dump and commit app/etc/config.php after every admin change, otherwise the configuration drifts.

Fixtures

Data patches with store-specific prices and visibilities for reproducible tests.

11. FAQ: Multi-Store Development Environment for Magento 2

1Is switching language in frontend enough?
No. Without a dedicated domain, cookie scoping, cache segmentation and per-website price rules are never tested.
2Separate container per store view?
No. Magento resolves stores via MAGE_RUN_CODE and domain, one container with multiple Nginx vhosts is enough.
3Typo in MAGE_RUN_CODE?
Leads to a silent fallback to the default store, not a visible error. Cross-check regularly with store:list.
4Keep config.php in sync?
Run app:config:dump and commit after every admin change, otherwise the file drifts.
5Empty category tree in new store view?
Usually a wrong root category assignment after copy-paste creation. Check root category per store view.
6Test store-specific prices locally?
Via data patch or CSV import with a store view column, without manual backend clicks.
7Which domain suffix locally?
.test is reserved under RFC 2606, never collides with real domains. Avoid .local and .dev.
8Separate SSL certificate per domain?
Yes, for HTTPS-dependent features. mkcert or generate-cert produce trusted certificates for multiple domains.
9How many store views at minimum?
At least two with different locales, to provoke store-specific bugs at all.
10How long does the build take?
With a prepared data patch, usually one to two hours; without automation, a full working day.