django-multisite2¶
Source code: https://github.com/erikvw/django-multisite2
With django-multisite2 a single instance of a Django project can serve multiple sites using a single settings file (multi-tenant). The current SITE_ID is extracted from the URL.
django-multisite2 provides the module multisite.
In settings, the static SITE_ID is replaced with multisite dynamic SiteID:
# settings.py
SITE_ID = SiteID(default=1)
the dynamic SiteID behaves like an integer. When combined with multisite middleware, SiteID will return the current SITE_ID based on the url. For example, each url below is an alias of the same server instance. With multisite you might have something like this:
# https://harare.example.com
>>> from django.conf import settings
>>> settings.SITE_ID
10
# https://kampala.example.com
>>> from django.conf import settings
>>> settings.SITE_ID
20
Python 3.12+ Django 5.2+. New releases are cut from the main branch.
Older versions of Django are supported by the original django-multisite project.
Installation¶
Install with pip:
pip install django-multisite2
Replace your SITE_ID in settings.py to:
from multisite import SiteID
SITE_ID = SiteID(default=1)
add to INSTALLED_APPS:
INSTALLED_APPS = [
...
'django.contrib.sites',
'multisite',
...
]
Edit settings.py MIDDLEWARE:
MIDDLEWARE = (
...
'multisite.middleware.DynamicSiteMiddleware',
...
)
On Django 6.0 and earlier, you have to silence the system check sites.E101:
SILENCED_SYSTEM_CHECKS = ["sites.E101"]
The Alias model¶
Alias is the lookup table that maps a hostname to a Site.
On each request DynamicSiteMiddleware takes the hostname from the Host header,
looks it up in Alias, and sets SITE_ID to the matching Alias.site_id. Django’s
Site.domain is not consulted for that lookup, so a Site is only reachable once it
has an Alias.
Canonical aliases are created for you¶
Every Site that has a domain gets exactly one canonical Alias, whose domain
mirrors Site.domain. You do not create these by hand. Multisite keeps them in step
through three hooks:
a
post_savesignal onSitecreates the canonicalAliasfor a new sitea
pre_savesignal onSiteupdates it whenSite.domainchangesthe
post_migratesignalpost_migrate_sync_aliasreconciles everySite, which catches sites created before multisite was installed, or created in ways that bypass signals such asloaddata,bulk_createor raw SQL
In the normal case, creating a Site is all you need:
>>> site = Site.objects.create(domain="example.com", name="Example")
>>> site.aliases.get(is_canonical=1)
<Alias: example.com -> example.com>
>>> site.domain = "example.org"
>>> site.save()
>>> site.aliases.get(is_canonical=1)
<Alias: example.org -> example.org>
Extra hostnames are what you add yourself¶
Any further Alias rows for the same Site are non-canonical: additional
hostnames that resolve to the same site. These are the ones you create:
Alias.objects.create(site=site, domain="www.example.org")
Alias.objects.create(site=site, domain="*.example.org")
A non-canonical alias defaults to redirect_to_canonical=True, so requests arriving on
it are redirected to the site’s canonical domain. Set it to False to serve the site on
that hostname without redirecting.
Alias.domain accepts wildcards. A hostname is matched from most to least specific, so
shop.example.org tries shop.example.org, then *.example.org, then *.org,
then *, each with and without the request’s port. An Alias with domain='*'
therefore catches everything.
Populating aliases yourself¶
Sites created in a data migration use historical models, which do not fire the signals above. Two helpers reconcile things, and both are idempotent:
from multisite.utils import (
create_or_sync_alias_from_site,
create_or_sync_canonical_from_all_sites,
)
create_or_sync_alias_from_site(site=site) # one site
create_or_sync_canonical_from_all_sites() # every site
Both accept an apps argument so they can be called from a data migration against
historical models:
def forwards(apps, schema_editor):
create_or_sync_canonical_from_all_sites(apps=apps)
If a Site has a blank domain, its canonical Alias is removed instead, since there
is no hostname to resolve.
Using a custom cache¶
Append to settings.py, in order to use a custom cache that can be safely cleared:
# The cache connection to use for multisite.
# Default: 'default'
CACHE_MULTISITE_ALIAS = 'multisite'
# The cache key prefix that multisite should use.
# If not set, defaults to the KEY_PREFIX used in the defined
# CACHE_MULTISITE_ALIAS or the default cache (empty string if not set)
CACHE_MULTISITE_KEY_PREFIX = ''
If you have set CACHE_MULTISITE_ALIAS to a custom value, e.g.
'multisite', add a separate backend to settings.py CACHES:
CACHES = {
'default': {
...
},
'multisite': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'TIMEOUT': 60 * 60 * 24, # 24 hours
...
},
}
Domain fallbacks¶
By default, if the domain name is unknown, multisite will respond with an HTTP 404 Not Found error. To change this behaviour, add to settings.py:
# The view function or class-based view that multisite will
# use when it cannot match the hostname with a Site. This can be
# the name of the function or the function itself.
# Default: None
MULTISITE_FALLBACK = 'django.views.generic.base.RedirectView
# Keyword arguments for the MULTISITE_FALLBACK view.
# Default: {}
MULTISITE_FALLBACK_KWARGS = {'url': 'http://example.com/',
'permanent': False}
Templates¶
This feature has been removed in version 2.0.0.
If required, create template subdirectories for domain level templates (in a location specified in settings.TEMPLATES[‘DIRS’].
Multisite’s template loader will look for templates in folders with the names of domains, such as:
templates/example.com
The template loader will also look for templates in a folder specified by the optional MULTISITE_DEFAULT_TEMPLATE_DIR setting, e.g.:
templates/multisite_templates
Post-migrate signal: post_migrate_sync_alias¶
The post-migrate signal post_migrate_sync_alias is registered in the apps.py. post_migrate_sync_alias
ensures the domain in multisite’s Alias model is updated to match that of django’s Site model. This signal must
run AFTER any post-migrate signals that manipulate Django’s Site model. If you have an app that manipulates Django’s
Site model, place it before multisite in settings. INSTALLED_APPS. If this is not possible, you may configure multisite
to not connect the post-migrate signal in apps.py so that you can do it somewhere else in your code.
To configure multisite to not connect the post-post_migrate_sync_alias in the apps.py, update your settings:
MULTISITE_REGISTER_POST_MIGRATE_SYNC_ALIAS = False
With the settings attribute set to False, it is your responsibility to connect the signal in your code. Note that if you do not sync the Alias and Site models after the Site model has changed, multisite may not recognize the domain and switch to the fallback view or raise a Http404 error.
Per-site time zones¶
DynamicSiteTimezoneMiddleware activates the current site’s time zone for the request
thread, so Django renders every datetime in local time for whichever site served the
request. It is the time zone equivalent of what SiteID does for SITE_ID.
Map each site to a time zone in settings.py. Values must be an IANA key:
MULTISITE_TIME_ZONES = {
1: "Africa/Dar_es_Salaam",
2: "America/New_York",
}
Add the middleware after DynamicSiteMiddleware
MIDDLEWARE = (
...
'multisite.middleware.DynamicSiteMiddleware',
'multisite.middleware.DynamicSiteTimezoneMiddleware',
...
)
Nothing else needs to change. django.utils.timezone.localtime(), template rendering,
form widgets and the admin all follow the activated time zone.
The lookup itself is available directly:
from multisite.utils import get_multisite_timezone
get_multisite_timezone(site_id=None) returns the time zone for site_id, or for the
current SITE_ID if not given. It requires DynamicSiteTimezoneMiddleware in
MIDDLEWARE and raises MultisiteTimezoneError otherwise, or if site_id has no
entry in MULTISITE_TIME_ZONES. The current site falls back to settings.TIME_ZONE.
Outside a request, in management commands, signal handlers or queue workers, no time zone
is activated and Django falls back to settings.TIME_ZONE. Wrap the entry point as you
would with SiteID.override():
from django.utils import timezone
with timezone.override(get_multisite_timezone()):
...
Three system checks cover the configuration:
multisite.E001ifDynamicSiteTimezoneMiddlewareis listed beforeDynamicSiteMiddlewaremultisite.E002if the middleware is installed butMULTISITE_TIME_ZONESis missing or emptymultisite.E003ifDynamicSiteMiddlewareis missing altogether
Development Environments¶
Multisite returns a valid Alias when in “development mode” (defaulting to the alias associated with the default SiteID.
- Development mode is either:
Running tests, i.e. manage.py test
Running locally in settings.DEBUG = True, where the hostname is a top-level name, i.e. localhost
In order to have multisite use aliases in local environments, add entries to your local etc/hosts file to match aliases in your applications. E.g.
127.0.0.1 example.com
127.0.0.1 examplealias.com
And access your application at example.com:8000 or examplealias.com:8000 instead of the usual localhost:8000.
Tests¶
To run the tests:
uv run runtests.py
or
uv run tox