Skip to main content

Управление доменами

Руководство по настройке и управлению доменами для маркетплейсов.

Основы доменов

Типы доменов

  1. Основной домен - главный домен платформы (example.com)
  2. Поддомен - домен маркетплейса (shop.example.com)
  3. Кастомный домен - собственный домен клиента (mystore.com)
  4. Wildcard домен - поддержка множественных поддоменов (*.example.com)

DNS настройка

# A запись для основного домена
example.com. IN A 192.168.1.100

# CNAME для поддомена
shop IN CNAME example.com.

# Wildcard запись
* IN CNAME example.com.

# MX записи для email
example.com. IN MX 10 mail.example.com.

# TXT записи для верификации
example.com. IN TXT "v=spf1 include:_spf.google.com ~all"
example.com. IN TXT "google-site-verification=abc123..."

Автоматическое создание поддоменов

Nginx конфигурация

# /etc/nginx/sites-available/marketplace-platform
server {
listen 80;
listen 443 ssl http2;
server_name ~^(?<subdomain>.+)\.example\.com$;

# SSL сертификат wildcard
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

# Логи для каждого поддомена
access_log /var/log/nginx/marketplace-$subdomain-access.log;
error_log /var/log/nginx/marketplace-$subdomain-error.log;

# Передача поддомена в приложение
location / {
proxy_pass http://marketplace-app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Subdomain $subdomain;
}

# Статические файлы
location /static/ {
alias /var/www/marketplace/static/;
expires 1y;
add_header Cache-Control "public, immutable";
}

# Редирект с HTTP на HTTPS
if ($scheme = http) {
return 301 https://$server_name$request_uri;
}
}

# Upstream для приложения
upstream marketplace-app {
server 127.0.0.1:3000;
server 127.0.0.1:3001 backup;
}

Express.js обработка поддоменов

const express = require('express');
const app = express();

// Middleware для определения маркетплейса по поддомену
app.use((req, res, next) => {
const subdomain = req.get('X-Subdomain') ||
req.hostname.split('.')[0];

// Исключаем системные поддомены
const systemSubdomains = ['www', 'api', 'admin', 'mail'];
if (systemSubdomains.includes(subdomain)) {
req.marketplace = null;
return next();
}

// Загружаем данные маркетплейса
req.marketplace = {
subdomain: subdomain,
domain: req.hostname
};

next();
});

// Middleware для загрузки настроек маркетплейса
app.use(async (req, res, next) => {
if (!req.marketplace) {
return next();
}

try {
const marketplace = await db.query(
'SELECT * FROM marketplaces WHERE subdomain = $1 OR custom_domain = $2',
[req.marketplace.subdomain, req.hostname]
);

if (marketplace.rows.length === 0) {
return res.status(404).render('marketplace-not-found');
}

req.marketplace = {
...req.marketplace,
...marketplace.rows[0]
};

// Проверка активности маркетплейса
if (!req.marketplace.is_active) {
return res.status(503).render('marketplace-inactive');
}

next();
} catch (error) {
console.error('Marketplace loading error:', error);
res.status(500).render('error');
}
});

// Роуты
app.get('/', (req, res) => {
if (!req.marketplace) {
return res.render('platform-homepage');
}

res.render('marketplace-homepage', {
marketplace: req.marketplace
});
});

Кастомные домены

Добавление кастомного домена

// API для добавления кастомного домена
app.post('/api/admin/marketplace/:id/domain', async (req, res) => {
try {
const { id } = req.params;
const { domain } = req.body;

// Валидация домена
if (!isValidDomain(domain)) {
return res.status(400).json({ error: 'Invalid domain format' });
}

// Проверка, что домен не используется
const existingDomain = await db.query(
'SELECT id FROM marketplaces WHERE custom_domain = $1',
[domain]
);

if (existingDomain.rows.length > 0) {
return res.status(409).json({ error: 'Domain already in use' });
}

// Проверка DNS настроек
const dnsValid = await verifyDNSSettings(domain);
if (!dnsValid) {
return res.status(400).json({
error: 'DNS settings incorrect',
instructions: getDNSInstructions(domain)
});
}

// Обновление маркетплейса
await db.query(
'UPDATE marketplaces SET custom_domain = $1, domain_verified = true WHERE id = $2',
[domain, id]
);

// Генерация SSL сертификата
await generateSSLCertificate(domain);

res.json({
success: true,
message: 'Custom domain added successfully'
});

} catch (error) {
console.error('Add domain error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});

// Функция валидации домена
function isValidDomain(domain) {
const domainRegex = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i;
return domainRegex.test(domain);
}

// Проверка DNS настроек
async function verifyDNSSettings(domain) {
const dns = require('dns').promises;

try {
const records = await dns.resolve4(domain);
const expectedIP = process.env.SERVER_IP;

return records.includes(expectedIP);
} catch (error) {
console.error('DNS verification error:', error);
return false;
}
}

// Инструкции по настройке DNS
function getDNSInstructions(domain) {
return {
type: 'A',
name: domain,
value: process.env.SERVER_IP,
ttl: 300,
instructions: [
`1. Войдите в панель управления DNS вашего домена ${domain}`,
`2. Создайте A-запись:`,
` - Имя: @ (или оставьте пустым для корневого домена)`,
` - Тип: A`,
` - Значение: ${process.env.SERVER_IP}`,
` - TTL: 300 (или минимальное значение)`,
`3. Дождитесь распространения DNS (до 24 часов)`,
`4. Повторите попытку добавления домена`
]
};
}

Автоматическое получение SSL сертификатов

const { exec } = require('child_process');
const fs = require('fs').promises;

// Генерация SSL сертификата через Let's Encrypt
async function generateSSLCertificate(domain) {
try {
// Проверяем, что домен доступен
const isAccessible = await checkDomainAccessibility(domain);
if (!isAccessible) {
throw new Error('Domain is not accessible');
}

// Генерируем сертификат
const certbotCommand = `certbot certonly --webroot -w /var/www/certbot -d ${domain} --email admin@example.com --agree-tos --non-interactive`;

await executeCommand(certbotCommand);

// Обновляем конфигурацию Nginx
await updateNginxConfig(domain);

// Перезагружаем Nginx
await executeCommand('nginx -s reload');

console.log(`SSL certificate generated for ${domain}`);

} catch (error) {
console.error(`SSL generation failed for ${domain}:`, error);
throw error;
}
}

// Проверка доступности домена
async function checkDomainAccessibility(domain) {
const http = require('http');

return new Promise((resolve) => {
const req = http.request({
hostname: domain,
port: 80,
path: '/.well-known/acme-challenge/test',
method: 'GET',
timeout: 5000
}, (res) => {
resolve(res.statusCode < 500);
});

req.on('error', () => resolve(false));
req.on('timeout', () => resolve(false));
req.end();
});
}

// Обновление конфигурации Nginx
async function updateNginxConfig(domain) {
const configTemplate = `
server {
listen 80;
server_name ${domain};
return 301 https://$server_name$request_uri;
}

server {
listen 443 ssl http2;
server_name ${domain};

ssl_certificate /etc/letsencrypt/live/${domain}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${domain}/privkey.pem;

# SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;

# Security headers
add_header Strict-Transport-Security "max-age=63072000" always;
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;

location / {
proxy_pass http://marketplace-app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

# Let's Encrypt challenge
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
}
`;

await fs.writeFile(`/etc/nginx/sites-available/${domain}`, configTemplate);
await executeCommand(`ln -sf /etc/nginx/sites-available/${domain} /etc/nginx/sites-enabled/`);
}

// Выполнение команды
function executeCommand(command) {
return new Promise((resolve, reject) => {
exec(command, (error, stdout, stderr) => {
if (error) {
reject(error);
} else {
resolve(stdout);
}
});
});
}

Мониторинг доменов

Проверка статуса доменов

// Cron job для проверки доменов
const cron = require('node-cron');
const https = require('https');

// Каждый час проверяем доступность доменов
cron.schedule('0 * * * *', async () => {
console.log('Checking domain status...');
await checkAllDomains();
});

async function checkAllDomains() {
try {
const domains = await db.query(`
SELECT id, custom_domain, subdomain, name
FROM marketplaces
WHERE is_active = true AND (custom_domain IS NOT NULL OR subdomain IS NOT NULL)
`);

for (const marketplace of domains.rows) {
const domain = marketplace.custom_domain || `${marketplace.subdomain}.example.com`;
const status = await checkDomainStatus(domain);

await db.query(
'UPDATE marketplaces SET domain_status = $1, last_checked = NOW() WHERE id = $2',
[status, marketplace.id]
);

// Уведомление при проблемах
if (status !== 'ok') {
await sendDomainAlert(marketplace, status);
}
}
} catch (error) {
console.error('Domain check error:', error);
}
}

// Проверка статуса домена
function checkDomainStatus(domain) {
return new Promise((resolve) => {
const req = https.request({
hostname: domain,
port: 443,
path: '/health',
method: 'GET',
timeout: 10000
}, (res) => {
if (res.statusCode === 200) {
resolve('ok');
} else {
resolve(`http_error_${res.statusCode}`);
}
});

req.on('error', (error) => {
if (error.code === 'ENOTFOUND') {
resolve('dns_error');
} else if (error.code === 'CERT_HAS_EXPIRED') {
resolve('ssl_expired');
} else {
resolve('connection_error');
}
});

req.on('timeout', () => {
resolve('timeout');
});

req.end();
});
}

// Уведомление о проблемах с доменом
async function sendDomainAlert(marketplace, status) {
const alertMessages = {
dns_error: 'DNS resolution failed',
ssl_expired: 'SSL certificate expired',
connection_error: 'Connection failed',
timeout: 'Request timeout',
http_error_500: 'Internal server error',
http_error_502: 'Bad gateway',
http_error_503: 'Service unavailable'
};

const message = alertMessages[status] || `Unknown error: ${status}`;

// Отправка email уведомления
await sendEmail({
to: marketplace.owner_email,
subject: `Domain Issue: ${marketplace.custom_domain || marketplace.subdomain}`,
body: `
There is an issue with your marketplace domain.

Marketplace: ${marketplace.name}
Domain: ${marketplace.custom_domain || marketplace.subdomain + '.example.com'}
Status: ${message}

Please check your domain configuration and contact support if needed.
`
});

// Логирование
console.error(`Domain alert for ${marketplace.name}: ${message}`);
}

SEO для множественных доменов

Canonical URLs

<!-- В head каждой страницы -->
<link rel="canonical" href="https://{{ marketplace.custom_domain || (marketplace.subdomain + '.example.com') }}{{ current_path }}">

Sitemap для каждого домена

// Генерация sitemap для маркетплейса
app.get('/sitemap.xml', async (req, res) => {
try {
if (!req.marketplace) {
return res.status(404).send('Marketplace not found');
}

const baseUrl = `https://${req.marketplace.custom_domain || req.hostname}`;

// Получаем страницы маркетплейса
const pages = await db.query(`
SELECT
'product' as type, slug, updated_at
FROM products
WHERE marketplace_id = $1 AND status = 'active'

UNION ALL

SELECT
'category' as type, slug, updated_at
FROM categories
WHERE marketplace_id = $1 AND status = 'active'

UNION ALL

SELECT
'page' as type, slug, updated_at
FROM pages
WHERE marketplace_id = $1 AND status = 'published'
`, [req.marketplace.id]);

// Генерируем XML
let sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>${baseUrl}/</loc>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>`;

pages.rows.forEach(page => {
const url = page.type === 'product' ? `/product/${page.slug}` :
page.type === 'category' ? `/category/${page.slug}` :
`/${page.slug}`;

sitemap += `
<url>
<loc>${baseUrl}${url}</loc>
<lastmod>${page.updated_at.toISOString()}</lastmod>
<changefreq>${page.type === 'product' ? 'weekly' : 'monthly'}</changefreq>
<priority>${page.type === 'product' ? '0.8' : '0.6'}</priority>
</url>`;
});

sitemap += '\n</urlset>';

res.set('Content-Type', 'application/xml');
res.send(sitemap);

} catch (error) {
console.error('Sitemap generation error:', error);
res.status(500).send('Error generating sitemap');
}
});

Резервное копирование конфигураций

Автоматическое резервное копирование

#!/bin/bash
# backup-nginx-configs.sh

BACKUP_DIR="/backups/nginx"
DATE=$(date +%Y%m%d_%H%M%S)
NGINX_DIR="/etc/nginx/sites-available"

# Создаем директорию для резервных копий
mkdir -p $BACKUP_DIR/$DATE

# Копируем все конфигурации
cp -r $NGINX_DIR/* $BACKUP_DIR/$DATE/

# Архивируем
cd $BACKUP_DIR
tar -czf nginx-configs-$DATE.tar.gz $DATE
rm -rf $DATE

# Удаляем старые резервные копии (старше 30 дней)
find $BACKUP_DIR -name "nginx-configs-*.tar.gz" -mtime +30 -delete

echo "Nginx configurations backed up to $BACKUP_DIR/nginx-configs-$DATE.tar.gz"

Миграция доменов

Скрипт миграции

// Скрипт для миграции с одного домена на другой
async function migrateDomain(marketplaceId, oldDomain, newDomain) {
try {
console.log(`Starting domain migration from ${oldDomain} to ${newDomain}`);

// 1. Проверяем новый домен
const newDomainValid = await verifyDNSSettings(newDomain);
if (!newDomainValid) {
throw new Error('New domain DNS settings are incorrect');
}

// 2. Генерируем SSL для нового домена
await generateSSLCertificate(newDomain);

// 3. Обновляем базу данных
await db.query(
'UPDATE marketplaces SET custom_domain = $1, old_domain = $2 WHERE id = $3',
[newDomain, oldDomain, marketplaceId]
);

// 4. Создаем редиректы со старого домена
await createDomainRedirects(oldDomain, newDomain);

// 5. Обновляем sitemap
await updateSitemap(marketplaceId, newDomain);

// 6. Уведомляем поисковые системы
await notifySearchEngines(marketplaceId, oldDomain, newDomain);

console.log(`Domain migration completed successfully`);

} catch (error) {
console.error('Domain migration failed:', error);
throw error;
}
}

// Создание редиректов
async function createDomainRedirects(oldDomain, newDomain) {
const redirectConfig = `
server {
listen 80;
listen 443 ssl http2;
server_name ${oldDomain};

ssl_certificate /etc/letsencrypt/live/${oldDomain}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${oldDomain}/privkey.pem;

return 301 https://${newDomain}$request_uri;
}
`;

await fs.writeFile(`/etc/nginx/sites-available/${oldDomain}-redirect`, redirectConfig);
await executeCommand(`ln -sf /etc/nginx/sites-available/${oldDomain}-redirect /etc/nginx/sites-enabled/`);
await executeCommand('nginx -s reload');
}