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.
- Build the path from
BASE_DIR:STATICFILES_DIRS = [BASE_DIR / "static"]. - Check
BASE_DIRis still the project root after splitting settings into a package. - If the folder is empty, git does not track it; add a
.gitkeepfile. - Check
.dockerignoreand your build steps do not exclude or delete the folder. - Keep
STATIC_ROOT(the output) separate fromSTATICFILES_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.
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.
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\\staticcopied 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
- If the output folder is the problem instead, see Fix STATIC_ROOT Not Set in Django.
- For other collectstatic failures, read Fix collectstatic Errors in Django.
- Confirm the whole setup with the Django Static Files Production Checklist.
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.