Use Docker Secrets With Django
Pass Django secrets to containers as files instead of environment variables: Docker Compose and Swarm secrets mounted at /run/secrets, the _FILE convention, reading them in settings, and keeping secrets out of images and docker inspect.
Problem statement
Environment variables are the usual way to give Django its SECRET_KEY and database password in containers, but they leak easily: docker inspect shows them, they are inherited by every child process, they appear in crash reports and debug pages, and they end up in shell history when set on the command line. Docker secrets deliver sensitive values as files mounted in memory at /run/secrets/<name>, visible only to the services that are granted them. Django needs a small helper to read those files.
- Define secrets in Compose (
secrets:withfile:orenvironment:sources) or create them in Swarm withdocker secret create. - Grant them per service:
secrets: [django_secret_key, db_password]. - In settings, read
SECRET_KEYfrom the file named bySECRET_KEY_FILE, falling back to the plain variable. - Point PostgreSQL's official image at the same file with
POSTGRES_PASSWORD_FILE. - Keep secret source files out of Git and out of the build context.
- Rotate in Swarm by creating a new secret version and updating the service.
Step-by-step solution
1. Define and grant secrets in Compose
services:
web:
image: ghcr.io/you/myapp:${IMAGE_TAG}
environment:
SECRET_KEY_FILE: /run/secrets/django_secret_key
DB_PASSWORD_FILE: /run/secrets/db_password
secrets: [django_secret_key, db_password]
db:
image: postgres:16
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets: [db_password]
secrets:
django_secret_key:
file: /etc/myapp/secrets/django_secret_key
db_password:
file: /etc/myapp/secrets/db_password
Keep the source files on the host with chmod 600, outside the project directory. In plain Compose (not Swarm), secrets are bind-mounted files rather than encrypted at rest, but they still stay out of docker inspect and the environment.
2. Read secret files in settings
# config/settings/secrets.py
import os
from pathlib import Path
from django.core.exceptions import ImproperlyConfigured
def secret(name: str, default=None):
"""Read NAME from a file named by NAME_FILE, else from NAME, else default."""
file_path = os.environ.get(f"{name}_FILE")
if file_path:
return Path(file_path).read_text().strip()
if name in os.environ:
return os.environ[name]
if default is not None:
return default
raise ImproperlyConfigured(f"Missing secret {name} (set {name} or {name}_FILE)")
from .secrets import secret
SECRET_KEY = secret("SECRET_KEY")
DATABASES = {"default": {
"ENGINE": "django.db.backends.postgresql",
"HOST": "db", "NAME": "app", "USER": "app",
"PASSWORD": secret("DB_PASSWORD"),
}}
.strip() removes the trailing newline that editors add to files, a frequent cause of "password authentication failed".
3. Swarm secrets
openssl rand -base64 48 | docker secret create django_secret_key_v1 -
docker service create --name web --secret source=django_secret_key_v1,target=django_secret_key ...
Swarm stores secrets encrypted in its Raft log and mounts them in a tmpfs only on nodes running granted tasks. Secrets are immutable; to rotate, create django_secret_key_v2 and update the service:
docker service update --secret-rm django_secret_key_v1 \
--secret-add source=django_secret_key_v2,target=django_secret_key web
For SECRET_KEY, keep the old value in SECRET_KEY_FALLBACKS during rotation so sessions survive.
4. Keep secrets out of images
Never COPY secret files into images or pass them as build arguments; both persist in image layers and history. For build-time secrets (private package indexes), use BuildKit secret mounts:
RUN --mount=type=secret,id=pip_conf,target=/etc/pip.conf pip install -r requirements.txt
Verification check: docker inspect <web-container> --format '{{json .Config.Env}}' shows SECRET_KEY_FILE but no secret value, docker compose exec web ls -l /run/secrets lists only the granted files, and the app passes check --deploy.
Explanation
Docker secrets separate sensitive values from container configuration. In Swarm mode, secrets are encrypted at rest and in transit and exposed to tasks through an in-memory filesystem; in Compose, they are mounted files with the same interface. Many official images (PostgreSQL, MySQL, Redis tooling) support the _FILE suffix convention, reading a variable's value from a file path. Applying the same convention in Django settings keeps local development (plain variables) and production (files) on one code path.
Edge cases / notes
- Kubernetes Secrets can also be mounted as files; the same
_FILEhelper works there. - Celery and other services need the same secrets granted explicitly; a missing grant shows up as
ImproperlyConfiguredon startup. - File permissions. In Compose, mounted files keep host ownership; non-root containers must be able to read them (use
uid/gid/modeoptions in Swarm or long syntax). - Debug pages never show settings values marked sensitive, but
DEBUGmust be off anyway in production.
Next steps
- Survey all approaches in Manage Django Secrets in Production.
- Swarm in depth: Deploy Django With Docker Swarm.
FAQ
What are Docker secrets?
Sensitive values delivered to containers as files under /run/secrets, granted per service, and encrypted at rest in Swarm.
How does Django read a Docker secret?
Read the file path from a NAME_FILE environment variable in settings, falling back to NAME.
Do Docker secrets work without Swarm?
Yes, Compose supports file-based secrets, though without Swarm's encryption at rest.
How do I rotate a Docker secret?
Create a new secret version and update the service to use it; secrets are immutable.