Django + Docker + pipenvでWebアプリ開発環境構築

Hello Worldからはじまるプログラミング学習から、実際に動く”もの”を作りたいときに障壁となるのが環境構築です。

Dockerを使えば、自分の開発環境と同じ環境を、お手軽に友達のPCにもデプロイ用のサーバーにも再現することができます。

この環境を再現するコストの低さは、他の仮想化技術と比較したパフォーマンスの高さと並んでDockerの重要なメリットです。

筆者の環境を以下に示します。DockerはLinuxでなくても利用可能ですが、コンテナ上で利用可能なOSは限られます。

筆者の環境

  • OS: Ubuntu18.04 LTS
  • Docker version: 19.03
  • Django version: 2.2.3

WindowsにはDocker for Windows、MacにはDocker for Mac、Linuxには主要なディストリビューションでのインストール方法が用意されているので、それぞれ導入されていることを前提に進めます。

cdmkdirなど、基本的なLinuxコマンドも利用するので、Windows上で実行する場合はPowershellや専用のターミナル上で実行するか、同様の操作をGUIで行う必要があります。

Pythonの仮想環境

Pythonの仮想環境の構築にはpyenv, virtualenv, venvなど、利用できるツールは複数用意されています。ここではその中で、pipenvを利用します。

参考 Pipenv: 人間のためのPython開発ワークフローpipenv document

Pipenvの導入

pipenvの導入は簡単です。pipがインストールされている状態で、以下のコマンドを実行しましょう。

$ pip install pipenv

pipenvを使ったライブラリのバージョン管理はPipfile, Pipfile.lockという2つのファイルを使って行われますが、ユーザー自身が作成する必要はなく、例えばpipenv install django==2.2.3とコマンドを実行すると自動的に生成・更新されます。

virtualenv等の仮想環境ではrequirements.txtに使用するライブラリとバージョン情報を記述して、それに従って仮想環境を作成します。

pipenvはrequirements.txtに従って仮想環境を作成することもできますし、requirements.txtをエクスポートすることもできます。

# requirements.txtのインポート
$ pipenv install -r requirements.txt
# requirements.txtの生成
$ pipenv lock -r
https://datascientist-toolbox.com/wp-content/uploads/2019/09/man.png
りーぐる

明確な指針をもってバージョン管理をすることが重要であり、どのツールを使うかは好みの問題です

仮想環境の作成

pipenvの仮想環境の作成は以下のコマンドで、簡単に行うことが出来ます。

# Python3系で仮想環境を作成
$ pipenv --python 3
# Python3.7で仮想環境を作成
$ pipenv --python 3.7

仮想環境作成時、コマンドを実行したフォルダの直下にPipfileが作成されます。

[source]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]

[packages]

[requires]
python_version = "3.7"

パッケージのインストール

仮想環境へのパッケージのインストールは、pipenv install <package name>で行います。インストール時に、Pipfileが存在しなければ自動的に仮想環境を作成します。

$ pipenv install django==2.2.3

パッケージをインストールすると、Pipfile.lockが自動生成され、バージョン情報やパッケージの依存関係などが記述されます。このPipfile.lockはパッケージをインストールする度にpipenvが自動的に更新してくれます。

{
    "_meta": {
        "hash": {
            "sha256": "bdee8d9bc98c76650378c4f593328f5f0f720b347e58626a1453bb96c322340c"
        },
        "pipfile-spec": 7,
        "requires": {
            "python_version": "3.7"
        },
        "sources": [
            {
                "name": "pypi",
                "url": "https://pypi.org/simple",
                "verify_ssl": true
            }
        ]
    },
    "default": {
        "django": {
            "hashes": [
                "sha256:4d23f61b26892bac785f07401bc38cbf8fa4cec993f400e9cd9ddf28fd51c0ea",
                "sha256:6e974d4b57e3b29e4882b244d40171d6a75202ab8d2402b8e8adbd182e25cf0c"
            ],
            "index": "pypi",
            "version": "==2.2.3"
        },
        "pytz": {
            "hashes": [
                "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
                "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
            ],
            "version": "==2019.3"
        },
        "sqlparse": {
            "hashes": [
                "sha256:40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177",
                "sha256:7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873"
            ],
            "version": "==0.3.0"
        }
    },
    "develop": {}
}

仮想環境のactivate, deactivate

仮想環境を作成しても、仮想環境がアクティブになっていなければ、当然仮想環境内のライブラリは利用できません。

最後に仮想環境をアクティブにする方法と、仮想環境から抜ける方法を確認しておきましょう。

# 仮想環境のアクティベート(Pipfileの存在するフォルダで実行)
$ pipenv shell
# 仮想環境から抜ける
$ exit

Webフレームワーク Django

DjangoはフルスタックなPythonのWebフレームワークです。軽量なフレームワークとしては、Flaskなどが有名です。

今回は環境構築がメインなので、Djangoについて深く立ち入ることはしません。Djangoについて知りたい方は下記のDjangoチュートリアルなどを参考にしてください。

本記事では、Djangoの動作確認が可能な最低限のコードの実装を行います。

Djangoチュートリアル

Djangoは無料のチュートリアル(日本語版あり)がわかりやすいので、Django習得の第一歩として非常に有益なリソースですので、初心者の方は是非参照してみてください。

参考 Django girls tutorial (日本語版)Django girls

次のステップとして、Django公式のチュートリアルが充実しています。こちらはDjango girlsよりやや高度な内容となっています。

参考 Django 公式チュートリアル (日本語版)Django公式

自身の習熟度に合わせて、トライしてみてください!

DjangoでHello World

DjangoでWebブラウザのhttp://127.0.0.1:8000/ (localhostの8000番ポート)にHello Worldを表示してみましょう。

まずは、アプリケーション開発フォルダを作成しましょう。

$ mkdir DsWebApp
$ cd DsWebApp

DsWebAppフォルダに移動して、仮想環境を作成します。今回はdjango 2.2.3を使用しますので、以下のコマンドでパッケージのインストールと仮想環境の作成を同時に行います。

作成が完了したら、仮想環境をアクティブにするのを忘れないようにしましょう。

$ pipenv install django==2.2.3
$ pipenv shell
(DsWebApp) $

Djangoプロジェクトの作成

続いてDjangoプロジェクトを作成します。カレントディレクトリにdshack_projectを作成します。プロジェクト名は好きな名前をつけてください。

仮想環境の有効化に成功していれば、以下のコマンドでプロジェクトの作成ができます。

$ django-admin startproject dshack_project .

続いてデータベースの初期化を行います。python manage.py migrateはデータベースに変更を加える(マイグレーション)の際に、実行するコマンドですが、ここでは初期化のために実行します。

$ python manage.py migrate

Webアプリケーション開発の初期段階では、自身のWebブラウザ上(localhost)で画面を確認しながら開発を進めるのが一般的です。Djangoでは、以下のコマンドでローカルWebサーバーを起動することができます。

$ python manage.py runserver

コマンド実行後、自身のブラウザでhttp://127.0.0.1:8000/にアクセスするとDjangoのウェルカムページが表示されます。

アプリの作成

Djangoではstartappコマンドで複数のアプリを作成することができます。

この「アプリ」はWebアプリ全体のことではなく、Webアプリを構成する個々の機能を独立した「アプリ」として開発できると捉えると良いかもしれません。(但し必ずしも1機能が1アプリというわけではありません)

ここではhello_worldアプリを作成しましょう。以下のコマンドでアプリの作成が実行できます。

$ python manage.py startapp hello_world

settings.pyの更新

アプリの作成が完了したら先程作成したプロジェクト(私の場合はdshack_project)に対して、作成したhelloworldアプリを認識させる必要があります。

dshack_project/settings.py内でINSTALLED_APPSを見つけ、一番最後にhelloworld.apps.HelloworldConfigを追加することで、アプリの登録ができます。

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # apps
    'helloworld.apps.HelloworldConfig',
]

urls.pyの更新

続いてURLルーティングを設定します。Djangoのベストプラクティスとして、アプリケーションごとにurls.pyを配置するのが一般的です。

階層構造をとることで、プロジェクト全体のURLパターンとアプリケーション内部のURLパターンを分離することができ、管理がしやすくなるというメリットがあります。

プロジェクト作成時にはプロジェクトフォルダ内にurls.pyが自動作成されますが、アプリケーション作成時には自動作成されないので自分でurls.pyを配置する必要があります。

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('helloworld.urls')),
]

続いてhelloworldアプリ内にurls.pyを作成し、URLパターンの設定を行いましょう。(今回は1ページのみです)

$ touch helloworld/urls.py
from django.urls import path

from .views import homePageView

urlpatterns = [
    path('', homePageView.as_view(), name='home')
]

アプリのurls.pyには、ビュー(View)を登録し、ビューがリクエストを受け取ってレスポンスを返す役割を担います。続いてビューの作成を行います。

ビューの作成

今回作成するビューはTemplateViewクラスを継承して、使用するテンプレート名を設定するだけの単純なものです。

from django.views.generic import TemplateView

class HomePageView(TemplateView):
    template_name = 'home.html'

テンプレートの作成

Djangoの見た目部分は、主にテンプレートが担当します。テンプレートの仕組みを利用して、バックエンドの処理に合わせて、HTMLファイルの中身を変更することができます。

Djangoのデフォルトでは、各アプリフォルダの直下でテンプレートを管理することになっていますが、個人的にはテンプレートファイルはプロジェクトレベルで一元管理した方が楽なので、その方法を紹介します。(プロジェクトの規模に合わせて、適した方法を選びましょう)

まずはテンプレートを格納するフォルダを作成しましょう。

$ mkdir templates

続いて、テンプレートフォルダをdshack_project/settings.pyファイルに設定することで、プロジェクトに認識させる必要があります。

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')], # update here
        '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',
            ],
        },
    },
]

続いてテンプレート本体の作成ですが、ページの共通部分はbase.htmlにまとめることがベストプラクティスとされています。base.htmlはただの慣例です。

今回はHelloWorldの表示だけなので、特にbase.htmlを作成する必要はありませんが、サンプルとしてhome.htmlからbase.htmlを継承する例を以下に示します。

$ touch templates/base.html
$ touch templates/home.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>{% block title %}DsHack{% endblock title %}</title>
</head>
<body>
    {% block content %}
    {% endblock content %}
</body>
</html>

{% block title %}{% endblock title %}および{% block content %}{% endblock content %}で囲まれた部分が見た目のhtmlの可変部分です。

home.htmlから{% extends 'base.html' %}により、テンプレートを継承することができます。

{% extends 'base.html' %}

{% block title %}DsHack Top{% endblock title %}


{% block content %}
<h1>Django+Docker+pipenvでWebアプリを作成しよう</h1>
<p>HelloWorld!</p>
{% endblock content %}

Webページの確認

最後にローカルのWebブラウザで、HelloWorld!が表示出来ていることを確認しましょう。

$ python manage.py runserver

ブラウザでhttp://127.0.0.1:8000/ にアクセスします。

Djangoで頻繁に編集するのは、ここでサンプルとして挙げたsettings.py、urls.py、views.py、template群の他に、データベースモデルを扱うmodels.py、テストを実行するためのtests.pyがあります。

Djangoの詳しい記事はWebアプリケーションハンズオンを作成する際にかけたらなぁと考えてたりします。

Dockerを使ってWebアプリ開発

ここまででPythonの仮想環境を準備し、Djangoでhello worldを実行しました。

実際hello worldを作成するだけならDockerを使う必要性は皆無なのですが、Dockerを使った開発環境の構築の説明材料として単純なhello worldのアプリケーションを使います。

Dockerによる開発のメリット

Dockerを使うことのメリットは様々ですが、バージョン管理の観点から言えばPythonのライブラリに限らず、OSやミドルウェアのバージョンも揃えることができるので、複数人開発やデプロイ時のバージョンの相違による不具合が起きにくいのが大きな利点として挙げられるでしょう。

DjangoのデータベースはデフォルトでSQLiteを使用しています。本格的なアプリケーションを作成する際は、PostgreSQLやMySQLなど、より本格的なDBMSを利用することも多いです。

本記事では、Docker上にPostgreSQLを用意し、Djangoで利用するDBをSQLiteからPostgreSQLに変更する方法を紹介します。

Docker上でHelloWorld

この記事ではDocker自体についての詳しい説明は行いません。初心者の方は下記のQiita記事などを参考にしてみてください。

参考 いまさらDockerに入門したので分かりやすくまとめますQiita

Docker入門を終えたら、早速Dockerfileを作成しましょう。

FROM python:3.7

# バッファの無効化と.pycの生成無効化
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

WORKDIR /DsWebApp
COPY Pipfile Pipfile.lock /DsWebApp/
RUN pip install pipenv && pipenv install --system
COPY . /DsWebApp/

Dockerfileは必ずFROMから始まります。FROMによって、DockerHubなどのリポジトリに存在するベースイメージをインポートします。

今回はPython3.7をベースイメージとして利用します。続いて、ENV PYTHONDONTWRITEBYTECODE 1およびENV PYTHONUNBUFFERED 1で環境変数を設定しています。

この環境変数はそれぞれ.pycファイルの生成と、stdout/stderrのバッファリングを無効化するための設定です。続く4行がメインの処理になります。

WORKDIR /DsWebApp
COPY Pipfile Pipfile.lock /DsWebApp/
RUN pip install pipenv && pipenv install --system
COPY . /DsWebApp/

まずWORKDIRでコンテナ内のワーキングディレクトリを設定しています。

続いて、ホスト環境にあるPipfile, Pipfile.lockをコンテナ上にコピーしています。はじめのうちはコンテナ上の環境とホスト環境を混乱しがちなので、明確に意識しておきましょう。

続いて、RUNコマンドでコピーを行ったPipfile、Pipfile.lockを基にDocker内にローカルの仮想環境を再現するために必要なパッケージのインストールを行います。

ここで注意が必要なのは、Dockerはコンテナは既に仮想環境であり、pipenvの仮想環境を利用する必要がないことです。そのため、pipenv install --systemによりコンテナ全体でパッケージを利用可能にしています。

そして、最後にDjangoプロジェクト全体をコンテナ上にコピーします。

Dockerfileの作成が完了したらDockerイメージのビルドを行いましょう。

$ docker image build .

docker image lsを実行して、以下のように表示されればイメージのビルドに成功しています。

ここまで来ればあと一息です。docker-compose.ymlを使うことで、各コンテナの起動・停止、コマンドの実行などが容易に記述できます。

また、volumesオプションを使って、ホスト環境のファイルシステムとコンテナ上のファイルシステムをマウント(共有)することができます。

コンテナは破棄されると、そのコンテナの中身は全て失われるので残したい情報がある場合はこの機能を使います。

version: '3.7'


services:
  web:
    build: .
    command: python /DsWebApp/manage.py runserver 0.0.0.0:8000
    volumes:
      - .:/DsWebApp
    ports:
      - 8000:8000

docker-compose up -dでコンテナの起動を行います。-dオプションはよく使用するオプションで、このオプションによりコンテナはバックグラウンドで動作するため実行後もターミナルを利用することができます。

試しに-dなしでも実行してみてください。

最後にコンテナが起動されている状態でhttp://127.0.0.1:8000/ にアクセスしましょう。すると再びHelloWorldが表示されるはずです。

dockerコンテナを停止する際は、docker-compose stop、破棄する際は、docker-compose downが利用できます。

コンテナを停止しても破棄するまでは、コンテナは生存していることに注意しましょう。

Django+DockerでPostgreSQLを使う

Dockerを利用すれば自身のホスト環境にPostgreSQLをインストールしなくても、お手軽にPostgreSQLを利用することができます。

そのためにDocker側で必要な作業はdocker-compose.ymlにたった4行加えるだけです。

version: '3.7'


services:
  web:
    build: .
    command: python /DsWebApp/manage.py runserver 0.0.0.0:8000
    volumes:
      - .:/DsWebApp
    ports:
      - 8000:8000
    depends_on:
      - db
  db:
    image: postgres:11

servicesにdbが加わっています。serviceはざっくりいうとアプリの単位で、web側ではDockerfileを基にイメージをビルドしましたが、db側では公式のpostgres:11イメージを利用しています。

docker-compose up -dにより再びコンテナを起動しましょう。docker container lsで起動中のコンテナを表示してみてください。

webとdb、2つのコンテナが起動されていることがわかります。単純なシステムであればこのようにdocker-compose.ymlに複数のコンテナの依存関係を記述することで、コンテナの協調を行うことができます。

今回はdocker-compose down行わずに、Django側の設定に進みましょう。後で、起動中のコンテナの中でコマンドの実行を行います。

Docker側はこれで完了ですが、まだこれで終わりではありません。DjangoはデフォルトでSQLiteを使用しています。データベースの設定もやはり、settings.pyファイルの中に含まれています。

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}

デフォルトでPostgreSQLを利用するには、以下のように書き換えます。ポートはデフォルトで5432が使用されますが、docker-compose.yml上にports:オプションを追加して5432以外のポートを使うことも可能です。

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'postgres',
        'USER': 'postgres',
        'PASSWORD': 'postgres',
        'HOST': 'db',
        'PORT': 5432
    }
}

続いてデータベースアダプターをインストールする必要があります。ここでは、psycopgをインストールします。

起動中のdockerで、コマンドを実行するにはdocker-compose execコマンドを利用します。

$ docker-compose exec web pipenv install psycopg2-binary==2.8.3

このように開発を行う上で必要なパッケージは随時追加されていくと思います。この時ローカルよりもコンテナ上でパッケージをインストールするようにすれば、複数人・複数環境で開発する場合にもOSの差異に影響され辛くなります。

ディスクのマウントを行っているので、更新されたPipfileおよびPipfile.lockはホストのディスク上にも反映されています。

Dockerを利用するのであれば、パッケージのインストールはDocker上で実施しましょう

エラーが起きた場合は、PipfileのrequiresにあるPythonバージョンがコンテナのバージョンに一致していないことが原因と考えられるので、”3″または”3.7″に書き換えましょう。

Dockerは自動的にキャッシュ機能が働くので、パッケージをインストールするなどの変更を加えた際は必ずリビルドを行いましょう。

$ docker-compose down
$ docker-compose up -d --build

いよいよ最後になりますが、Djangoプロジェクトを作成した際にデータベースをmigrateしたのを覚えているでしょうか。

データベース自体が変更された場合も当然マイグレーションを実行しなくては変更は反映されておらず、エラーとなります。以下によりデータベースのマイグレーションを実行しましょう。(よく忘れます。。)

$ docker-compose exec web python manage.py migrate
https://datascientist-toolbox.com/wp-content/uploads/2019/09/man.png
りーぐる

ちなみに実際の開発では、カスタムユーザーモデルを作成する前にマイグレーションを行うとデフォルトのユーザーモデルから変更するのに非常に苦労するので注意しましょう!

データベースの設定が上手く出来ているか確認するために、Djangoの管理者画面を表示します。その前にsuperuserの作成を行いましょう。

$ docker-compose exec web python manage.py createsuperuser

適当にユーザー名とEmailアドレス、パスワードを設定して、http://127.0.0.1:8000/admin/より管理画面にログインします。

もはやPostgreSQLの導入が完了しました。

本記事では、はじめにpipenvでPythonの仮想環境を作成し、DjangoでHelloWorldアプリを作成しました。Dockerコンテナ上にpipenvで作成した仮想環境を再現し、HelloWorldアプリをコンテナ上で動作させました。

また、Docker上でパッケージをインストールする方法を紹介し、Dockerでパッケージをインストールすることで、OSやミドルウェアの差異によるバージョン問題が減らせることを強調しました。

https://datascientist-toolbox.com/wp-content/uploads/2019/09/man.png
りーぐる

この記事書いてたら、HelloWorldアプリ作るのにめっちゃ苦労した感がでてきた・・w