I've been fidding with Docker and Docker Swarm a bit for the past couple of years and wanted to see how easy it was to use Kubernetes. I thought I'd start with a simple Django project I could deploy locally using Minikube. The tutorial uses Django 1.10 and 18.03.1-ce. Assuming a blank slate, we can start by installing Docker.
sudo apt-get update sudo apt-get install \ apt-transport-https \ ca-certificates \ curl \ software-properties-common curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - sudo apt-key fingerprint 0EBFCD88 sudo add-apt-repository \ "deb [arch=amd64] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) \ stable" sudo apt-get update sudo apt-get install docker-ce
Next we can create a Django app to use for testing Docker. The app will allow us to add names to a local database.
sudo mkdir /kubernetestest sudo chown dan:dan /kubernetestest cd /kubernetestest django-admin startproject simplesite
Next I created a models directory in the app directory with a people.py file with the following:
from django.db import models class People(models.Model): class Meta: db_table = "people" managed = True people_id = models.AutoField(primary_key=True) name = models.CharField(max_length=50)
The main page code in views.py page looks like this:
from models.people import People def index(request): people = [] try: #if posting, add a new person to the database if request.method == 'POST': People.objects.create(name=request.POST['name']) #fetch all names from the database to display on the page people = People.objects.all() except Exception as ex: pass return render(request, "index.html", { "people": people })
And the template looks like this:
<html> <body> Current Values <ul> {% for person in people %} <li>{{person.name}}</li> {% endfor %} </ul> <form method="POST"> {% csrf_token %} Add a value:<br/> <input type="text" name="name"/><br/> <input type="submit" value="Add"/> </form> </body> </html>
For the initial run, I created a sqlite database so the site can be run locally:
python manage.py makemigrations simplesite python manage.py sqlmigrate simplesite 0001 python manage.py migrate
Now we can run our server and test it:
python manage.py runserver
Now that the basic app is runable as a site, we can think about how to containerize. I wanted the containerized solution to use NGINX and uWSGI instead of just the built in Django webserver so Docker would need to set them up complete with config files. In a production solution you might use separate containers for these services but I combined them with Django for simplicity. I created a directory parallel to the Django website called setupscripts with the basic uWSGI and NGINX config files. The uWSGI config sits in simplesite.ini:
[uwsgi] touch-reload = /tmp/simplesite socket = 127.0.0.1:3031 enable-threads = true single-interpreter = true chmod-socket = 770 chown-socket www-data:www-data workers = 15 uid = www-data gid = www-data chdir = /simplesite wsgi-file=/simplesite/simplesite/simplesite.wsgi for-readline = /simplesite/uwsgi_vars.ini env = %(_) endfor = module = django.core.handlers.wsgi:WSGIHander() buffer-size = 16384
The environment variables that uWSGI will pass to Django will come from a config file that will be created when the container starts. The NGINX settings are in a file called nginx-default:
server { listen 80 default_server; listen [::]:80 default_server ipv6only=on; location / { include uwsgi_params; uwsgi_pass 127.0.0.1:3031; } }
Finally we need a wsgi file in our Django app for uWSGI to use. The simplesite.wsgi file goes in the main app directory:
import os, sys import json import socket apache_configuration = os.path.dirname(__file__) project = os.path.dirname(apache_configuration) workspace = os.path.dirname(project) toplevel = os.path.dirname(workspace) sys.path.append(workspace) sys.path.append(project) sys.path.append(toplevel) #if not set by uwsgi, default to base settings if not "DJANGO_SETTINGS_MODULE" in os.environ: os.environ["DJANGO_SETTINGS_MODULE"] = "simplesite.settings" if "MYSQL_HOST_DNS" in os.environ: os.environ["MAIN_DB_HOST"] = socket.gethostbyname(os.environ["MYSQL_HOST_DNS"]) else: os.environ["MAIN_DB_HOST"] = "localhost" from django.core.wsgi import get_wsgi_application application = get_wsgi_application()
The file makes sure the app defaults to the base settings file if no alternate settings file is supplied in the environment variables. The actual Dockerfile to house the Djano app sits in the root project directory along with manage.py. It looks like this:
FROM ubuntu:latest RUN apt-get update RUN apt-get install -y python python-dev python-pip build-essential RUN apt-get install -y nginx uwsgi uwsgi-plugin-python curl nano net-tools RUN apt-get install -y libmysqlclient-dev python-mysqldb RUN /usr/bin/pip install Django==1.10.8 RUN mkdir /simplesite && chown www-data:www-data /simplesite ADD setupscripts/init.sh /tmp/init.sh ADD setupscripts/nginx-default /etc/nginx/sites-available/default ADD setupscripts/simplesite.ini /etc/uwsgi/apps-available/simplesite.ini ADD manage.py /simplesite COPY simplesite /simplesite/simplesite RUN ln -s /etc/uwsgi/apps-available/simplesite.ini /etc/uwsgi/apps-enabled/simplesite.ini RUN touch /tmp/simplesite RUN ["chmod", "+x", "/tmp/init.sh"] ENTRYPOINT ["/tmp/init.sh"] CMD ["nginx", "-g", "daemon off;"]
Now we need to create the init.sh file that the container will call when it starts. The file goes in the setupscripts directory and creates the file with the needed uWSGI variables from environment variables and then runs NGINX:
#!/bin/bash chown -R www-data:www-data /simplesite touch /simplesite/uwsgi_vars.ini service uwsgi restart exec "$@"
Finally we can build our docker container:
sudo docker build -t simplesite .
To test it:
sudo docker run -d -p 8095:80 simplesite
To run it, go to http://localhost:8095/. You should be able to add names to the database using the text entry box on the form.
The next step is to set up our MySQL database so we can use it in place of sqlite. I created a new Dockerfile in a parallel directory to the Django project root:
FROM mysql:5.7.15 ADD schema.sql /docker-entrypoint-initdb.d EXPOSE 3306
Next I created a schema.sql file in the same directory to handle the base setup for our new db:
create database kubernetes_test; create user 'testuser'@'%' identified by 'password'; grant all privileges on kubernetes_test.* to 'testuser'@'%' with grant option; use kubernetes_test; create table people (people_id integer not null auto_increment primary key, name varchar(50));
Now we can create the docker image from the directory where the Dockerfile is:
sudo docker build -t mysqlmain .
Now we need to update our Django container to be able to optionally use a MySQL database. I created a new settings file in the Django app and called it settings_docker.py:
from settings import * import os DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "NAME": os.getenv("MAIN_DB_NAME"), "USER": os.getenv("MAIN_DB_USER"), "PASSWORD": os.getenv("MAIN_DB_PASSWORD"), "HOST": os.getenv("MAIN_DB_HOST"), } }
Environment variables allow us to not hard code any assumptions about the database. The init.sh file can now be updated to set the variables our Django container will need.
#!/bin/bash chown -R www-data:www-data /simplesite touch /simplesite/uwsgi_vars.ini echo "MYSQL_HOST_DNS=$MYSQL_HOST_DNS" >>/simplesite/uwsgi_vars.ini echo "MAIN_DB_NAME=$MAIN_DB_NAME" >>/simplesite/uwsgi_vars.ini echo "MAIN_DB_USER=$MAIN_DB_USER" >>/simplesite/uwsgi_vars.ini echo "MAIN_DB_PASSWORD=$MAIN_DB_PASSWORD" >>/simplesite/uwsgi_vars.ini if [ -z "$DJANGO_SETTINGS_FILE" ]; then echo "DJANGO_SETTINGS_MODULE=simplesite.settings" >>/simplesite/uwsgi_vars.ini python /simplesite/manage.py migrate chown www-data:www-data /simplesite/db.sqlite3 else echo "DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_FILE" >>/simplesite/uwsgi_vars.ini fi service uwsgi restart exec "$@"
We can now re-create our docker image:
sudo docker build -t simplesite .
Next we will use docker compose to test them out together. You can install docker compose with the following if you don't already have it:
sudo curl -L https://github.com/docker/compose/releases/download/1.18.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose sudo chmod +x /usr/local/bin/docker-compose
Now we can create a docker-compose.yml file in a separate directory:
version: '3' services: db: image: "mysqlmain" environment: - MYSQL_ROOT_PASSWORD=password web: image: "simplesite" ports: - "8095:80" links: - db:mysqlmain environment: - DJANGO_SETTINGS_FILE=settings_docker - MYSQL_HOST_DNS=mysqlmain - MAIN_DB_NAME=kubernetes_test - MAIN_DB_USER=testuser - MAIN_DB_PASSWORD=password
This file will set some environment variables for us such as the settings file Django should use and the credentials to connect to our database. To create our containers, run:
sudo docker-compose up
And now the site should again be reachable at http://localhost:8095/. In the part 2 I'll show how to use kubernetes locally to manage the containers.