Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 59

Environment Variables for Secure Configuration

Recommended resources

Hello again! Let's continue our journey into deploying Django applications.

In our last lesson, we refactored our project's settings into a more professional structure, separating configurations for local and production environments. We also had our first look at using a .env file to keep our SECRET_KEY and DEBUG flag out of our code.

Today, we will build directly on that foundation. Our goal is to master the use of environment variables for all types of configuration, especially sensitive data. This is a non-negotiable skill for modern web developers. We'll explore why this practice is so critical and learn how to manage database credentials and third-party API keys securely and efficiently.

1. The "Why": Configuration as a Guiding Principle

Storing configuration in environment variables isn't just a Django trick; it's a core principle for building robust, scalable, and secure web applications. This principle is famously articulated in the 12-Factor App methodology, a set of best practices for building software-as-a-service applications.

Configuring Django Settings: Best Practices

To understand the theory behind our practice, let's read about the third 'factor' from the article 'Configuring Django Settings: Best Practices' by Djangostars.

Please read the sections 'Pros:' and 'Cons:' under 'Environment variables', and then the sections '12 Factors' and the short introduction to 'django-environ'. Focus on the main rule described for the 'Config' factor.

As the article explains, the third factor, Config, states that configuration should be strictly separated from the code. An app's configuration is everything that is likely to vary between deployments (development, staging, production). This includes:

  • Resource handles: Database locators, cache services.
  • Credentials to external services: API keys for services like Stripe or AWS S3.
  • Per-deployment values: The canonical hostname for the deployment.

By storing this information in environment variables, we create a clean boundary. The code remains the same across all environments, while the configuration is supplied by the environment it's running in. This makes our application highly portable and prevents us from accidentally committing sensitive secrets to a version control system like Git.


2. The "How": Loading Environment Variables

In the last lesson, we saw how django-environ can both load variables from a .env file and provide convenient methods for accessing them. Let's now look at another very popular and slightly more fundamental library, python-dotenv. Understanding it helps clarify what's happening under the hood.

python-dotenv has one primary job: to read key-value pairs from a .env file and load them into the operating system's environment. From there, you can access them using Python's standard os module.

Configure environment variables with Dotenv in Django

The video 'Configure environment variables with Dotenv in Django' by Cloud With Django provides a concise, step-by-step guide on using this library.

Please watch the video from the beginning until 07:03, and then the final summary from 08:21 to 09:16. The video covers: Why we need env vars (00:04): A quick refresher on securing keys and credentials. Installation (01:13): pip install python-dotenv. Loading in settings.py (01:42): Importing and calling load_dotenv(). Creating .env (03:15): Adding SECRET_KEY to the file. Accessing variables (06:17): Using os.environ.get(). .gitignore (08:21): A critical reminder to not commit the .env file.

To summarize the workflow with python-dotenv:

  1. Install: pip install python-dotenv
  2. Load: At the top of your settings.py (or settings/base.py):
    from dotenv import load_dotenv
    import os
    
    load_dotenv() # This reads the .env file
    
  3. Access: Use os.environ.get('VARIABLE_NAME', 'default_value') to retrieve the value.
    SECRET_KEY = os.environ.get('SECRET_KEY')
    DEBUG_STR = os.environ.get('DEBUG', 'True')
    DEBUG = DEBUG_STR.upper() == 'TRUE'
    

Notice that since environment variables are always strings, you have to handle type casting yourself (like converting the string 'True' to the boolean True). This is a key difference from django-environ, which provides helpers like env.bool('DEBUG') to do this for you. Both approaches are valid and widely used. For the rest of this course, we'll stick with django-environ due to its convenience features tailored for Django.


3. Practical Example: Securing Database Credentials

Let's apply this to a real-world scenario that goes beyond simple strings: configuring your production database. Hard-coding your database username, password, and host in settings.py is a major security risk.

The standard practice is to store the entire database connection info in a single environment variable, typically called DATABASE_URL. The format looks like this:

postgres://USER:PASSWORD@HOST:PORT/NAME

We can then use a small helper library, dj-database-url, to parse this URL and convert it into the dictionary format that Django's DATABASES setting expects.

Django Tutorial Part 11: Deploying Django to production

The official Mozilla Developer Network (MDN) Django tutorial provides a clear guide on this exact topic. We'll use it to configure our database securely.

Please read the section titled 'Database configuration'. It will walk you through: Installing dj-database-url and psycopg2-binary (for PostgreSQL). The code snippet to add to your settings.py to parse the DATABASE_URL environment variable.

Let's integrate this into the settings/production.py file we created in the last lesson.

1. Install the necessary packages:

pip install dj-database-url psycopg2-binary

(Remember to add these to your requirements.txt file)

2. Add the DATABASE_URL to your .env file:
This is for local testing of your production settings. On a real server, this would be a true environment variable.

# .env file
DATABASE_URL=postgres://myuser:mypassword@db.example.com:5432/mydatabase

3. Update settings/production.py:
If you're using django-environ, it conveniently bundles dj-database-url. The implementation is even cleaner. Your settings/production.py might look like this:

# settings/production.py

from .base import * # Imports the 'env' object from base.py

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False

ALLOWED_HOSTS = ['your-domain.com']

# ... other production settings

# Database
# Uses the DATABASE_URL from your .env file
DATABASES = {
    'default': env.db(),
}

The env.db() method from django-environ automatically looks for the DATABASE_URL variable, parses it, and returns the correct dictionary. This is a perfect example of keeping sensitive configuration completely separate from your code.

Test your understanding!

You need to integrate an external email service (like SendGrid) into your application for sending emails in production. The service provides you with an API key, which you need to use in your Django settings.

How would you securely store and access this SENDGRID_API_KEY in your project? Describe the steps from start to finish.

Show answer
  1. Store the Secret: Add the key to your .env file. This file is local to your machine and is not tracked by Git.

    # .env
    SENDGRID_API_KEY=SG.xxxxxxxxxxxxxxxxxxxx.yyyyyyyyyyyyyyyyyy
    
  2. Ignore the Secret: Double-check that your .gitignore file includes .env to prevent it from ever being committed to your repository.

  3. Create a Template: Add the variable name (without its value) to your .env.example file. This tells other developers that this key is required for the project.

    # .env.example
    SENDGRID_API_KEY=
    
  4. Access in Settings: In your settings/base.py (or production.py if it's only for production), use django-environ to load the key into a Django setting.

    # settings/base.py or settings/production.py
    
    # ... assuming 'env' is already initialized
    
    SENDGRID_API_KEY = env('SENDGRID_API_KEY')
    EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' # Example setting
    # ... other email settings that might use the key
    
  5. Use in Code: Now, the rest of your application can access the key safely via from django.conf import settings; settings.SENDGRID_API_KEY.


Conclusion

You have now mastered a foundational practice for building secure and maintainable Django applications. By externalizing configuration, you make your application more robust, portable, and significantly more secure.

Key Takeaways:

  • Storing configuration in environment variables is a core principle of the 12-Factor App methodology, promoting a strict separation of config and code.
  • Sensitive information like database URLs and API keys must never be committed to version control.
  • Libraries like python-dotenv and django-environ make it easy to load variables from a .env file during local development.
  • In a live production environment, these variables are set directly on the hosting platform, and your Django app reads them from the environment seamlessly.
  • Helper libraries like dj-database-url (included in django-environ) simplify parsing complex configuration strings like DATABASE_URL.

Next Up

Our application is now securely configured. The next piece of the deployment puzzle is managing static files—the CSS, JavaScript, and images that make your site look great. In the next lesson, we will learn how Django's static file handling works in production, using the collectstatic command and configuring a web server to serve these files efficiently.