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.

Where output gets lostApp logsprint, loggerPython buffernot flushedFile handlerinside containerDocker sees noneempty logsDocker only captures stdout and stderr, and only after Python flushes them.
  1. Set ENV PYTHONUNBUFFERED=1 in the Dockerfile (or run python -u).
  2. Configure Django LOGGING with a StreamHandler to the console, and remove file handlers.
  3. Tell Gunicorn to log to stdout: --access-logfile - --error-logfile -.
  4. Make sure the root or django logger level is INFO and handlers are attached.
  5. 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.

symptom and causeSymptomCause and fixoutput appears in burstsstdout buffered; PYTHONUNBUFFERED=1nothing from the app at allFileHandler in container;use StreamHandlerno request linesGunicorn access logoff; --access-logfile -errors missing, info showsdjango.requestswallowed; check LOGGINGEach symptom points at a different layerdjango-deployment.com
Check buffering first; it is the cheapest fix and the most common cause.

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.

every layer must pass the message ondocker logs / log driverreads stdout and stderrGunicorn or Celerymust not redirect to filesStreamHandlerwrites to the consoleLogger and levelINFO or lowerA message dropped at any layer never reaches Dockerdjango-deployment.com
Fix from the bottom up: logger, handler, process, then Docker.

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 INFO from Gunicorn and Django doubles lines. Keep Gunicorn's access log or a Django middleware log, not both.
  • Log rotation. Docker's default json-file driver 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

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.

Keep going