Fix TemplateDoesNotExist in Django Production
Fix TemplateDoesNotExist that appears only in production: case-sensitive paths, TEMPLATES DIRS relative to the wrong directory, apps missing from INSTALLED_APPS, files excluded from images, and cached loaders.
Problem statement
A page that works locally returns 500 in production, and the logs show:
django.template.exceptions.TemplateDoesNotExist: orders/Invoice_detail.html
In development with DEBUG = True, Django shows a "Template-loader postmortem" listing every directory it searched. In production you only get the exception, so you have to reconstruct where Django looked and why the file was not there. The cause is almost always a difference between your laptop and the server: filesystem case sensitivity, the working directory, installed apps, or files that never made it into the deployed artifact.
- Ask Django directly in production:
python manage.py shell -c "from django.template.loader import get_template; get_template('orders/detail.html')". - Check letter case: Linux treats
Invoice_detail.htmlandinvoice_detail.htmlas different files. - Build
TEMPLATES[0]["DIRS"]fromBASE_DIR, never from relative paths. - Make sure the app containing the template is in
INSTALLED_APPSin production settings andAPP_DIRSisTrue(or the app loader is configured). - Confirm the file is in the deployed release or image (
.dockerignore,.gitignore, packaging rules). - Restart the application after deploying new templates.
Step-by-step solution
1. Reproduce the lookup on the server
cd /srv/myapp/current
set -a; . /etc/myapp.env; set +a
.venv/bin/python manage.py shell -c "
from django.template.loader import get_template
from django.template import TemplateDoesNotExist
try:
t = get_template('orders/Invoice_detail.html')
print('found:', t.origin.name)
except TemplateDoesNotExist as e:
for tried in e.tried:
print(tried[0].name, '->', tried[1])
"
e.tried lists every path Django attempted, with the reason each was skipped. It is the same information as the debug postmortem, available in production.
2. Check case sensitivity
macOS and Windows filesystems are usually case-insensitive; Linux is not. If the view renders orders/Invoice_detail.html and the file is orders/invoice_detail.html, it works on a Mac and fails on the server.
find . -path ./.venv -prune -o -iname "invoice_detail.html" -print
Rename the file or fix the reference so they match exactly. Git on a case-insensitive filesystem may not even record a case-only rename; use git mv -f old New and check git ls-files.
3. Build DIRS from BASE_DIR
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {"context_processors": [...]},
}
]
A relative entry like "DIRS": ["templates"] is resolved against the current working directory, which is the project root when you run runserver but may be / or another directory under systemd, cron, or a container. Also check that BASE_DIR is correct if settings live in a package (config/settings/production.py needs one more .parent than config/settings.py).
4. Check installed apps and loaders
Templates inside app/templates/ are found only if the app is in INSTALLED_APPS and APP_DIRS is True. Two production-specific traps:
- The app is added only in
settings/dev.py, so production does not load it. - Custom
OPTIONS["loaders"]are set, which disablesAPP_DIRS-based discovery unlessdjango.template.loaders.app_directories.Loaderis listed.
.venv/bin/python manage.py shell -c "from django.conf import settings; print('orders' in [a.split('.')[-1] for a in settings.INSTALLED_APPS]); print(settings.TEMPLATES)"
Also follow Django's namespacing convention: put templates in orders/templates/orders/detail.html and refer to them as orders/detail.html. Without the inner folder, templates with the same name in different apps shadow each other, and which one wins depends on app order.
5. Confirm the file was deployed
ls -l /srv/myapp/current/orders/templates/orders/
git ls-files | grep invoice_detail
docker run --rm ghcr.io/example/myapp:1.4.0 find /app -name "invoice_detail.html"
Common reasons a template is missing from the artifact:
- It was never committed, or a
.gitignorepattern (for example*.htmlin a generated-files rule) excludes it. .dockerignoreexcludes it; patterns such as**/templatesor an over-broad*.htmlrule are easy to add by accident.- The app is installed as a package, and package data rules (
MANIFEST.in,package-datainpyproject.toml) do not include templates.
6. Restart after deploying templates
With DEBUG = False, Django uses the cached template loader: each template is read and compiled once per process and kept in memory. New or changed templates are not visible to running workers. Restart Gunicorn (or deploy into a new release directory and restart) so workers load the current files.
7. Missing error templates
If the error mentions 404.html or 500.html, a custom handler or render() call references an error template that does not exist. Django's default handlers fall back to built-in responses when those templates are missing; a custom handler500 that renders a template explicitly does not.
Verification check:
.venv/bin/python manage.py shell -c "from django.template.loader import get_template; print(get_template('orders/invoice_detail.html').origin.name)"
curl -sI https://example.com/orders/42/invoice/ | head -1
The first command prints the absolute path of the template file; the page returns 200.
Explanation
Django's template engine asks each loader in turn for the template name. The filesystem loader checks each DIRS entry, and the app directories loader checks each installed app's templates/ folder, in INSTALLED_APPS order. TemplateDoesNotExist means every candidate path failed. In production, the candidates differ from development because the settings, working directory, filesystem, or deployed files differ, so reproducing the lookup on the server with e.tried shows exactly which assumption broke.
Edge cases / notes
Templates referenced from other templates
{% include %} and {% extends %} raise the same error for the referenced name. The traceback's template name tells you which reference failed.
Third-party apps
If a template belongs to a third-party package, the package may be missing or at a different version in production. Compare pip freeze between environments.
When to automate this
Add a test that renders every template (or at least every template referenced by views) with production settings, and run it in CI inside the built image. It catches case mismatches and packaging exclusions before deploy.
Next steps
- For other production-only behaviour, see Django Works with DEBUG=True but Fails with DEBUG=False.
- Find the traceback in the first place with Fix Django 500 Internal Server Error in Production.
- Review
.dockerignorerules with Build Smaller, Faster Django Docker Images. - If template changes do not appear after deploying, see Fix Django Code Changes Not Showing After Deploy.
FAQ
Why does TemplateDoesNotExist only happen in production?
Usually because of case-sensitive filenames on Linux, relative DIRS paths, an app missing from production INSTALLED_APPS, or the template not being included in the deployed files.
How can I see which paths Django searched without DEBUG?
Catch TemplateDoesNotExist in a shell on the server and print e.tried, which lists each attempted path and the reason it failed.
Do I need to restart Django after changing templates?
Yes, in production. The cached template loader keeps compiled templates in memory per process.
Where should app templates live?
In app/templates/app/name.html, referenced as app/name.html, so names from different apps do not collide.