Vue.js & Django を Docker と組み合わせてSPA+APIサーバー環境をつくる

## はじめに

今回は、Django REST Frameworkを使った、
Vue.jsをSPAとして運用できる環境の作り方をシェアできればと思います。

Docker Composeを使うので、本番で使うときはGKEなどの
モダンなコンテナ型オーケストレーションツールを選択可能かつ、開発でも
メンバーの増員などに柔軟に対応できる環境を作れたらと思います。

今回は抜粋で書いているので、
不足等ございましたら、ご連絡よろしくお願いいたします。

## 構成

構成は下記のような形にします。
本来だと下記に＋DBもあるような構成が基本ですが、
コンテナ上で動かす場合と、クラウドのDBサービスを使用する場合など
多岐の選択があるため、今回はなしとします。

### サーバー構成

| サーバー名       | 名称 | ポート番号 |
|:-----|:-------------------------------|:-----|
| 開発サーバー(Vue.js)    | front | 8080 |
| APIサーバー(Django)    | back | 8001 |
| Webサーバー(Nginx)    | web | 8000 |


## Docker Compose ファイルで骨組みを作る

最初にdocker-compose.ymlファイルと各種サーバーのDockerfileを作成して、
骨組みを形作っていきます。


### 1. docker-compose.ymlを作成

注意点としては、バックエンド側のstaticファイルをnginxに置いて
配信可能とする点です。
今回はAPIサーバーとしての使用のため、不要と思われますが、
djangoのデバッグ画面（管理画面など）を表示する際に使用できるので、
volumesにて、同期します。
基本的に起動は、拡張していくことも考えて、
Shellファイルに落とし込んでいきます。
（直で記載でも良いとは思います。）

```yml:docker-compose.yml
version: '3'

services:

  web:
    build:
      context: ./
      dockerfile: ./web/Dockerfile
    environment:
      TZ: 'Asia/Tokyo'
    ports:
      - 8000:8000
    volumes:
      - ./web/logs/nginx/:/var/log/nginx/
      - ./web/uwsgi_params:/etc/nginx/uwsgi_params
      - ./back/static:/var/www/static/
    depends_on:
      - back

  back:
    build:
      context: ./back
      dockerfile: Dockerfile
    command: 'sh /server/start.sh'
    expose:
      - "8001"
    volumes:
      - ./back:/server/

  front:
    build:
      context: ./front
    command: 'sh /app/start.sh'
    volumes:
      - ./front:/app/:cached
      - ./front/node_modules:/app/node_modules
    ports:
      - "8080:8080"
```

### 2. DockerFile フロントエンド側を作成

npmを使用するために、nodeイメージを入れます。
後ほど立ち上げるためにコメントアウトなども挟むので、
一旦スキップしても構いません。

```docker:Dockerfile(フロントエンド)

FROM node:10.7.0

WORKDIR /app
RUN npm install -g @vue/cli
ADD ./package.json /app/package.json
RUN npm install
ADD ./start.sh /app/start.sh

```

### 3. DockerFile バックエンド側を作成

バックエンド側をゴリゴリ書いていきます。
ライブラリのインストールも都度できるように
requirements.txt経由でインストールします。

```docker:Dockerfile(バックエンド)

FROM python:3.7
ENV PYTHONUNBUFFERED 1

WORKDIR /server
ADD . /server/
RUN pip install --upgrade pip
RUN pip install -r requirements.txt

```

### 4. DockerFile Webサーバー側を作成

設定ファイル、Vue.jsで作成しビルドしたものを格納します。
バージョンはお好みのものをご使用ください。

```docker:Dockerfile(Webサーバー)

FROM nginx:1.11.7

# 設定ファイル
ADD ./web/nginx.conf /etc/nginx/nginx.conf
ADD ./web/default.conf /etc/nginx/sites-available/default
ADD ./web/default.conf /etc/nginx/sites-enabled/default
ADD ./web/uwsgi_params /etc/nginx/uwsgi_params

RUN mkdir /var/www
RUN mkdir /var/www/front
RUN mkdir /var/www/static

```

ここまで作成するとある程度骨組み部分は出来上がった状態になります。
ここから、実際にサーバーとして使用できるように
各サーバー内に手を加えて初期構築をしていきます。

## フロントエンド側を作成

Vue.jsを使えるようにVue CLIを導入して、
使用できる状態にしていきます。
エラーを防ぐために一旦Dockerfileで書いた下記をコメントアウトします

```docker
FROM node:10.7.0

WORKDIR /app
RUN npm install -g @vue/cli
# ADD ./package.json /app/package.json
# RUN npm install
# ADD ./start.sh /app/start.sh
```

Vue CLIを入れることで、Vueの開発環境を簡単に作成することができます。
早速Terminalからプロジェクトを作成し、開発サーバーとして立ち上げてみましょう。
各種設定方法を聞かれますが、enterで進んでいくとデフォルトのものがインストールされます。

```terminal:ターミナル
// hoge-projectにはプロジェクト名を入力してください。
$ docker-compose run front vue create hoge-project
```

先ほどDockerfileに入力したコメントアウトを外して、
start.shを軽く記載した後、立ち上げてみましょう。
（プロジェクト先に向き先を当てるため、ファイルの向き先も変更するか、
　プロジェクトフォルダを移動して、動くか確認してみてください。）

```shell:start.sh
npm run serve
```

```terminal:ターミナル
$ docker-compose build front
$ docker-compose up -d front
```

ローカルホストで確認すると無事立ち上がっているかと思います。



## バックエンド側を作成

次にDjango側を作成していきます。
ターミナルからDjangoアプリの初期作成コマンドを打ちます。

```terminal:ターミナル
$ docker-compose exec back django-admin startproject mysite
```

次にアプリケーションを作成します。
最後の確認の際に使います。

```terminal:ターミナル
$ docker-compose exec back python mysite/manage.py startapp testapi
```

起動用のシェルも書いておきます。

```shell:start.sh
#!/bin/bash
sleep 5
python manage.py makemigrations
python manage.py migrate
python manage.py collectstatic --noinput
uwsgi --socket :8001 --module mysite.wsgi
```

Django REST Frameworkを使用するので
インストールする設定ファイルに記載します。

```txt:requirements.txt
django>=2.1.10
uwsgi==2.0.17.1
djangorestframework==3.8.2
```

一旦ここまでで、最後に疎通確認するので、
次に進みます。

## Webサーバーを立てる

nginx.conf, default.conf, uwsgi_paramsなどの
設定ファイルを作成していきます。

### 1. nginx.conf

```conf:nginx.conf

user www-data;
worker_processes auto;
pid /run/nginx.pid;
daemon off;

events {
	worker_connections 65535;
	multi_accept on;
	use epoll;
}

http {

	sendfile on;
	tcp_nopush on;
	tcp_nodelay on;
	types_hash_max_size 2048;
    client_max_body_size 20M;
    keepalive_timeout     3600;
    proxy_connect_timeout 3600;
    proxy_send_timeout    3600;
    proxy_read_timeout    3600;
    send_timeout          3600;
	client_body_timeout   300;

	include /etc/nginx/mime.types;
	default_type application/octet-stream;

	log_format with_time '$remote_addr - $remote_user [$time_local] '
                         '"$request" $status $body_bytes_sent '
                         '"$http_referer" "$http_user_agent" $request_time';

	access_log /dev/stdout with_time;
	error_log stderr;

	gzip on;
	gzip_disable "msie6";

	include /etc/nginx/conf.d/*.conf;
	include /etc/nginx/sites-enabled/*;

	limit_req_zone $binary_remote_addr zone=perip:10m rate=5r/s;
	limit_req_status 429;
}
```

### 2. default.conf

```conf:default.conf

# development
upstream webserver {
  ip_hash;
  server back:8001;
}

map $http_upgrade $connection_upgrade {
  default upgrade;
  '' close;
}

server {
  listen 8000;
  server_name 127.0.0.1;

  client_header_buffer_size 1k;
  large_client_header_buffers 8 32k;

  add_header Strict-Transport-Security 'max-age=31536000';
  add_header X-Frame-Options DENY;
  add_header X-XSS-Protection "1; mode=block";

  error_page 500 502 503 504 /50x.html;

  # フロントエンド
  location / {
    root /var/www/front;
    try_files $uri $uri/ /index.html;
  }

  # バックエンドサーバー 静的ファイル群
  location /static {
    alias /var/www/static;
  }

  # バックエンドサーバー
  location /back/ {
    include /etc/nginx/uwsgi_params;
    uwsgi_pass webserver;
    proxy_redirect     off;
    proxy_set_header   Host $host;
    proxy_set_header   X-Real-IP $remote_addr;
    proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header   X-Forwarded-Host $server_name;
    proxy_read_timeout 86400s;
    proxy_send_timeout 86400s;
  }

  # バックエンド adminサーバー
  location /admin/ {
    include /etc/nginx/uwsgi_params;
    uwsgi_pass webserver;
    proxy_redirect     off;
    proxy_set_header   Host $host;
    proxy_set_header   X-Real-IP $remote_addr;
    proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header   X-Forwarded-Host $server_name;
  }

  location = /50x.html {
    root /usr/share/nginx/html;
  }
}
```

### 3. uwsgi_params

```conf:uwsgi_params

uwsgi_param  QUERY_STRING       $query_string;
uwsgi_param  REQUEST_METHOD     $request_method;
uwsgi_param  CONTENT_TYPE       $content_type;
uwsgi_param  CONTENT_LENGTH     $content_length;

uwsgi_param  REQUEST_URI        $request_uri;
uwsgi_param  PATH_INFO          $document_uri;
uwsgi_param  DOCUMENT_ROOT      $document_root;
uwsgi_param  SERVER_PROTOCOL    $server_protocol;
uwsgi_param  REQUEST_SCHEME     $scheme;
uwsgi_param  HTTPS              $https if_not_empty;

uwsgi_param  REMOTE_ADDR        $remote_addr;
uwsgi_param  REMOTE_PORT        $remote_port;
uwsgi_param  SERVER_PORT        $server_port;
uwsgi_param  SERVER_NAME        $server_name;
```


## 問題ないかテスト

試しにVue.js側でAxiosを導入して、
リクエストを送ってみて、きちんとAPIサーバーから値が帰ってくるか、
確認してみます。

### 1. Vue.js側のAxios通信の作成

試しにボタンからAPIサーバーへリクエストを送る処理を書いていきます。
戻り値を表示する部分も用意します。

```terminal:ターミナル
$ docker-compose run front npm install -S axios
```

インストールしたAxiosをmain.jsにセッティングします。

```js:main.js
import Vue from 'vue'
import App from './App.vue'
import axios from 'axios'

Vue.config.productionTip = false
Vue.prototype.$axios = axios
new Vue({
  render: h => h(App),
}).$mount('#app')
```

初期作成されたHelloWorld.vueに追記していきます。

```vue:HelloWorld.vue
<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <!-- 下記記載 -->
    <h2>ここに結果が表示されます → {{ result }}</h2>
    <button @click="getAPI()">クリック！</button>
    <p>
      For a guide and recipes on how to configure / customize this project,<br>
      check out the
      <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
    </p>

<!-- ~~~ 割愛 ~~~ -->
</template>

<script>
export default {
  name: 'HelloWorld',
  props: {
    msg: String
  },
  // 下記記載
  data () {
    return {
      result: 'No Result',
      url: 'http://localhost:8000/back/testapi/get/'
    }
  },
  methods: {
    getAPI () {
      this.$axios.get(this.url)
      .then(response => {
        this.result = response.data.message
      })
    }
  }
}
</script>
```

### 2. Django側のレスポンス処理の作成

URLConfへの記載、Views.pyにてレスポンス処理を書いていきます。
今回はhello worldを返すようにします。

```py:root側のurls.py
from django.contrib import admin
from django.urls import path

urlpatterns = [
    path('admin/', admin.site.urls),
    path('back/testapi', include('testapi.urls', namespace='testapi')),
]
```

```py:App側のurls.py
from django.urls import include, path
from .views import *

app_name = 'testapi'

urlpatterns = [
    path('get/', GetTestAPI.as_view()),
]
```

```py:views.py
from django.shortcuts import render
from rest_framework import status, viewsets, filters
from rest_framework import permissions
from rest_framework.response import Response

class GetTestAPI(APIView):
    permission_classes = (permissions.AllowAny,)

    def get(self, request, format=None):
        return Response(data={'status': 'Hello World!'}, status=status.HTTP_200_OK)
```

```py:settings.py

# ~~ 省略 ~~

# Application definition

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

# ~~ 省略 ~~

```

### 3. 疎通

サーバーを立ち上げてブラウザ上から確認してみます。

```
$ docker-compose up -d
```

## まとめ

現在SPA＋APIサーバー環境をDocker上で構築することで、
短い時間で、簡単に構築することができます。

現在弊社では、HRモンスターと呼ばれる
採用の新しいスタイルを提供するサービスをローンチいたしました。

ローンチ後のさらなる機能追加、改善などのPDCAサイクルを回すべく、
エンジニアを募集しております。
https://www.wantedly.com/projects/341182

Kubernetes、Vue.js（Javascript）、Django（Python）といったモダンな技術を使って、
開発しておりますので、もしご興味がある方はぜひ、ご応募お待ちしております。