如何基于单个settings.py部署多域名多语言Django网站,实现单容器运行
我运行着一个多语言网站,涉及域名包括example.com、example.it、example.at、example.ch……example.fr。目前使用Django sites框架,每个域名对应一个Docker容器。按照Django官方文档的说明,为每个SITE_ID创建了单独的设置文件(导入全局设置避免重复),通过指定DJANGO_SETTINGS_MODULE来对应不同站点。
当前目录结构如下:
├── ... ├── urls.py ├── settings.py ├── sites │ ├── examplecom.py │ ├── exampleit.py │ ├── exampleat.py │ ├── examplech.py │ ├── ... │ └── examplefr.py ├── ... └── wsgi.py
自定义配置文件示例(以examplepl.py为例):
from myapp.settings import * SITE_ID = 19 LANGUAGE_CODE = SITEID_INFO[SITE_ID]['language'] COUNTRY_CODE = SITEID_INFO[SITE_ID]['country'] ALPHABET_CHARACTERS = SITEID_INFO[SITE_ID]['alphabet'] SITE_RELATED_COUNTRIES = ['PL'] DEFAULT_FROM_EMAIL = SITEID_INFO[SITE_ID]['default_from_email'] DEFAULT_FROM_EMAIL_NAME = SITEID_INFO[SITE_ID]['default_from_email_name'] SHORT_DATE_FORMAT = 'd.m.Y' SOME_TOKEN = 'foobar' AVAILABLE_ORDER_PACKAGES = [28, 5, 6, 29, 14, 15, 30, 23, 24 ] PAYMENT_CURRENCY_CODE = "PLN" PAYMENTOPTION['przelewy24'] = True ...
目前每个站点的语言、访问令牌、货币、支付方式等变量都是硬编码在各自的配置文件中,同时使用Wagtail(内容动态展示部分不在讨论范围内)。随着站点数量增加,每个域名对应一个Docker容器的维护成本极高,目标是用单个Docker容器承载所有站点,现提出两个技术问题:
- 如何实现基于域名的动态语言切换?是否存在类似Locale Middleware的机制?
- 能否通过中间件根据请求域名动态修改settings.py中的变量?
问题解答
1. 基于域名的动态语言切换
可以通过自定义中间件实现域名驱动的语言切换逻辑,步骤如下:
建立域名与语言的映射关系
在全局settings.py中维护字典,记录域名对应的语言代码:DOMAIN_LANG_MAP = { 'example.com': 'en', 'example.it': 'it', 'example.at': 'de', 'example.ch': 'de', 'example.fr': 'fr', # 补充其他域名映射 }编写自定义语言中间件
创建middleware.py,实现根据请求域名设置当前语言的逻辑:from django.utils import translation from django.conf import settings class DomainLocaleMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): # 提取域名(去除端口号) host = request.get_host().split(':')[0] # 获取对应语言,默认使用全局LANGUAGE_CODE lang_code = settings.DOMAIN_LANG_MAP.get(host, settings.LANGUAGE_CODE) # 激活当前语言 translation.activate(lang_code) request.LANGUAGE_CODE = translation.get_language() response = self.get_response(request) # 在响应头中添加语言标识 response['Content-Language'] = translation.get_language() translation.deactivate() return response替换Locale Middleware
在settings.py的MIDDLEWARE列表中,将django.middleware.locale.LocaleMiddleware替换为自定义的DomainLocaleMiddleware,注意中间件顺序:放在SessionMiddleware之后、CommonMiddleware之前,确保语言设置能正确作用于后续请求处理:MIDDLEWARE = [ # ...其他中间件 'myapp.middleware.DomainLocaleMiddleware', # ...其他中间件 ]
2. 通过中间件动态调整站点配置
Django的全局设置在启动时已加载缓存,直接修改settings模块的变量会导致多请求环境下的状态混乱,不推荐这么做。推荐两种安全可行的方案:
方案一:数据库存储站点配置
利用Django sites框架的Site模型,扩展字段存储每个站点的自定义配置:
- 创建
SiteConfig模型,关联Site:from django.db import models from django.contrib.sites.models import Site class SiteConfig(models.Model): site = models.OneToOneField(Site, on_delete=models.CASCADE) country_code = models.CharField(max_length=2) payment_currency = models.CharField(max_length=3) some_token = models.CharField(max_length=255) available_packages = models.JSONField() payment_options = models.JSONField() # 按需添加其他配置字段 - 编写中间件,根据请求域名获取对应
Site和SiteConfig,绑定到request对象:from django.contrib.sites.models import Site from myapp.models import SiteConfig class SiteConfigMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): host = request.get_host().split(':')[0] try: site = Site.objects.get(domain=host) site_config = SiteConfig.objects.get(site=site) request.site_config = site_config request.site_id = site.id except Site.DoesNotExist: # 域名未匹配时使用默认站点配置 site = Site.objects.get(id=settings.SITE_ID) request.site_config = SiteConfig.objects.get(site=site) request.site_id = settings.SITE_ID response = self.get_response(request) return response - 在视图或模板中通过
request.site_config调用配置:def checkout_view(request): currency = request.site_config.payment_currency # 业务逻辑处理
方案二:线程局部存储配置
若不想改动数据库,可使用线程局部变量存储当前站点配置,避免全局污染:
- 创建
site_settings.py定义线程局部存储:import threading _thread_locals = threading.local() def get_current_site_config(): return getattr(_thread_locals, 'site_config', None) def set_current_site_config(config): _thread_locals.site_config = config - 修改原站点配置文件,将变量封装到
CONFIG字典:from myapp.settings import * CONFIG = { 'SITE_ID': 19, 'LANGUAGE_CODE': SITEID_INFO[19]['language'], 'COUNTRY_CODE': SITEID_INFO[19]['country'], 'PAYMENT_CURRENCY_CODE': "PLN", # 其他配置项 } - 编写中间件,根据域名加载对应配置并设置到线程局部变量:
from myapp.sites.examplecom import CONFIG as COM_CONFIG from myapp.sites.exampleit import CONFIG as IT_CONFIG from myapp.site_settings import set_current_site_config DOMAIN_CONFIG_MAP = { 'example.com': COM_CONFIG, 'example.it': IT_CONFIG, # 其他域名映射 } class DynamicSiteConfigMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): host = request.get_host().split(':')[0] config = DOMAIN_CONFIG_MAP.get(host, DEFAULT_CONFIG) set_current_site_config(config) response = self.get_response(request) return response - 在视图中通过
get_current_site_config()获取配置:from myapp.site_settings import get_current_site_config def token_view(request): config = get_current_site_config() token = config['SOME_TOKEN'] # 业务逻辑处理
单容器部署注意事项
- Web服务器配置:确保Nginx/Apache将所有域名请求转发到同一个容器内的Django应用,虚拟主机配置统一指向容器端口。
- Django站点记录:提前在数据库中创建所有站点的
Site实例,确保域名与Site的domain字段完全匹配(注意是否带www前缀)。 - 资源管理:统一管理静态/媒体文件,若站点有专属资源,可按域名分目录存储,在视图中根据当前站点动态加载。
内容的提问来源于stack exchange,提问作者Philipp S.

