Multi-domain website setup with country-specific domains

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Showing 1 of 1 servicesAll 2065 services
Multi-domain website setup with country-specific domains
Complex
~5 business days
FAQ

Our competencies:

Development stages

Latest works

  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1171
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    831
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    879
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    453

Setting Up a Multi-Domain Website (Different Domains by Country)

When business operates in multiple countries, each typically gets its own domain: company.ru, company.kz, company.by, company.ua. One code engine serves all domains — different languages, prices, legal texts, phones. More complex than subdirectories but maximum local SEO signal and complete content isolation.

Domain Configuration

Central domains table links host to settings:

CREATE TABLE site_domains (
    id           SERIAL PRIMARY KEY,
    host         VARCHAR(253) UNIQUE NOT NULL, -- 'company.ru'
    country_code CHAR(2) NOT NULL,             -- 'RU', 'KZ', 'BY'
    locale       VARCHAR(10) NOT NULL,          -- 'ru', 'kk', 'be'
    currency     CHAR(3) NOT NULL,              -- 'RUB', 'KZT', 'BYN'
    timezone     VARCHAR(64) NOT NULL,
    is_primary   BOOLEAN DEFAULT false,
    is_active    BOOLEAN DEFAULT true,
    meta         JSONB DEFAULT '{}'
);

INSERT INTO site_domains VALUES
  (DEFAULT, 'company.ru',  'RU', 'ru', 'RUB', 'Europe/Moscow',    true,  true, '{}'),
  (DEFAULT, 'company.kz',  'KZ', 'kk', 'KZT', 'Asia/Almaty',      false, true, '{}'),
  (DEFAULT, 'company.by',  'BY', 'ru', 'BYN', 'Europe/Minsk',     false, true, '{}');

Domain Detection Middleware

// app/Http/Middleware/ResolveSiteDomain.php
class ResolveSiteDomain
{
    public function handle(Request $request, Closure $next): Response
    {
        $host = $request->getHost(); // 'company.kz'

        $domain = SiteDomain::where('host', $host)
            ->where('is_active', true)
            ->first();

        if (!$domain) {
            // Unknown domain — redirect to primary
            $primary = SiteDomain::where('is_primary', true)->firstOrFail();
            return redirect("https://{$primary->host}" . $request->getRequestUri(), 301);
        }

        app()->instance('site.domain', $domain);

        // Set locale and timezone
        App::setLocale($domain->locale);
        Carbon::setlocale($domain->locale);
        date_default_timezone_set($domain->timezone);

        return $next($request);
    }
}

Multi-Tenant Laravel Configuration

// app/Providers/DomainServiceProvider.php
public function boot(): void
{
    $this->app->resolving('current.domain', function () {
        return app('site.domain');
    });

    // Override mail from for each domain
    $this->app['events']->listen(MessageSending::class, function ($event) {
        $domain = app('site.domain');
        config([
            'mail.from.address' => "noreply@{$domain->host}",
            'mail.from.name'    => config('app.name') . ' ' . strtoupper($domain->country_code),
        ]);
    });
}

Content Storage by Domain

-- Translatable texts bound to domain
CREATE TABLE pages (
    id         SERIAL PRIMARY KEY,
    slug       VARCHAR(255) NOT NULL,
    domain_id  INTEGER REFERENCES site_domains(id),
    -- NULL in domain_id = shared content for all domains
    UNIQUE (slug, domain_id)
);

CREATE TABLE page_translations (
    page_id    INTEGER REFERENCES pages(id),
    locale     VARCHAR(10) NOT NULL,
    title      TEXT,
    body       TEXT,
    meta_title TEXT,
    meta_desc  TEXT,
    PRIMARY KEY (page_id, locale)
);

Fetch content with fallback to shared:

public function getPage(string $slug): Page
{
    $domain = app('site.domain');

    // Try domain-specific page first
    $page = Page::where('slug', $slug)
        ->where('domain_id', $domain->id)
        ->first();

    // Fallback to shared page
    $page ??= Page::where('slug', $slug)
        ->whereNull('domain_id')
        ->firstOrFail();

    return $page;
}

Nginx: Virtual Hosts for PHP-FPM

# /etc/nginx/sites-available/multisite.conf
server {
    listen 443 ssl http2;
    server_name company.ru company.kz company.by;

    ssl_certificate     /etc/letsencrypt/live/company.ru/fullchain.pem;
    # Wildcard cert or multi-domain SAN

    root /var/www/company/public;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        fastcgi_param HTTP_HOST $host; # Pass real host to PHP
        include fastcgi_params;
    }
}

If domains on different servers — add load balancer with X-Forwarded-Host header, read via TrustProxies middleware.

SSL Certificates

Certbot with multi-domain SAN:

certbot certonly --nginx \
  -d company.ru -d www.company.ru \
  -d company.kz -d www.company.kz \
  -d company.by -d www.company.by \
  --email [email protected] \
  --agree-tos

Alternative — wildcard *.company.ru + DNS challenge.

Crossdomain SEO

Each domain is separate site for Google. Link them with hreflang in <head>:

<!-- On company.ru -->
<link rel="alternate" hreflang="ru-RU" href="https://company.ru/products/item-1" />
<link rel="alternate" hreflang="ru-KZ" href="https://company.kz/products/item-1" />
<link rel="alternate" hreflang="ru-BY" href="https://company.by/products/item-1" />

Generate via helper:

function hreflangTags(string $path): string
{
    $domains = SiteDomain::where('is_active', true)->get();
    return $domains->map(fn($d) =>
        "<link rel=\"alternate\" hreflang=\"{$d->locale}-{$d->country_code}\" href=\"https://{$d->host}{$path}\" />"
    )->join("\n");
}

Cookie and Session Problem

Cookies don't transmit between different domains. Solutions:

SSO via shared token: click "Go to Kazakhstan site" generates one-time token, user redirected with it, second domain exchanges token for session.

Shared session storage: Redis with same SESSION_DOMAIN — but browser won't send cookie from .ru to .kz. Works only for subdomains.

Adding New Domain Timeline

Adding domain takes less than hour:

  1. Register domain, setup DNS A records — minutes to day (propagation)
  2. Add to site_domains — 2 minutes
  3. Add to SAN certificate (certbot --expand) — 5 minutes
  4. Add server_name to Nginx — 1 minute
  5. Import or create content — depends on volume

Monitoring

Separate health check /health per domain. Uptime Robot or Checkly pings all domains minute, alert on unavailability. Check SSL expiry separately — via ssl_certificate_expire metric in Prometheus or third-party service.