5.3. Container First Start#

What happens the first time you start your container and the database is not initialized? An error! In our packaging work so far we have relied on the user to manually run the migrate and createsuperuser commands. You may have forgotten about that if you kept a copy of the db.sqlite file outside of your container, but you’ll be reminded every time you start it in Kubernetes. In this lab we’ll put some thought into what should happen the first time our container starts.

First Start#

What should happen the first time our container starts? When there’s no database present we know we have to run the following commands:

$ python3 manage.py migrate 
$ python3 manage.py createsuperuser

To make the initial start of our container frictionless for our users we are going to add a shell script to the container that detects the first start and runs those commands. As you’ll see in this lab that’s a bit easier said than done, so we’ll work through some of the issues that are typical of this task.

Detecting the Database#

How do you know if the database has been initialized or not? This can be a bit tricky because we need Django’s help. You might wonder, why not just test for the presence of the db.sqlite file? The file exists in our current implementation but won’t when we later transition to a real database. To sort it out I Googled, “django detect database connection.” Take a look at the advice there and see if there’s an easy way to do it.

I settled on this:

$ python3 manage.py migrate --check

It’s not perfect because it might automatically apply migrations when we don’t want to, but it’s good enough for us.

Automatically Creating a Supersuser#

The python3 manage.py createsuperuser command requires an interactive terminal. That’s a no-go for us because we want to do this process automatically using environment variables. Take a look at the help message for the createsuperuser subcommand like this:

$ python3 manage.py createsuperuser --help 
usage: manage.py createsuperuser [-h] [--username USERNAME] [--noinput] [--database {default}] [--email EMAIL] [--version]
                                 [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback] [--no-color]
                                 [--force-color] [--skip-checks]

Used to create a superuser.

options:
  -h, --help            show this help message and exit
  --username USERNAME   Specifies the login for the superuser.
  --noinput, --no-input
                        Tells Django to NOT prompt the user for input of any kind. You must use --username with --noinput,
                        along with an option for any other required field. Superusers created with --noinput will not be
                        able to log in until they're given a valid password.
  --database {default}  Specifies the database to use. Default is "default".
  --email EMAIL         Specifies the email for the superuser.
  --version             Show program's version number and exit.
  -v {0,1,2,3}, --verbosity {0,1,2,3}
                        Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output
  --settings SETTINGS   The Python path to a settings module, e.g. "myproject.settings.main". If this isn't provided, the
                        DJANGO_SETTINGS_MODULE environment variable will be used.
  --pythonpath PYTHONPATH
                        A directory to add to the Python path, e.g. "/home/djangoprojects/myproject".
  --traceback           Display a full stack trace on CommandError exceptions.
  --no-color            Don't colorize the command output.
  --force-color         Force colorization of the command output.
  --skip-checks         Skip system checks.

There’s clearly a way to do this without interaction using the --noinput flag, but how do you set the password? It doesn’t say. So let’s Google this:

django create superuser command line noinput

According to the results the createsuperuser command responds to the environment variable DJANGO_SUPERUSER_PASSWORD. We can use that only when the migrate subcommand detects no database.

Changing the Start Command#

We need the check to run every time we start the container. Do do that we’re going to replace the CMD instruction in our Dockerfile to run a shell script instead of running the manage.py command directly.

Create a file called start.sh in the root of your repository and put this in:

#! /usr/bin/bash 

set -e # Exit on any error 

# Make sure the data directory exists. This silences an ugly error but isn't
# strictly necessary. 
mkdir -p $DATA_DIR 

if ! python3 manage.py migrate --check; then 
    echo Setting up Django for the first time. 
    python3 manage.py migrate
    python3 manage.py createsuperuser --noinput \
            --username $DJANGO_SUPERUSER_NAME \
            --email $DJANGO_SUPERUSER_EMAIL 
else
    echo "Django has been setup."
fi

python3 manage.py runserver 0.0.0.0:$PORT

Make this file executable:

$ chmod ugo+x ./djangotutorial/start.sh

Now, instead of running the runserver subcommand change the CMD instruction in your Dockerfile to this:

CMD ["/bin/sh", "-c", "./start.sh"]

Important

We’ve added new environment variables. We need to account for them in the next lab.

Try a Build and Run#

Error

The container is broken at this point. Keep going!

$ mkdir ~/data
$ docker build -t myapp .
$ docker run -it --rm \
    -u $(id -u):$(id -g) \
    -p 8080:8080 \
    -v $HOME/data:/data \
    myapp