29
Doppler: How to Set Environment Variables for a Python Django Application using Apache and mod_wsgi in Docker
Using environment variables to configure Django and other Python applications is awesome, but using them with Apache and mod_wsgi in Docker is a tricky thing to get right.
That's why I created this step-by-step tutorial and sample application to put all the info you need in one place.
Although this tutorial is for Docker and Django, the same steps apply, whether you're using a Virtual Machine or a different Python framework.
Prefer just to read the code? Head to the accompanying repository at https://github.com/DopplerUniversity/django-apache-mod-wsgi
When hosting a Python WSGI compatible framework like Django in Apache with mod_wsgi, the only environment variables populated in the os.environ dictionary are those that exist in the environment of the script that starts Apache. But instead of having to mess with Apache's service manager settings (e.g. systemd or systemctl), there's a better way.
Most Apache distributions provide a shell script specifically for the purpose of setting environment variables that will be made available to modules such as mod_wsgi.
It's then a matter of knowing the location of this shell script as it can be different depending on the Linux distribution. For example:
- Debian/Ubuntu: /etc/apache2/envvars
- CentOS: /etc/sysconfig/httpd
We'll be using the python:3.9-slim-buster Docker image is Debian based.
Essentially, it boils down to fetching the secrets as key/value pairs and writing them to the envvars file in the typical shell environiment variables format:
export FIRST_NAME="The"
export LAST_NAME="Mandalorion"
But where from and how do we fetch the app config and secrets to populate that file?
As I'm the Developer Advocate for Doppler, I'll start with a Doppler CLI example, but the mechanics of "fetch secrets, then append to file" can easily be adapted.
First, you would need to set up your project in Doppler and you can use the following button to get you started if you want to follow along.
Then use the Doppler CLI inside the Docker container to fetch the secrets (requires a DOPPLER_TOKEN environment variable with a Service Token value):
# Transform JSON key:value pairs into export statements using jq
if [ -n "$DOPPLER_TOKEN" ]; then
echo '[info]: Appending environment variables to /etc/apache/envvars using Doppler CLI'
doppler secrets download --no-file | jq -r $'. | to_entries[] | "export \(.key)=\'\(.value)\'"' >> /etc/apache2/envvars
fi
Notice I've used single quotes, not double quotes around the values?
That's because it gives you the flexibility of storing secrets with double quotes such as JSON in Doppler which you could use for example, to dynamically set Django's ALLOWED_HOSTS setting dynamically for any environment.
ALLOWED_HOSTS = json.loads(os.environ['ALLOWED_HOSTS'])
You could also use a .env file but I wouldn't recommend it and instead, I'd look into using a secrets manager.
But that aside, here is how you could do it using an .env file:
if [ -f "$PWD/.env" ]; then
echo '[info]: Appending environment variables to /etc/apache/envvars from .env file'
cat "$PWD/.env" >> /etc/apache2/envvars
fi
Now that we know how to pass environment variables from Apache to mod_wsgi, let's move onto getting this working in Docker.
Let's breakdown the task of configuring a Python Django Application using Apache and mod_wsgi in Docker into three steps:
- Custom Start Script
- Apache Site Config
- Dockerfile
If you only want to see the working code examples, head to the accompanying repository at https://github.com/DopplerUniversity/django-apache-mod-wsgi
As this isn't a Docker or Apache tutorial, I won't be diving too deeply into the Dockerfile or Apache site config file, but if you've got questions, head over to the Doppler community forum and I'll be able to help you there.
Running your application in Docker is usually a case of setting the CMD, for example:
CMD ["python", "src/app.py"]
But it's trickier here as we first need to append the environment variables to /etc/apache2/envvars
before running Apache.
As this requires multiple commands, we'll create a custom script:
#!/bin/bash
# apache-doppler-start
set -e
echo 'ServerName localhost' >> /etc/apache2/apache2.conf # Silence FQDN warning
# Doppler CLI
if [ -n "$DOPPLER_TOKEN" ]; then
echo '[info]: Appending environment variables to /etc/apache/envvars from Doppler CLI'
doppler secrets download --no-file | jq -r $'. | to_entries[] | "export \(.key)=\'\(.value)\'"' >> /etc/apache2/envvars
fi
# Mounted .env file
if [ -f "$PWD/.env" ]; then
echo '[info]: Appending environment variables to /etc/apache/envvars from .env file'
cat "$PWD/.env" >> /etc/apache2/envvars
fi
# Run Apache
apache2ctl -D FOREGROUND
Here is an example Apache site config file for a Django application:
# wsgi.conf
<VirtualHost *:80>
ServerName django-apache-mod-wsgi
ServerAlias django-apache-mod-wsgi
ServerAdmin webmaster@doppler
# Defining `WSGIDaemonProcess` and `WSGIProcessGroup` triggers daemon mode
WSGIDaemonProcess django-apache-mod-wsgi processes=2 threads=15 display-name=%{GROUP} python-path=/usr/local/lib/python3.9/site-packages:/usr/src/app
WSGIProcessGroup django-apache-mod-wsgi
WSGIScriptAlias / /usr/src/app/doppler/wsgi.py
<Directory /usr/src/app/doppler/>
<Files wsgi.py>
Require all granted
</Files>
</Directory>
# Redirect all logging to stdout for Docker
LogLevel INFO
ErrorLog /dev/stdout
TransferLog /dev/stdout
</VirtualHost>
The Dockerfile is reasonably straightforward, installing the Doppler CLI and Apache dependencies before copying the Django source code, custom script, and Apache site config:
FROM python:3.9-slim-buster
ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1
# Install Doppler CLI and related dependencies
RUN apt-get -qq update && apt-get install -y apt-transport-https ca-certificates curl gnupg jq && \
curl -sLf --retry 3 --tlsv1.2 --proto "=https" 'https://packages.doppler.com/public/cli/gpg.DE2A7741A397C129.key' | apt-key add - && \
echo "deb https://packages.doppler.com/public/cli/deb/debian any-version main" | tee /etc/apt/sources.list.d/doppler-cli.list && \
apt-get -qq update && apt-get install doppler
# Install Apache and related dependencies
RUN apt-get install --yes apache2 apache2-dev libapache2-mod-wsgi-py3 && \
apt-get clean && \
apt-get remove --purge --auto-remove -y && \
rm -rf /var/lib/apt/lists/*
WORKDIR /usr/src/app
COPY requirements*.txt .
RUN pip install --quiet --no-cache-dir --upgrade pip && \
pip install --quiet --no-cache-dir -r requirements.txt
# Application source
COPY src/ ./
# Custom CMD script
COPY apache-doppler-start /usr/local/bin/
# Apache site config
COPY wsgi.conf /etc/apache2/sites-enabled/000-default.conf
EXPOSE 80 443
# https://httpd.apache.org/docs/2.4/stopping.html#gracefulstop
STOPSIGNAL SIGWINCH
CMD ["apache-doppler-start"]
With all the pieces in place, we can nowbuild the Docker image (clone the sample repository to follow along):
docker image build -t django-apache-mod-wsgi:latest .
Now we're ready to run the container!
We'll start with a Doppler example, then with an .env file.
With Doppler, you'll first need to set a DOPPLER_TOKEN environment variable to the value of a Service Token. This is what provides read-only access to a specific Doppler config in production environments.
Usually, this would be securely set by your deployment environment (e.g. GitHub Action Secret) but for completeness and simplicity, we'll set it manually below:
export DOPPLER_TOKEN="dp.st.xxxx" # Service token value created from Doppler dashboard
Now run the container:
docker container run \
-it \
--init \
--name doppler-apache-mod-wsgi \
--rm \
-p 8080:80 \
-e DOPPLER_TOKEN="$DOPPLER_TOKEN" \
django-apache-mod-wsgi
To run the .env file version, we'll use the sample.env file from the sample repository:
# sample.env
export DJANGO_SETTINGS_MODULE='doppler.settings'
export DEBUG='yes'
export ALLOWED_HOSTS='["*"]'
export SECRET_KEY='bf5e1b31-6ba7-48e2-9175-f2293671e6df'
Then to run the container:
docker container run \
-it \
--init \
--name dotenv-apache-mod-wsgi \
--rm \
-v $(pwd)/sample.env:/usr/src/app/.env \
-p 8080:80 \
django-apache-mod-wsgi
Nice work in making it to the end!
Now you know how to configure Python applications hosted with Apache and mod_wsgi running in Docker using environment variables for app configuration and secrets.
Feedback is welcome and you can reach us on Twitter, our Community forum, or send me an email at [email protected].
29