Рабочее окружение с Docker, Django и Mysql

Докер это софт для управления контейнерами. Позволяет создавать изолированные окружения для запуска приложений.

Я рассматриваю докер с точки зрения удобства разработки, поэтому деплоя на продакшене постараюсь не касаться.

Преимущества Docker и сравнение с Vagrant

Ранее я использовал Vagrant при разработке питонячих приложений. Это помогает легко переносить работу с одного компьютера на другой и обратно, без сопутствующей мороки с настройкой проекта, зависимостей и всего остального. Переходим за другой компьютер, git pull, vagrant up, emacs - работаем дальше!

В отличие от виртуальных машин(имеется ввиду использование Vagrant) у контейнеров нет накладных расходов ресурсов на операционку в виртуалке.

Если речь идет о работе на Mac OS X, то вы можете возразить, что виртуалку тоже придется запускать, какая разница с вагрантом?

Разница есть. В том случае, если приходится работать с несколькими проектами одновременно или часто переключаться между проектами, то профит будет чувствоваться практически сразу: виртуалка для докера нужна всего одна, а в ней сколько надо контейнеров.

Еще одно сравнение между Vagrant и Docker в случае нескольких параллельных проектов: кроме того, что в вагранте придется создавать несколько виртуалок и "платить" используемой оперативкой за виртуальные линуксы, так еще и будут оставаться излишки памяти, отведенные в этих виртуалках для работы приложения. Скажем под виртуалку с питонячим приложением отведено 600 Мб. И так три проекта, три виртуалки. В каждой остается свободно скажем по 150 Мб. И так три раза, а значит на хост машине впустую съедено 450 мб оперативки. А еще в какой-то момент какому-то приложению может не хватить памяти и тогда придется править Vagrantfile, с целью увеличения доступной памяти.

С Docker каждое приложение будет использовать столько памяти, сколько ему нужно и не будет бесцельного резервирования оперативки на хост машине.

Установка Docker на Ubuntu 14.04 64bit

Установка на убунту с комментариями про разные её версии хорошо описана на официальном сайте докера.

https://docs.docker.com/engine/installation/linux/ubuntulinux/

Главное, что я тут замечу - ставить докер из пакетов, которые доступны в убунте не следует!

Здесь я опишу как поставить докер на 14.04, если до этого не было попыток его установить. Если что-то пойдет не так – следует читать официальный сайт докера, по ссылке выше.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
apt-get update
apt-get install apt-transport-https ca-certificates
sudo apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D
echo "deb https://apt.dockerproject.org/repo ubuntu-trusty main" > /etc/apt/sources.list.d/docker.list
apt-get update
apt-get purge lxc-docker
apt-cache policy docker-engine
sudo apt-get install linux-image-extra-$(uname -r)
apt-get install apparmor
sudo apt-get install docker-engine
sudo service docker start
sudo docker run hello-world

Если по итогам этих команд запустится и отработает hello-world контейнер – всё хорошо. Troubleshooting'а в этой статье не будет.

Разработка Django-приложения с Docker

Материалы, которые легли в основу этого раздела:

https://docs.docker.com/compose/django/

http://michal.karzynski.pl/blog/2015/04/19/packaging-django-applications-as-docker-container-images/

Создайте пустую директорию для проекта, назовем её sampleapp.

В ней создайте 3 файла: requirements.txt, Dockerfile, docker-compose.yml

Dockerfile
1
2
3
4
5
6
7
8
9
FROM python:2.7
ENV PYTHONUNBUFFERED 1
RUN apt-get update && apt-get install -y python python-pip netcat && rm -rf /var/lib/apt/lists/*
RUN mkdir /code
WORKDIR /code
COPY requirements.txt /code/
RUN pip install -r requirements.txt
ADD . /code/
EXPOSE 8080

requirements.txt
1
2
3
Django
MySQL-python
gunicorn

docker-compose.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
version: '2'
services:
  db:
    image: mysql
    environment:
      MYSQL_ROOT_PASSWORD: Somepassword
      MYSQL_DATABASE: projectdb
      MYSQL_USER: projectuser
      MYSQL_PASSWORD: strongpassword
    ports:
      - "3306:3306"
  dev:
    build: .
    command: python manage.py runserver 0.0.0.0:8080
    volumes:
      - .:/code
    ports:
      - "8080:8080"
    depends_on:
      - db
    links:
      - db

Мы помним, что директория с проектом у нас пустая, поэтому создадим проект

1
docker-compose run dev django-admin.py startproject sampleproject .

Теперь можно запустить дев-сервер:

1
docker-compose run dev

На выходе:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
sampleapp $ docker-compose run dev
Performing system checks...
 
System check identified no issues (0 silenced).
 
You have unapplied migrations; your app may not work properly until they are applied.
Run 'python manage.py migrate' to apply them.
 
March 02, 2016 - 21:35:51
Django version 1.9.3, using settings 'sampleproject.settings'
Starting development server at http://0.0.0.0:8080/
Quit the server with CONTROL-C.

Радуемся – проект запустился! (Ctrl+C, чтобы убить процесс)

Настройки

Проект может быть запущен в различных окружениях, а не только в докере. Кто ж знает как будете деплоить на продакшене?

Настройки от проекта к проекту могут расходиться, поэтому я привык выделять общие настройки в базовый файл, а специфичные для места деплоя - в отдельный файл.

Таким образом наша задача избавиться от settings.py и сделать модуль settings, в котором будет base.py и docker.py

1
2
3
4
5
6
cd sampleproject/
mkdir settings
touch settings/__init__.py
mv settings.py settings/base.py
rm settings.pyc
touch settings/docker.py

Отредактируем settings/base.py, чтобы он выглядел примерно так:

settings/base.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import os

PROJECT_PATH = os.path.realpath(os.path.dirname(__file__))
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

MEDIA_ROOT = PROJECT_PATH + '/../media/'
MEDIA_URL = '/media/'
STATIC_URL = '/static/'
STATIC_ROOT = PROJECT_PATH + '/../static/'
SECRET_KEY = '_(uj2%#0r1cg2*xq)c8^_v(5i)(c7@44)@tg7pa41n65vht)6f'
DEBUG = True

ALLOWED_HOSTS = []
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

MIDDLEWARE_CLASSES = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'sampleproject.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]
WSGI_APPLICATION = 'sampleproject.wsgi.application'
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}
AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]
LANGUAGE_CODE = 'ru'
TIME_ZONE = 'Europe/Moscow'
USE_I18N = True
USE_L10N = True
USE_TZ = True

Файл settings/docker.py будет выглядеть так:

settings/docker.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from .base import *

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'projectdb',
        'USER': 'projectuser',
        'PASSWORD': 'strongpassword',
        'HOST': 'db',
    }
}

Теперь поясню:

В качестве DJANGO_SETTINGS_MODULE мы будем указывать settings.docker, таким образом будут импортированы все стандартные/базовые настройки, а потом они будут перезаписаны настройками под конкретный деплой. В данном случае мы перезаписываем только настройки БД.

Теперь нам надо донести до приложение, откуда брать настройки для запуска.

Для этого нужно передать переменную DJANGO_SETTINGS_MODULE="sampleproject.settings.docker"

Внесем изменения в docker-compose.yml

docker-compose.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
version: '2'
services:
  db:
    image: mysql
    environment:
      MYSQL_ROOT_PASSWORD: Somepassword
      MYSQL_DATABASE: projectdb
      MYSQL_USER: projectuser
      MYSQL_PASSWORD: strongpassword
    ports:
      - "3306:3306"
  dev:
    build: .
    environment:
      DJANGO_SETTINGS_MODULE: sampleproject.settings.docker
    volumes:
      - .:/code
    ports:
      - "8080:8080"
    depends_on:
      - db
    links:
      - db

Результат:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
$ docker-compose run dev python manage.py migrate
Operations to perform:
  Apply all migrations: admin, contenttypes, auth, sessions
Running migrations:
  Rendering model states... DONE
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying sessions.0001_initial... OK
sampleapp $

Теперь можно запустить наш контейнер:

1
docker-compose up

ENTRYPOINT

Нашему приложению не хватает этапа со сбором статики и проведением миграций.

docker-entrypoint.sh
1
2
3
4
5
6
7
#!/bin/bash
while ! nc -z db 3306; do echo "*** looks like mysql is not ready yet"; sleep 3; done
echo "+++ Mysql server is ready +++"
python manage.py migrate                  # Apply database migrations
python manage.py collectstatic --noinput  # Collect static files
echo Starting django dev server
python manage.py runserver 0.0.0.0:8080

Доработаем Dockerfile

Dockerfile:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
FROM python:2.7
ENV PYTHONUNBUFFERED 1
ENV DJANGO_SETTINGS_MODULE sampleproject.settings.docker
RUN apt-get update && apt-get install -y python python-pip && rm -rf /var/lib/apt/lists/*
RUN mkdir /code
WORKDIR /code
COPY requirements.txt /code/
RUN pip install -r requirements.txt
ADD . /code/
EXPOSE 8080
ENTRYPOINT ["/code/docker-entrypoint.sh"]

На чем тут можно остановиться, так это на второй строке (while...) где проверяется поднялась ли MYSQL БД.

Здесь мы проверяем открыт ли порт 3306 на db. Когда mysql поднимается, тогда entrypoint будет отрабатывать дальше.

Пересоберем image

1
docker build -t sampleapp .

Бонус: Docker, Django, Gunicorn

Если очень хочется запустить приложение через gunicorn, тогда docker-entrypoint.sh будет выглядеть так:

docker-entrypoint.sh
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#!/bin/bash
while ! nc -z db 3306; do echo "*** looks like mysql is not ready yet"; sleep 3; done
echo "+++ Mysql server is ready +++"
python manage.py migrate                  # Apply database migrations
python manage.py collectstatic --noinput  # Collect static files
mkdir /code/logs
tail -n 0 -f /code/logs/*.log &

echo Starting Gunicorn.
exec gunicorn sampleproject.wsgi:application \
    --reload \
    --name sampleapp \
    --bind 0.0.0.0:8080 \
    --workers 3 \
    --log-level=info \
    --log-file=/code/logs/gunicorn.log \
    --access-logfile=/code/logs/access.log \
    "$@"

Строка --reload означает, что при изменении кодовой базы приложение перезапустится. Для production стоит удалить эту строку.

Запустим всё

1
docker-compose up

Можно заходить на порт 8080 и любоваться джанговским "It worked!".

Если вы используете docker-machine, то IP можно узнать запустив команду

1
docker-machine ip default

Опубликовано: 3 марта 2016 г.

Although that way may not be obvious at first unless you're Dutch.

PEP20: The Zen of Python