Adding a database service using Docker Compose

To resolve the application error we currently have when running the release image, we need to run a database that the application can connect to, and ensure the application is configured to use the database.

We can achieve this using Docker Compose by adding a new service called db, which is based on the official MySQL server container:

version: '2.4'

services:
test:
build:
context: .
dockerfile: Dockerfile
target: test
release:
build:
context: .
dockerfile: Dockerfile
ports:
- 8000:8000
command:
- uwsgi
- --http=0.0.0.0:8000
- --module=todobackend.wsgi
- --master
db:
image: mysql:5.7
environment:
MYSQL_DATABASE: todobackend
MYSQL_USER: todo
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: password

Note that you can specify an external image using the image property, and the environment settings configure the MySQL container with a database called todobackend, a username, password, and a root password.

Now, you might be wondering how we configure our application to use MySQL and the new db service.  The todobackend application includes a settings file called src/todobackend/settings_release.py, which configures support for MySQL as the database backend:

# Import base settings
from .settings import *
import os

# Disable debug
DEBUG = True

# Set secret key
SECRET_KEY = os.environ.get('SECRET_KEY', SECRET_KEY)

# Must be explicitly specified when Debug is disabled
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '*').split(',')

# Database settings
DATABASES = {
'default': {
'ENGINE': 'mysql.connector.django',
'NAME': os.environ.get('MYSQL_DATABASE','todobackend'),
'USER': os.environ.get('MYSQL_USER','todo'),
'PASSWORD': os.environ.get('MYSQL_PASSWORD','password'),
'HOST': os.environ.get('MYSQL_HOST','localhost'),
'PORT': os.environ.get('MYSQL_PORT','3306'),
},
'OPTIONS': {
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'"
}
}

STATIC_ROOT = os.environ.get('STATIC_ROOT', '/public/static')
MEDIA_ROOT = os.environ.get('MEDIA_ROOT', '/public/media')

The DATABASES setting includes a configuration that specifies an engine of mysql.connector.django, which provides support for MySQL overriding the default SQLite driver, and you can see that the database name, username, and password can be obtained from the environment via the os.environ.get call. Also note that the STATIC_ROOT setting this is where Django looks for static content, such as HTML, CSS, JavaScript, and images—and by default, Django will look in /public/static if this environment variable is not defined.  As we saw earlier, currently our web application is missing this content, so keep this setting in mind for later when we fix the missing content issue.

Now that you understand how the todobackend application can be configured to support a MySQL database, let's modify the Docker Compose file to use the db service:

version: '2.4'

services:
test:
build:
context: .
dockerfile: Dockerfile
target: test
release:
build:
context: .
dockerfile: Dockerfile
ports:
- 8000:8000
depends_on:
db:
condition: service_healthy
environment:
DJANGO_SETTINGS_MODULE: todobackend.settings_release
MYSQL_HOST: db
MYSQL_USER: todo
MYSQL_PASSWORD: password
command:
- uwsgi
- --http=0.0.0.0:8000
- --module=todobackend.wsgi
- --master
db:
image: mysql:5.7
healthcheck:
test: mysqlshow -u $$MYSQL_USER -p$$MYSQL_PASSWORD
interval: 3s
retries: 10

environment:
MYSQL_DATABASE: todobackend
MYSQL_USER: todo
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: password

We first configure the environment property on the release service, which configures environment variables that will be passed to the container. Note that for Django applications, you can configure the DJANGO_SETTINGS_MODULE environment variable to specify which settings should be used, and this allows you to use the settings_release configuration that adds MySQL support. This configuration also allows you to use environment variables to specify the MySQL database settings, which must match the configuration of the db service.

We next configure the depends_on property for the release service, which describes any dependencies the service may have. Because the application must have a working connection to the database before it can start, we specify a condition of service_healthy, which means the db service must have passed a Docker health check before Docker Compose will attempt to start the release service. To configure the Docker health check on the db service, we configure the healthcheck property, which will configure Docker to run the command specified by the test parameter inside the db service container to verify service health, and to retry this command every 3 seconds up to 10 times until the db service is healthy. For this scenario, we use the mysqlshow command, which will only return a successful zero exit code once the MySQL process is accepting connections. Because Docker Compose will interpret single dollar signs as environment variables it should evaluate and replace in the Docker Compose file, we escape the environment variables referenced in the test command with double dollar signs to ensure that the command will literally execute mysqlshow -u $MYSQL_USER -p$MYSQL_PASSWORD.

At this point, we can test the changes by tearing down the current environment by pressing Ctrl + C in the terminal running the release service and typing the docker-compose down -v command (the -v flag will also delete any volumes created by Docker Compose), and then executing the docker-compose up release command:

> docker-compose down -v
Removing todobackend_release_1 ... done
Removing todobackend_test_run_1 ... done
Removing network todobackend_default
> docker-compose up release
Creating network "todobackend_default" with the default driver
Pulling db (mysql:5.7)...
5.7: Pulling from library/mysql
683abbb4ea60: Pull complete
0550d17aeefa: Pull complete
7e26605ddd77: Pull complete
9882737bd15f: Pull complete
999c06ab75f6: Pull complete
c71d695f9937: Pull complete
c38f847c1491: Pull complete
74f9c61f40bf: Pull complete
30b252a90a12: Pull complete
9f92ebb7da55: Pull complete
90303981d276: Pull complete
Digest: sha256:1203dfba2600f140b74e375a354b1b801fa1b32d6f80fdee5f155d1e9f38c841
Status: Downloaded newer image for mysql:5.7
Creating todobackend_db_1 ... done
Creating todobackend_release_1 ... done
Attaching to todobackend_release_1
release_1 | *** Starting uWSGI 2.0.17 (64bit) on [Thu Jul 5 07:45:38 2018] ***
release_1 | compiled with version: 6.4.0 on 04 July 2018 11:33:09
release_1 | os: Linux-4.9.93-linuxkit-aufs #1 SMP Wed Jun 6 16:55:56 UTC 2018
...
...
*** uWSGI is running in multiple interpreter mode ***
release_1 | spawned uWSGI master process (pid: 1)
release_1 | spawned uWSGI worker 1 (pid: 7, cores: 1)
release_1 | spawned uWSGI http 1 (pid: 8)

In the preceding example, note that Docker Compose automatically pulls the MySQL 5.7 image as configured via the image property, and then starts the db service. This will take between 15-30 seconds, and during this period, Docker Compose is waiting for Docker to report back that the db service is healthy. Every 3 seconds Docker runs the mysqlshow command as configured in the health check, repeating this continuously until the command returns a successful exit code (that is, an exit code of 0), at which point Docker will mark the container as healthy. Only at this point will Docker Compose start up the release service, which should start successfully given the db service is fully operational. 

If you browse once again to http://localhost:8000/todos, you will find that even though we added a db service and configure the release service to use this database, you are still receiving the no such table error you saw previously in the previous screenshot.