In this post I am going to describe how to share environmental variables between applications running in a uwsgi
application server instance and the shell in the same Docker container.
Background
Consider the following scenario:
- A Django project running in a
uwsgi
application server instance within a Docker container. - The Django project requires access to certain external environmental variables, e.g.
DEBUG
orLOGS_DIR
. - The user requires these same environmental variables to be available to the shell so that they can use Django’s command line tools, such as
manage.py
, to perform various operations on the project.
Possible solutions include:
- Hard code these variables into the Django project’s
settings.py
file and specify them on the command line when runningmanage.py
, etc. - Hard code these variables into the Django project’s
settings.py
file and separately hard code them into a startup file for the shell, e.g.~/.profile
. - Keep all environmental variables in a single file and have both Django and the shell import them.
The first solution is cumbersome (remembering and typing out the variables every time you need them). The second solution is less so, but both lead to the possibility of inconsistency if a value is changed in one place but not the other.
This leaves the third solution as the best. In addition, it can allow for quicker switching between development and production environments.
The Solution
Create a file called environment.txt
in your Docker build directory. This file should contain your environmental variables as key-value pairs:
# Comment VAR1=VALUE1 VAR2=VALUE2 ...
In the Dockerfile for your container, add the following line to copy this file into a suitable place in the container (I use /project/conf
) during the build:
COPY environment.txt /project/conf/
Now the environmental variables will be present in your Docker container. The next step is making them available to the shell.
Create a shell startup file called profile
in your Docker build directory and add the following line to it:
export $(grep -v '^#' /project/conf/environment.txt | xargs -0)
When executed, this file will read the environment.txt
file and export the key-value pairs contained within it into the shell environment. The -v '^#'
argument to grep
will skip lines beginning with #
so you can use them for comments. Piping the output of grep
to xargs -0
allows values to contain spaces (the argument to xargs
may vary depending on the distribution you are using).
In the Dockerfile for your container, add the following line to copy this file into the primary user (usually root
) directory in your Docker container:
COPY profile /root/.profile
There is, however, one gotcha. In Alpine Linux (and possibly others) the default shell is not a login shell, which means .profile
will not be executed when the container is accessed using something like docker exec -it container /bin/sh
. To fix this , also add the following to the same Dockerfile:
ENV ENV="/root/.profile"
Django gets its environmental variables from the application server it is running in, so we need uwsgi
to also read environment.txt
. Add the following to your uwsgi.ini
file:
for-readline = /project/conf/environment.txt env = %(_) endfor
The final part of this solution is allowing the Django project to also access these variables. Add the following to the top of your Django project’s settings.py
file:
# DON'T USE THIS CODE - SEE UPDATE BELOW from django.core.exceptions import ImproperlyConfigured import os def get_env_variable(var_name): try: return os.environ[var_name] except KeyError: error_msg = 'Set the {} environmental variable'.format(var_name) raise ImproperlyConfigured(error_msg)
*UPDATE 2019-02-23*
So I realised that for boolean variable my original approach would not work properly. If you have DEBUG=False
in your environment.txt
file, the result of get_env_variable('DEBUG')
will be 'False'
(a string value) as opposed to False
(a boolean value). In Python, evaluating a variable set to a string value will result in True
, which is not what we want.
I added some lines to deal with the boolean case:
# USE THIS INSTEAD from django.core.exceptions import ImproperlyConfigured import os def get_env_variable(var_name): try: value = os.environ[var_name] if value.lower() == 'true': return True elif value.lower() == 'false': return False return value except KeyError: error_msg = 'Set the {} environmental variable'.format(var_name) raise ImproperlyConfigured(error_msg)
Ideally I would use a more complex case-insensitive comparison routine (see casefold
, unicode.normalize,
etc.) but I think this will suffice for the purposes here.
Then anywhere in settings.py
that you need to access an environmental variable, simply use:
VAR1 = get_env_variable('VAR1')
All done!
To-Do
One unsolved problem remains with this solution- if you execute manage.py
directly using docker exec
, it does not receive the environmental variables:
docker exec -it container command ...
Instead you have to use the more cumbersome:
docker exec -it container /bin/sh -lc 'command ...'
I don’t do this often but the grey matter is searching for a solution nonetheless…
Conclusion
This solution should also apply to other configurations, e.g. different Linux distributions, other uWSGI applications, Python 2 vs. Python 3, etc., with minimal modification.
FYI – don’t use this for passwords and other secrets!
Any comments, suggestions or corrections welcome!
Photo by Valeria Farina on Unsplash because I like lizards.
Hey people!!!!!
Good mood and good luck to everyone!!!!!