Fix Django Logs and print() Output Not Showing in docker logs
Fix Django, Gunicorn, and Celery output missing from docker logs or appearing late: set PYTHONUNBUFFERED, log to stdout instead of files, configure Gunicorn access and error logs, and keep LOGGING from silencing django.request errors.
Problem statement
docker compose logs -f web shows Gunicorn's startup lines and then nothing. Requests clearly reach the app, print() calls and logger.info() messages exist in the code, and errors happen, but none of it appears. Sometimes everything shows up at once minutes later, or only when the container stops.
Docker collects whatever a container writes to its standard output and standard error. Output goes missing for three reasons: Python buffers it instead of writing it immediately, the app writes to files inside the container instead of the console, or Django's logging configuration drops the messages before they are written anywhere.
- Set
ENV PYTHONUNBUFFERED=1in the Dockerfile (or runpython -u). - Configure Django
LOGGINGwith aStreamHandlerto the console, and remove file handlers. - Tell Gunicorn to log to stdout:
--access-logfile - --error-logfile -. - Make sure the root or
djangologger level isINFOand handlers are attached. - For Celery, avoid
--logfile; the default writes to stderr.
Step-by-step solution
1. Turn off output buffering
When stdout is not a terminal, Python buffers it in blocks of several kilobytes. In a container, output sits in the buffer until it fills or the process exits:
ENV PYTHONUNBUFFERED=1
This makes print() and stream handlers write immediately. Logging's StreamHandler flushes after each record anyway, but print() and many third-party libraries do not.
2. Log to the console, not to files
A FileHandler writing to /app/logs/django.log inside the container hides everything from Docker. Replace it:
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"plain": {"format": "%(asctime)s %(levelname)s %(name)s %(message)s"},
},
"handlers": {
"console": {"class": "logging.StreamHandler", "formatter": "plain"},
},
"root": {"handlers": ["console"], "level": "INFO"},
"loggers": {
"django": {"handlers": ["console"], "level": "INFO", "propagate": False},
},
}
disable_existing_loggers: False matters: with True, loggers created before configuration, including some in django, go silent.
3. Configure Gunicorn's logs
Gunicorn's error log goes to stderr by default, but the access log is off unless you enable it:
gunicorn config.wsgi:application --bind 0.0.0.0:8000 \
--access-logfile - --error-logfile - --capture-output
The - means stdout or stderr. --capture-output redirects workers' stdout and stderr into the error log, which catches stray print() output too. The same settings work in gunicorn.conf.py as accesslog = "-" and errorlog = "-".
4. Make sure errors are not swallowed
Unhandled exceptions are logged by the django.request logger. If your LOGGING defines django with a higher level, a handler that filters with require_debug_true, or only mail_admins, errors vanish in production. Test it:
docker compose exec web python -c "
import django, logging; django.setup()
logging.getLogger('django.request').error('test error from container')"
docker compose logs --tail 5 web
The test line should appear.
5. Celery and other processes
Celery logs to stderr unless you pass --logfile. Keep it that way in containers:
celery -A config worker -l info
For management commands in cron containers, write output to stdout, and in Kubernetes CronJobs it appears with kubectl logs.
Explanation
Docker's logging drivers attach to the container's stdout and stderr file descriptors. Anything written to a file inside the container's filesystem is invisible to them. Python adds a buffer between print() and the file descriptor when output is not a terminal, which is why output arrives late or only at exit. Django adds another layer: its LOGGING dictionary decides which loggers emit records and which handlers receive them, and a configuration designed for files or email in production can easily send nothing to the console.
Edge cases / notes
- Log volume. Logging every request at
INFOfrom Gunicorn and Django doubles lines. Keep Gunicorn's access log or a Django middleware log, not both. - Log rotation. Docker's default
json-filedriver needs size limits, or console logging fills the disk; see Fix Docker Logs Filling the Disk. - JSON logs. For log platforms, emit JSON on stdout; see Django Structured JSON Logging.
Next steps
- For logging problems outside Docker, see Fix Django Logging Not Working in Production.
- Make logs machine-readable with Django Structured JSON Logging.
- If the container exits before logging anything, read Fix Django Docker Container Exits Immediately.
FAQ
What does PYTHONUNBUFFERED do?
It disables buffering of stdout and stderr, so output is written immediately instead of when a buffer fills.
Why do logs appear when the container stops?
The process flushes its buffers on exit. Before that, output was waiting in memory.
Should I keep log files inside the container?
No. They are lost when the container is replaced and invisible to Docker. Log to stdout and let the logging driver or a shipper store them.
Does DEBUG = False affect logging?
Indirectly. Django's default configuration sends errors to the console only when DEBUG = True. With your own LOGGING, you control it explicitly.