Magento 2 Multi-Store & Multi-Website Setup (Step by Step)
One Magento install can serve any number of brands, regions, and languages — but only if you map the Website, Store, and Store View scopes correctly from the start. This guide walks you through every step: admin creation, nginx virtual host wiring, per-scope CLI config, shared vs separate catalogs, and the common pitfalls that break production multi-store setups.
A single Magento 2.4.4 — 2.4.9 installation can power multiple storefronts under one codebase and one database. The Website → Store → Store View hierarchy is the mechanism that separates them: each scope controls a different layer of the shopping experience, from checkout and currency down to language and theme. Get the scope structure right early and adding a new country or brand later is a two-hour task; get it wrong and you end up rebuilding URL rewrites across a production catalog. This guide walks through every step of a magento 2 multi store setup, from the first admin click to the nginx virtual-host and CLI config patterns that keep environments reproducible.
Understanding the Website → Store → Store View hierarchy
Before you create anything, you need a firm mental model of the three scopes. Misconfiguring them is the root cause of most multi-store problems — shared customer accounts when they should be separate, wrong currencies on checkout, or translations that bleed across brands.
| Scope | What it controls | Typical use |
|---|---|---|
| Website | Customers, orders, payment/shipping methods, base currency | Separate brand or region with its own checkout |
| Store (Group) | Root catalog category | Different product trees under one website |
| Store View | Language, locale, theme, presentation | Translations of the same store |
A practical example: you run a sporting-goods brand in the US and a separate outdoor-gear brand in the UK. That is two Websites — separate customers, separate orders, separate currencies (USD / GBP). Each website has one Store that owns its own root category (so the product trees are completely independent). Each Store has two Store Views — English and Spanish for the US site, English and Welsh for the UK site.
If instead you want one brand with one checkout but two language versions, you need one Website, one Store, and two Store Views. Customers are shared; only the presentation changes.
Choose the Website boundary based on whether customers and orders must be separate. If checkout, currency, and customer accounts can be shared, a Store View is the right scope — not a new Website. Creating a Website when you only need a Store View doubles your configuration surface for no benefit.
Step 1 — Create the Website, Store, and Store View in admin
All three objects live in Stores → All Stores. The UI enforces the hierarchy: you must create a Website before you can create a Store under it, and a Store before you can create a Store View.
1a. Create the Website
- Go to Stores → All Stores → Create Website.
- Set a human-readable Name (e.g.
UK Outdoor Gear). - Set a machine-readable Code (e.g.
uk_outdoor). This is the value you will use inMAGE_RUN_CODEand--scope-code. It must be lowercase, alphanumeric, and start with a letter. - Set Sort Order if the sequence matters on the storefront switcher.
- Save.
1b. Create the Store (Group)
- Create Store.
- Set Website to the one you just created.
- Set Name (e.g.
UK Outdoor Gear Store). - Set Root Category. If you want a completely separate product tree, create a new root category first under Catalog → Categories and select it here. If you select the default root category, both stores share the same product tree — useful for multi-currency/multi-language setups where the catalog is identical.
- Save.
1c. Create the Store View
- Create Store View.
- Set Store to the group you created.
- Set a Name (e.g.
English (UK)) and a Code (e.g.uk_outdoor_en). This code is used whenMAGE_RUN_TYPE=store. - Set Status to Enabled.
- Save.
You now have the skeleton. No storefront traffic reaches it yet — that requires base URL config and nginx wiring in the next steps.
Step 2 — Set base URLs per website
Every Website needs its own base URL. Without it, Magento generates links pointing to the default website domain — a silent bug that only surfaces when a customer from the second site clicks a product link and lands on the wrong storefront.
Via admin
- Go to Stores → Configuration → General → Web → Base URLs.
- Switch the scope selector (top-left) to your new Website.
- Uncheck Use Default for Base URL and Base Link URL, then enter your domain:
https://uk.yourbrand.com/(trailing slash required). - Repeat under Base URLs (Secure) for the HTTPS version.
- Save Config → flush cache.
Via CLI (recommended for reproducibility)
# Set base URLs for the website scope
bin/magento config:set \
--scope=websites \
--scope-code=uk_outdoor \
web/unsecure/base_url \
https://uk.yourbrand.com/
bin/magento config:set \
--scope=websites \
--scope-code=uk_outdoor \
web/secure/base_url \
https://uk.yourbrand.com/
# Force secure on storefront
bin/magento config:set \
--scope=websites \
--scope-code=uk_outdoor \
web/secure/use_in_frontend 1
bin/magento cache:flushThe --scope flag accepts websites or stores (plural). The --scope-code is the Website or Store View code you set in admin — not the numeric ID, not the name. Getting these two values wrong silently writes config to the wrong scope and takes time to diagnose.
Step 3 — Wire nginx to pass MAGE_RUN_CODE and MAGE_RUN_TYPE
Magento bootstraps the correct scope from two PHP environment variables: MAGE_RUN_CODE and MAGE_RUN_TYPE. In nginx, you pass them as fastcgi_param values inside the PHP location block. There are two patterns: a single server block with a map directive (preferred for many domains), or separate server blocks per domain.
Pattern A — nginx map directive (preferred)
Add the map outside your server blocks, typically at the top of /etc/nginx/conf.d/magento.conf or inside an http block:
map $http_host $mage_run_code {
default base;
uk.yourbrand.com uk_outdoor;
de.yourbrand.com de_outdoor;
}
map $http_host $mage_run_type {
default website;
uk.yourbrand.com website;
de.yourbrand.com website;
}Then inside the location ~ \.php$ block, add the two params before the include fastcgi_params line:
location ~ \.php$ {
fastcgi_param MAGE_RUN_TYPE $mage_run_type;
fastcgi_param MAGE_RUN_CODE $mage_run_code;
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}Reload nginx: nginx -t && systemctl reload nginx.
Pattern B — separate server blocks per domain
Use this when each domain has distinct SSL certificates or different PHP-FPM pools:
server {
listen 443 ssl;
server_name uk.yourbrand.com;
location ~ \.php$ {
fastcgi_param MAGE_RUN_TYPE website;
fastcgi_param MAGE_RUN_CODE uk_outdoor;
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}Pattern C — index.php environment injection (fallback / staging)
If you cannot touch nginx (shared hosting, legacy setup), set the variables directly in pub/index.php before the bootstrap call. This approach works but is less clean than nginx — you couple deployment config to a tracked file.
<?php
// pub/index.php - add BEFORE the bootstrap require lines
$host = $_SERVER['HTTP_HOST'] ?? '';
if ($host === 'uk.yourbrand.com') {
$_SERVER['MAGE_RUN_TYPE'] = 'website';
$_SERVER['MAGE_RUN_CODE'] = 'uk_outdoor';
} elseif ($host === 'de.yourbrand.com') {
$_SERVER['MAGE_RUN_TYPE'] = 'website';
$_SERVER['MAGE_RUN_CODE'] = 'de_outdoor';
}
// ... existing bootstrap code below ...
require __DIR__ . '/../app/bootstrap.php';
$bootstrap = \Magento\Framework\App\Bootstrap::create(BP, $_SERVER);
$app = $bootstrap->createApplication(\Magento\Framework\App\Http::class);
$bootstrap->run($app);MAGE_RUN_TYPE accepts exactly two values: website or store. Use website when your MAGE_RUN_CODE is a Website code; use store when it is a Store View code. Mixing them up loads the wrong scope silently — no PHP error, just wrong prices and wrong locale.
Step 4 — Shared vs separate catalog per website
Magento lets you share the same product catalog across all websites or give each website its own isolated product tree. The decision is made at the Store level via the root category setting.
Shared catalog (same root category)
Both stores point to the same root category. Products are assigned once and appear on all storefronts. Website-level pricing can still differ via catalog price rules scoped to a website. This is the right choice for multi-language setups where the catalog is identical — no duplicate content in the DB, one indexer run covers everything.
Separate catalogs (different root categories)
Each store gets its own root category. Products assigned to Catalog A are invisible on the store that uses Catalog B as its root. Useful when brands carry entirely different SKUs. The trade-off: you manage two category trees and the indexer has more work to do.
Per-website pricing
Even on a shared catalog, you can set different prices per website. Go to a product, switch the scope selector to your target website, and set the Price field. Or use catalog price rules with a website condition.
Per-website product visibility
Product → Websites tab controls which websites a product is assigned to. A product not assigned to a website is invisible on that storefront even if both stores share a root category.
Step 5 — Per-store config: locales, currencies, and other settings
Once the scopes exist and URLs are wired, push locale and currency config per scope via CLI. Always use CLI in CI/CD pipelines — admin saves can be lost in a deploy that overwrites the database config table.
# Locale and timezone for a store view
bin/magento config:set \
--scope=stores \
--scope-code=uk_outdoor_en \
general/locale/code en_GB
bin/magento config:set \
--scope=stores \
--scope-code=uk_outdoor_en \
general/locale/timezone Europe/London
# Base and display currency for a website
bin/magento config:set \
--scope=websites \
--scope-code=uk_outdoor \
currency/options/base GBP
bin/magento config:set \
--scope=websites \
--scope-code=uk_outdoor \
currency/options/default GBP
bin/magento config:set \
--scope=websites \
--scope-code=uk_outdoor \
currency/options/allow GBP
# Email sender identity per store view
bin/magento config:set \
--scope=stores \
--scope-code=uk_outdoor_en \
trans_email/ident_general/name "UK Outdoor Gear"
bin/magento config:set \
--scope=stores \
--scope-code=uk_outdoor_en \
trans_email/ident_general/email hello@uk.yourbrand.com
# Flush after all config:set calls
bin/magento cache:flushStep 6 — Reindex and cache flush after scope changes
This step is not optional. Every time you create a new scope, change a base URL, or modify catalog visibility, Magento's index tables contain stale data. The most visible symptom is 404s on category pages and product URLs — the URL-rewrite index still holds the old store structure.
# Full reindex (safe on staging)
bin/magento indexer:reindex
# Targeted reindex for URL and catalog changes
bin/magento indexer:reindex \
catalog_product_price \
catalogrule_product \
catalog_product_attribute \
catalog_category_product \
catalogsearch_fulltext \
url_rewrite
bin/magento cache:flushOn production, switch URL-rewrite and catalog indexers to Update by Schedule before making bulk scope changes:
# Switch to update-by-schedule to avoid blocking storefront
bin/magento indexer:set-mode schedule url_rewrite catalog_category_product
# Confirm mode
bin/magento indexer:show-modeAfter any multi-store change — new website, new store view, base URL edit, root category swap — always run indexer:reindex url_rewrite and cache:flush as a pair. Skipping either produces 404s, wrong URLs in navigation, or stale full-page cache serving the old storefront on the new domain.
Common pitfalls and how to avoid them
Pitfall 1 — Cookie domain conflicts
When multiple websites share the same second-level domain (e.g. us.brand.com and uk.brand.com), the session cookie domain matters. By default Magento sets the cookie domain to the current request host, so sessions are naturally isolated per subdomain. If you need cross-subdomain sessions for a shared login, set web/cookie/cookie_domain to .brand.com — but be aware that doing so will let a session from one website authenticate on another, which is rarely correct for separate-checkout setups.
# Isolate cookies per website (recommended — leave this key empty or unset)
# bin/magento config:set web/cookie/cookie_domain ""
# Share cookies across all subdomains of yourbrand.com (use carefully)
bin/magento config:set \
--scope=websites \
--scope-code=uk_outdoor \
web/cookie/cookie_domain .yourbrand.comPitfall 2 — SSL and secure base URLs
A common mistake is setting the secure base URL for a new website but forgetting to set web/secure/use_in_frontend to 1. Without that flag, Magento still serves HTTP even when nginx forces HTTPS, causing mixed-content warnings and occasionally redirect loops.
bin/magento config:set \
--scope=websites \
--scope-code=uk_outdoor \
web/secure/use_in_frontend 1
bin/magento config:set \
--scope=websites \
--scope-code=uk_outdoor \
web/secure/use_in_adminhtml 1Pitfall 3 — base_url must have a trailing slash
Magento's URL builder appends paths to the base URL string. If you omit the trailing slash, all storefront links break with a doubled path segment (https://uk.yourbrand.comcatalog/product/view/...). Always end base URLs with /.
Pitfall 4 — URL rewrites not generated for new store views
When you add a Store View to an existing Store, Magento does not automatically generate URL rewrites for all existing products and categories in that new view. Run bin/magento indexer:reindex url_rewrite immediately after creation. On catalogs over 50,000 SKUs, run this inside a screen session.
Pitfall 5 — locking yourself out of admin
If you create a new website and accidentally overwrite the default scope base URL with the new domain before nginx/DNS is live, you lose admin access. Always set base URLs per website scope only — never touch the default scope URL unless that is the only website on the install.
Pitfall 6 — MAGE_RUN_CODE typo is silent
If MAGE_RUN_CODE does not match any existing website or store view code, Magento silently falls back to the default scope. You get the wrong storefront with no log entry. After wiring nginx, verify each domain loads the correct store by checking the X-Magento-Vary response header or adding a temporary log line during staging validation.
Verification checklist
Before marking a magento 2 multi store setup production-ready, confirm every item:
- Admin scope switcher — Stores → All Stores shows all Websites, Stores, and Store Views with correct codes.
- Base URLs — each website scope in Stores → Configuration → Web shows the correct domain, not the default.
- nginx test —
nginx -tpasses;curl -I https://uk.yourbrand.com/returns HTTP 200. - Correct locale — a category page on the second website shows the correct language and currency symbol.
- Checkout isolation — a test order on website A does not appear in the customer account on website B.
- URL rewrites — product and category URLs on the new store view resolve to HTTP 200, not 404.
- SSL — no mixed-content warnings;
web/secure/use_in_frontendis1per website. - Cache hit — after the first warm request, subsequent requests return
X-Magento-Cache-Debug: HITfor the correct store context.
Related reading
- Magento TTFB optimization from 1.8s to 180ms — performance baseline for a multi-store setup where each additional scope adds indexer and config-load overhead.
- Magento 2.4.9 / Mage-OS 2.3.0 upgrade guide — if you are upgrading an existing single-store install before adding multi-store, read this first.
- Magento extension development — custom modules that need to be scope-aware (different config per website) follow the same
ScopeConfigInterfacepattern documented here. - Magento development services
Setting up a multi-store Magento? The scope hierarchy, nginx wiring, and indexer sequence are straightforward when you know the order — but one wrong base URL or a missed reindex can cost hours of debugging on a live store. If you want it done right the first time, I am available for a fixed-scope setup engagement.
Get help from a certified Magento dev