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.
Table of Contents
- 1. Why a local multi-store environment matters
- 2. Docker Compose setup for multiple websites
- 3. Domains and hosts file for store views
- 4. Nginx vhosts and MAGE_RUN_CODE
- 5. app/etc/config.php and store-specific values
- 6. Creating website, store group and store view
- 7. Languages, currencies and locale per store view
- 8. Test data and fixtures per store
- 9. Single-store vs. multi-store compared
- 10. Summary
- 11. FAQ
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.