Keeping SECRET_KEY and API keys out of Django settings with Encore, django-environ, AWS Secrets Manager or Google Secret Manager, with local values and per-environment storage.
Django needs at least one secret, SECRET_KEY, and most projects add API keys for email, payments and storage. The default settings.py writes the key in plain text, and the usual fix moves it to environment variables, which then have to be stored and set for each environment.
This guide moves Django secrets out of the code with Encore, django-environ, AWS Secrets Manager and Google Secret Manager. Each version loads SECRET_KEY and a SendGrid API key.
What you need to decide:
SECRET_KEY without logging everyone out.Encore combines secrets declared in your Django code with per-environment storage, reading local values from a git-ignored file under encore run and keeping production values in AWS Secrets Manager or GCP Secret Manager in your own account. It is an infrastructure SDK added to the project, so apps, models and middleware stay as they are.

The secrets are declared in notes/resources.py next to the database. SECRET_KEY is shared across services, and the SendGrid key belongs to this service:
from encore import secrets
from encore.storage import postgres
db = postgres.Database("notes")
# Shared by every service; read once in settings.py.
django_secret_key = secrets.SharedSecret[str]("DjangoSecretKey")
# Private to this service.
sendgrid_api_key = secrets.Secret[str]("SendgridApiKey")
settings.py reads the key at startup and builds DATABASES from the declared database:
from psycopg.conninfo import conninfo_to_dict
from notes.resources import db, django_secret_key
SECRET_KEY = django_secret_key.value
DEBUG = False
connection = conninfo_to_dict(db.connection_string)
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": connection.pop("dbname"),
"USER": connection.pop("user", ""),
"PASSWORD": connection.pop("password", ""),
"HOST": connection.pop("host", ""),
"PORT": connection.pop("port", ""),
"OPTIONS": connection,
}
}
Views read the service secret where they use it. This one reports whether email is configured without revealing the key:
from django.http import HttpRequest, JsonResponse
from django.views.decorators.http import require_GET
from .resources import sendgrid_api_key
@require_GET
def mail_status(request: HttpRequest) -> JsonResponse:
# Read the value where it is used, so a rotated key is picked up.
key = sendgrid_api_key.value
return JsonResponse({"configured": key.startswith("SG.")})
Local values go in .secrets.user.toml, which stays out of git:
[shared]
DjangoSecretKey = "local-dev-only-key-0123456789abcdef0123456789abcdef"
[services.notes]
SendgridApiKey = "SG.local"
For production, generate a key with Python's secrets module and set it once per environment type:
On deploy, the values live in Secrets Manager on AWS or Secret Manager on GCP in your own account, and Encore provisions an IAM role for the service. Preview environments use their own values, so a pull request never runs with production keys.
When an agent adds a new integration, it declares the secret beside the code that uses it and sets a local value, and the reviewer sees the new secret in the diff.
Rotate SECRET_KEY by moving the old value into SECRET_KEY_FALLBACKS for a while, as the settings reference describes. Keep DEBUG = False outside development, because the debug page can print settings.
django-environ reads typed values from environment variables and a .env file:
import environ
env = environ.Env(DEBUG=(bool, False))
environ.Env.read_env(".env")
SECRET_KEY = env("DJANGO_SECRET_KEY")
SENDGRID_API_KEY = env("SENDGRID_API_KEY")
DATABASES = {"default": env.db("DATABASE_URL")}
The .env file stays out of git, and each developer keeps their own. In production, the platform has to set the variables, so the encrypted storage and who can read it depend on where you host.
On AWS, settings can fetch one JSON secret at startup with boto3:
import json
import boto3
secret = boto3.client("secretsmanager").get_secret_value(SecretId="notes/production")
values = json.loads(secret["SecretString"])
SECRET_KEY = values["DJANGO_SECRET_KEY"]
SENDGRID_API_KEY = values["SENDGRID_API_KEY"]
Settings now make a network call at import time, including for manage.py commands, so local development needs AWS credentials or a fallback. You create one secret per environment and grant the task role secretsmanager:GetSecretValue on it.
On GCP, Secret Manager values can be mounted into Cloud Run as environment variables, so settings only read os.environ:
printf %s "$KEY" | gcloud secrets create django-secret-key --data-file=-
printf %s "$SENDGRID_KEY" | gcloud secrets create sendgrid-api-key --data-file=-
for s in django-secret-key sendgrid-api-key; do
gcloud secrets add-iam-policy-binding "$s" \
--member="serviceAccount:notes@$PROJECT.iam.gserviceaccount.com" \
--role=roles/secretmanager.secretAccessor
done
gcloud run deploy notes --source . \
--service-account="notes@$PROJECT.iam.gserviceaccount.com" \
--set-secrets=DJANGO_SECRET_KEY=django-secret-key:latest,SENDGRID_API_KEY=sendgrid-api-key:latest
Each secret needs its own IAM binding for the service account, and the service account must exist before the deploy. Local development uses a separate mechanism such as a .env file.
Python support in Encore is in beta, and you can sign up for access to try it with your Django project.
Load it from a secret store at startup instead of writing it in the file. With Encore, declare django_secret_key = secrets.SharedSecret[str]("DjangoSecretKey") and set SECRET_KEY = django_secret_key.value in settings.py. Local values come from .secrets.user.toml and production values from encore secret set.
Use django.core.management.utils.get_random_secret_key(), or Python's secrets.token_urlsafe(50). Changing the key logs out users and invalidates password reset links, so plan rotation with SECRET_KEY_FALLBACKS.
Environment variables are a common delivery mechanism, but something still has to store the values encrypted and set them per environment. Libraries like django-environ read them; they do not store them.
Set the new value as SECRET_KEY and move the old one into SECRET_KEY_FALLBACKS. Django then accepts signatures made with either key until you remove the fallback.
Deploy to your cloud on AWS and Google Cloud
without managing infrastructure
Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.