Fix Django staticfiles.W004: STATICFILES_DIRS Directory Does Not Exist

Fix the Django staticfiles.W004 warning that a STATICFILES_DIRS directory does not exist, and the related E002 error: build paths from BASE_DIR, commit empty directories, check .dockerignore, and keep STATIC_ROOT out of STATICFILES_DIRS.

Problem statement

On the server, in CI, or during a Docker build, Django prints:

WARNINGS:
?: (staticfiles.W004) The directory '/app/static' in the STATICFILES_DIRS setting
does not exist.

It works on your laptop. In production, collectstatic finishes but your own CSS, JavaScript, and images are missing, while the admin's files are fine. The warning tells you exactly why: Django was told to collect from a directory that is not there in this environment, so it silently skips it.

A related error stops startup entirely:

?: (staticfiles.E002) The STATICFILES_DIRS setting should not contain the
STATIC_ROOT setting.
Why the directory is missingSetting pointsto a source folderFolder absentin this environmentDjango skips itwarning W004Only app assetsyour files missingThe path is computed on the server, so check what exists there, not on your laptop.
  1. Build the path from BASE_DIR: STATICFILES_DIRS = [BASE_DIR / "static"].
  2. Check BASE_DIR is still the project root after splitting settings into a package.
  3. If the folder is empty, git does not track it; add a .gitkeep file.
  4. Check .dockerignore and your build steps do not exclude or delete the folder.
  5. Keep STATIC_ROOT (the output) separate from STATICFILES_DIRS (the inputs).

Step-by-step solution

1. Print the resolved path where it fails

Run this in the failing environment, such as inside the container:

python manage.py shell -c "from django.conf import settings; print(settings.BASE_DIR); print(settings.STATICFILES_DIRS)"
ls -la /app/static

Compare the printed path with what is actually on disk. Most cases fall into one of four patterns: the path is wrong, the folder was never committed, the build excluded it, or the folder is created by a frontend build that did not run.

2. Fix BASE_DIR after splitting settings

The default settings.py defines:

BASE_DIR = Path(__file__).resolve().parent.parent

When you move settings into config/settings/base.py, that line now points at config/, one level too deep. The project still runs locally if you also have a config/static folder by accident, but on a clean server the path does not exist. Add one more .parent:

# config/settings/base.py
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent.parent
STATICFILES_DIRS = [BASE_DIR / "static"]

Avoid relative strings such as "static" or "./static": they resolve against the current working directory, which differs between your shell, systemd, cron, and Docker.

match the causeWhat you findCause and fixpath ends in config/staticBASE_DIR one leveltoo deep; add .parentfolder missing after cloneempty dir not in git;commit a .gitkeeppresent in repo, absent in image.dockerignore or COPY excludes itfilled by npm run buildfrontend build skipped;run it before collectPrint the resolved path in the failing environment to see which appliesdjango-deployment.com
Each cause leaves a different trace in the resolved path or the file listing.

3. Commit or create the directory

Git does not store empty directories. If static/ only exists locally because your editor or a tool created it, a fresh clone will not have it. Either commit a placeholder:

touch static/.gitkeep
git add static/.gitkeep

or include the directory conditionally so a missing optional folder is not an error:

STATICFILES_DIRS = [p for p in [BASE_DIR / "static", BASE_DIR / "frontend" / "dist"] if p.exists()]

Use the conditional form only for genuinely optional folders; hiding a missing required folder just moves the problem to a broken page.

4. Check the Docker build

Look at .dockerignore for patterns such as static, */static, or dist that were meant to exclude collected output but also match your source folder. Check that the COPY instructions include it, and that multi-stage builds copy the frontend output into the final stage:

COPY --from=frontend /src/frontend/dist /app/frontend/dist
COPY . /app
RUN python manage.py collectstatic --noinput

5. Separate inputs from output

STATICFILES_DIRS lists where your source files live. STATIC_ROOT is where collectstatic writes everything. If they overlap, Django raises staticfiles.E002, because collecting would copy files into their own source. Use distinct folders:

STATICFILES_DIRS = [BASE_DIR / "static"]          # sources, committed
STATIC_ROOT = BASE_DIR / "staticfiles"            # output, not committed

Add staticfiles/ to .gitignore.

collectstatic inputs and outputSTATIC_ROOT (staticfiles/)output, served in productioncollectstaticcopies and hashes filesSTATICFILES_DIRS (static/)your project-wide sourcesapp/static/ foldersadmin and installed appsSources sit below collectstatic; the output sits above and is never a sourcedjango-deployment.com
Admin files come from app folders, which is why they appear even when your own folder is missing.

Explanation

The staticfiles finders search two places: each installed app's static/ folder (AppDirectoriesFinder) and every path in STATICFILES_DIRS (FileSystemFinder). The file system finder checks at startup that each path exists and warns rather than fails, so a missing folder is easy to miss. The result is a production site where the admin looks right, because its files come from the app finder, while your own assets return 404.

Edge cases / notes

  • Prefixed entries. STATICFILES_DIRS = [("vendor", BASE_DIR / "vendor")] is valid; the check applies to the path part.
  • Windows paths in settings. A hard-coded C:\\project\\static copied from a teammate's machine never exists on Linux servers.
  • Read-only images. Do not create the folder at runtime in the entrypoint; build it into the image so every replica is identical.

Next steps

FAQ

Is W004 only a warning?

Yes, Django keeps running. But every file in that folder is missing from collectstatic output, which usually breaks the site's styling.

Why are admin styles fine but mine are missing?

Admin files are found in the django.contrib.admin app folder. Your project files come only from STATICFILES_DIRS, which points at a missing folder.

Should STATIC_ROOT be inside the project?

It can be, but never inside a STATICFILES_DIRS folder, and it should not be committed to git.

Can I silence the warning?

You can add it to SILENCED_SYSTEM_CHECKS, but only do that when the folder is optional and absent on purpose.

Keep going