Troubleshooting
Settings & secrets

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.

How Django finds a templateTemplate nameorders/detail.htmlDIRSproject foldersAPP_DIRSapp/templates/Found or raiseTemplateDoesNotExistDjango tries DIRS first, then each installed app's templates directory, in order.
  1. Ask Django directly in production: python manage.py shell -c "from django.template.loader import get_template; get_template('orders/detail.html')".
  2. Check letter case: Linux treats Invoice_detail.html and invoice_detail.html as different files.
  3. Build TEMPLATES[0]["DIRS"] from BASE_DIR, never from relative paths.
  4. Make sure the app containing the template is in INSTALLED_APPS in production settings and APP_DIRS is True (or the app loader is configured).
  5. Confirm the file is in the deployed release or image (.dockerignore, .gitignore, packaging rules).
  6. 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])
"
the postmortem lists every path triedTemplateDoesNotExist: shop/Product_detail.htmlfilesystem.Loader: /srv/app/templates/shop/Product_detail.htmlapp_directories.Loader: /srv/app/shop/templates/shop/Product_detail.html1231Capital P: Linux paths are case-sensitive.2Paths from TEMPLATES DIRS.3Each installed app’s templates folder.If the right folder is not listed, the loader config is the problemdjango-deployment.com
get_template() in manage.py shell prints the same postmortem.

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.

works locally, fails on the servermacOS or Windows laptopProduct_detail.html findsproduct_detail.html✓tests pass✕hides the mismatchLinux server or containerexact name required✕TemplateDoesNotExist✓fix: match the file name exactlyRunning tests in a Linux container catches this before deploydjango-deployment.com
git mv is needed to rename a file only by case on those laptops.
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 disables APP_DIRS-based discovery unless django.template.loaders.app_directories.Loader is listed.
where Django looks, in orderTEMPLATES DIRSfilesystem loader, firstInstalled apps, in orderapp/templates/ when APP_DIRS isTrueCached loaderwraps both when DEBUG is offAn app missing from INSTALLED_APPS is never searcheddjango-deployment.com
The cached loader is why new templates need a restart to appear.
.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 .gitignore pattern (for example *.html in a generated-files rule) excludes it.
  • .dockerignore excludes it; patterns such as **/templates or an over-broad *.html rule are easy to add by accident.
  • The app is installed as a package, and package data rules (MANIFEST.in, package-data in pyproject.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

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.

Keep going