Open-source project
jazzband/django-hosts avatar
jazzband/django-hosts

django-hosts: mapping hostnames to URLconfs so one Django site can serve many domains

Dynamic and static host resolving for Django. Maps hostnames to URLconfs.

1,062 stars107 forksPythonNOASSERTION

At a glance

What is it?
django-hosts is a Jazzband project that lets a single Django installation route requests for api.example.com and beta.example.com to different URLconf modules, with the mapping expressed as regular expressions evaluated in order. It ships as a reusable app with two pieces of middleware, and the pattern matching is anchored to the extreme left of the requested host, which is where most of its surprises live.
Who is it for?
django-hosts fits a Django project that needs to serve several subdomains from one installation with genuinely different URL configurations, such as an API host and a site host, and that is comfortable with hostname driven routing in production.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 25, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What the package actually does: one site, several URLconfs

django-hosts is a Django app that routes requests for specific hosts to different URL schemes defined in modules called hostconfs. The example in the documentation is the clearest statement of the purpose.

If you own example.com but want to serve specific content at api.example.com and beta.example.com, you add this to a hosts.py file:

python
from django_hosts import patterns, host

host_patterns = patterns(
    "path.to",
    host(r"api", "api.urls", name="api"),
    host(r"beta", "beta.urls", name="beta"),
)

The first argument to patterns is a plain path prefix, and each host entry is a regular expression, a URLconf module and a name. The result is that requests to api.example.com and beta.example.com are routed to their corresponding URLconf, and the documentation notes that you can use your urls.py as a template for these hostconfs.

The evaluation order is defined and the fallback is defined. Patterns are evaluated in order, and if no pattern matches, the request is processed in the usual way, using the standard ROOT_URLCONF. That fallback is what makes the package safe to adopt incrementally, since an unmatched host behaves exactly as it did before you installed anything.

The name argument is not decoration. The DEFAULT_HOST setting is the name of the host pattern you want to refer to as the default pattern, and it is used if no other pattern matches or if you do not give a name to the host_url template tag. So the named patterns are addressable from templates, which is how a relative link in a template on the api host can point at the beta host.

The project is hosted under the jazzband organisation, which is a collective of Django package maintainers, and the package is published on PyPI as django-hosts with the documentation at django-hosts.readthedocs.io. The author is Jannis Leidel. The licence is recorded as a custom one that GitHub could not classify, with a LICENSE file at the repository root, while setup.py declares the licence as BSD and the classifiers include the OSI Approved BSD License marker.

The three configuration steps, and where the middleware has to sit

Configuration is a short ordered list, and two of the steps are about position in a list, which is where mistakes happen.

First, add django_hosts to your INSTALLED_APPS setting.

Second, add django_hosts.middleware.HostsRequestMiddleware to the beginning of your MIDDLEWARE setting. The emphasis is in the documentation and it is the important one. This middleware is what reads the Host header and resolves it to a hostconf, and if anything runs before it, that thing sees a request whose URLconf has not been determined yet.

Third, add django_hosts.middleware.HostsResponseMiddleware to the end of your MIDDLEWARE setting. This one runs last, and its job is on the response side rather than the request side, which is why the two ends differ. A response middleware placed early would run before the view has produced anything to work with.

Then you create a new module containing your default host patterns, for example a hosts.py file next to your urls.py, and set the ROOT_HOSTCONF setting to the dotted Python import path of that module:

python
ROOT_HOSTCONF = "mysite.hosts"

And finally the DEFAULT_HOST setting, set to the name of the host pattern you want as the default.

So the whole configuration is: an app entry, two middleware positions, a hosts module, and two settings. That is a small surface, and the installation section says the package installs with your favourite package manager, for example pip install django-hosts, or alternatively from the repository on GitHub.

The ordering constraint is the part to internalise rather than copy. The package is not a wrapper around Django's URL resolution, it is a middleware that rewrites the request so that the standard resolver then does its job against the right URLconf. The two middlewares bracket the request, and everything Django does in between is unchanged.

Regular expressions matched against the extreme left of the host

The matching semantics are documented in a note, and they are the part of this package most likely to surprise you.

The first point is that patterns are matched against the extreme left of the requested host. Not the whole host, and not as a suffix search. The second point is that it is implied that all patterns end either with a literal full stop, that is a dot, or with an end of line metacharacter. The third is that as with all regular expressions, various metacharacters need quoting.

The example shows what the first point buys you. This ROOT_HOSTCONF setting routes foo.example.com and bar.example.com to the same URLconf:

python
from django_hosts import patterns, host

host_patterns = patterns(
    "",
    host(r"(foo|bar)", "path.to.urls", name="foo-or-bar"),
)

So the regex is a prefix match on the hostname, anchored at the start, and the implicit trailing dot or end of line means it must consume the rest of the host. Writing host(r"api") means the api label and then the dot, which is why api.example.com matches and not apifoo.example.com.

The empty first argument to patterns in that example is the path prefix, so paths on those hosts are passed through unchanged.

The regex form is what gives the package its power. A pattern like host(r"api", ...) looks like a simple string, but because it is a regular expression you can express alternatives, as the foo or bar example does, and you can express a wildcard subdomain with a pattern like r".*" if you want a catch-all host. The cost is that you now have to think about regex escaping, and the documentation's third point about quoting metacharacters is a warning about exactly that.

Because the patterns are evaluated in order, the order of your host entries is the resolution order, and a broad pattern placed early will shadow a narrow one placed later. That is ordinary regex behaviour but it is a place where the ordering rule and the anchoring rule interact, and it is worth writing your patterns out and testing them against the hostnames you actually expect before you deploy.

Packaging, supported Django versions and the test setup

The setup.py file gives the compatibility picture, and it is current.

The package requires Python 3.10 or newer, and the classifiers list Django 4.2, 5.1 and 5.2, and Python 3.10 through 3.13. The development status is 5, Production/Stable, and the intended audience is Developers. The packages are found with find_packages excluding the tests directory.

The version is not in setup.py at all. The setup call uses use_scm_version=True with setup_requires of setuptools_scm, which means the version is derived from git tags. That is the standard modern approach and it has one practical consequence for a downstream packager: building from a source tree without the git metadata does not give you a sensible version, so a distribution that vendors this package needs either a git checkout or a pinned sdist.

The Makefile shows the development workflow, and it is small. The test target runs pytest. The docs target creates a virtualenv at /tmp/django-hosts-env, installs the documentation requirements from docs/requirements.txt, installs the package in editable mode and builds the HTML docs with make -C docs html. The clean target runs the docs clean and removes the virtualenv. The VENV variable is exported onto the PATH, which is what makes the local pytest and the locally installed sphinx take precedence.

The repository listing is a conventional Django package layout: the django_hosts/ package, a tests/ directory, a docs/ directory, setup.py and setup.cfg, MANIFEST.in, tox.ini for the multi environment test matrix, a .pre-commit-config.yaml, a .coveragerc for coverage configuration and a .readthedocs.yml for the documentation build. The badge set in the README points at the PyPI version, the supported Python versions, the supported Django versions, the GitHub Actions test workflow, codecov coverage, the readthedocs build and Jazzband itself.

The presence of a tox.ini alongside a plain pytest invocation is worth a note. Running pytest directly is the fast inner loop, and tox is what covers the matrix of Django and Python versions, and the two are complementary rather than duplicated.

The one thing to check: what your web server passes as the Host header

The mechanism of this package is entirely dependent on one piece of information in the request, and getting it wrong produces a confusing result rather than an error.

The routing decision is made from the requested host. That host comes from the HTTP Host header, and if your deployment sits behind a reverse proxy or a load balancer, the value that reaches Django is whatever the proxy forwards. In the common setup, the proxy passes the original Host header through unchanged and everything works. In the setup where the proxy rewrites the host to the backend's own name, every request looks like it came to the same hostname, and the hostconf resolution silently picks the same URLconf for everything.

The symptom in that case is not a 500. It is a site where api.example.com serves the site's URLs and the api URLs 404, which is exactly the kind of thing that gets debugged in the wrong place for an afternoon. If your host resolution behaves oddly, log the host value the middleware sees before you start rewriting your URLconfs.

Django's own ALLOWED_HOSTS check still applies on top of this. The package does not replace it, and the hostconf resolution does not exempt any hostname from it. If you are serving api.example.com, that host has to be acceptable to Django's own validation, and forgetting that produces a DisallowedHost error rather than a routing surprise.

There is a second thing to be aware of, which is that the same Django installation is now serving multiple URLconfs, and each URLconf is a separate namespace of URL names. If two hostconfs both define a URL with the same name, the reverse lookup by name is ambiguous, and the host_url template tag exists precisely so you can address a URL in a specific hostconf. That is why DEFAULT_HOST and the name argument on each host pattern exist, and it is a design decision to spend a moment on if your site has several hosts with overlapping URL names.

For a multi-tenant site where each tenant gets its own subdomain and its own URLconf, the same reasoning applies at scale. The pattern mechanism handles the resolution; the question of how many hostconfs you want to maintain is a different problem, and the answer to that is usually one hostconf with tenant-aware views rather than one hostconf per tenant.

Editorial conclusion

django-hosts fits a Django project that needs to serve several subdomains from one installation with genuinely different URL configurations, such as an API host and a site host, and that is comfortable with hostname driven routing in production. It is the wrong choice if you only need to redirect one domain to another, where Django's own redirect mechanism or a reverse proxy is simpler, and it is the wrong choice if you cannot control the Host header your application sees, because the whole package keys off that header. Before adopting, write out the host patterns as regular expressions and confirm each one anchors as you expect, since the matching is against the extreme left of the host, and remember that Django's ALLOWED_HOSTS still applies on top of the routing.

Frequently asked questions

What does django-hosts do for a Django project?

It routes requests for specific hosts to different URLconf modules called hostconfs. If you own example.com and want specific content at api.example.com and beta.example.com, you declare host patterns in a hosts.py module and requests to those hosts reach their own URLconf, with unmatched hosts falling back to the standard ROOT_URLCONF.

How do I install and configure django-hosts?

Run pip install django-hosts, add django_hosts to INSTALLED_APPS, add django_hosts.middleware.HostsRequestMiddleware to the beginning of MIDDLEWARE and django_hosts.middleware.HostsResponseMiddleware to the end, create a hosts.py module next to your urls.py, and set ROOT_HOSTCONF to its dotted import path plus DEFAULT_HOST to the name of the default pattern.

How does the host matching work in django-hosts?

Patterns are regular expressions matched against the extreme left of the requested host, and it is implied that all patterns end with either a literal full stop or an end of line metacharacter. Patterns are evaluated in order, so a broad pattern placed early shadows a narrow one placed later.

Which Django and Python versions does django-hosts support?

The package requires Python 3.10 or newer, and the classifiers list Django 4.2, 5.1 and 5.2 with Python 3.10 through 3.13. The development status is Production/Stable, and the version is derived from git tags through setuptools_scm rather than being written into setup.py.

Official sources

  1. Issues
  2. jazzband/django-hosts on GitHub
  3. Project website
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/jazzband-django-hosts.svg)](https://hysenlabs.com/projects/jazzband-django-hosts)

Was this useful?