Nginx + Nuxt.js + Django REST frameworkの実装

# 初めに
[前回投稿](https://qiita.com/ryomatube/items/1b36fe6d73b9a6c3468c)の続きで、Nuxt.js + Django REST frameworkで動くアプリケーションをnginxに乗せていきます。

###構成イメージ
下記4つのコンテナがあります。
前回投稿からnginxコンテナを加えています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386475/4402b967-c637-5d9a-78c8-bcb5c77e6625.png)

**・初回ページ表示**
①nginx経由でnuxtコンテナにアクセス
②nuxtコンテナでSSRする。この際APIを叩きdjangoからDBの情報を取得する。
③レンダリングしたページをユーザに返す
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386475/6c6c3c4c-1c78-9666-0eb5-484031b05ff1.png)

**・ページ中でAPIを叩いた場合**
nginx経由でdjangoコンテナにアクセス、結果をユーザに返す。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386475/6e2bcb1d-f9d0-ad55-c05c-2aa702d29fbf.png)


###環境
Window10 Pro
Docker desktop v2.1.0.5
Python 3.6.9
django 2.2.7
djangorestframework 3.10.3
Nuxt.js v2.11.0
nginx 1.13.12

###ディレクトリ構造

```
.
├─django
│  ├─manage.py
│  ├─composeexample
│  │   ├─settings.py
│  │   ├─urls.py
│  │   ├─wsgi.py
│  │   └─__init__.py
│  └─myapp
│      ├─migrations
│      ├─admin.py
│      ├─apps.py
│      ├─models.py
│      ├─renderers.py
│      ├─serializers.py
│      ├─tests.py
│      ├─urls.py
│      ├─views.py
│      └─__init__.py
│
├─docker-compose.yml
│
├─dockerfiles
│  ├─django_docker
│  │   ├─dockerfile
│  │   └─requirements.txt
│  └─nuxt_docker
│      └─dockerfile
│
├─mysql
│  └─conf.d
│
├─nginx
│  ├─uwsgi_params
│  └─conf
│      └─app_nginx.conf
│
└─nuxt
    └─front
       └─以下略
```




#前回からの変更点
前回投稿ではSPAで設定していましたが、今回はSSRでの設定で進めます。

```javascript:./nuxt/front/nuxt.config.js
export default {
  mode: 'universal', //SSRに変更
```

#nginxの構築
##docker-compose.ymlの編集
docker-compose.ymlにnginxの設定を追加していきます。
80番ポートを割り当てます。

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

services:
  db:
    image: mysql:latest
    restart: always
    container_name: mysql
    environment:
      MYSQL_ROOT_PASSWORD: test
      MYSQL_USER: test
      MYSQL_DATABASE: test
      MYSQL_PASSWORD: test
    ports:
      - 3306:3306
    expose:
      - 3306
    volumes:
      - mysqldata:/var/lib/mysql
      - ./mysql/conf.d:/etc/mysql/conf.d

  web:
    container_name: django
    build: ./dockerfiles/django_docker
    command: 
      python3 manage.py runserver 0.0.0.0:8000
    volumes:
      - ./django:/code
    ports:
      - "8000:8000"
    depends_on:
      - db

  front:
    container_name: nuxt
    build: ./dockerfiles/nuxt_docker 
    tty: true
    volumes:
      - ./nuxt:/code
    ports:
      - "3000:3000"

#ここから

  nginx:
    image: nginx:1.13
    container_name: nginx
    ports:
      - "80:80"

#ここまで

volumes:
  mysqldata:
```

ここで一度nginxを立ち上げてみます。
docker psにてUpしていることを確認します。

```bash:
> docker-compose up -d nginx

> docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS                                                                      NAMES
aa3fe475db52        nginx:1.13          "nginx -g 'daemon of…"   About a minute ago   Up About a minute   0.0.0.0:80->80/tcp                                                         nginx
```

http://localhost/ にアクセスして下記画面が表示されれば成功です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386475/fc39db15-1036-d1a5-4cd4-a9bb69a2c9c0.png)

#Django側の変更
##nginx設定ファイルの追加
下記ディレクトリ及びファイルを追加していきます。

```
.
└─nginx
　  ├─uwsgi_params
　  └─conf
　      └─app_nginx.conf
```
以下はuWSGIを使用するための設定です。

```: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;
```

以下はnginx用の設定です。
パスによって宛先を分けていて、/api及び/adminへの通信はDjangoコンテナの8000番ポートに向かうように、
それ以外はnuxtのコンテナの3000番ポートにHTTPで向かうように設定しました。
[参考](https://qiita.com/yasunori/items/64606e63b36b396cf695)

```conf:./nginx/conf/app_nginx.conf
upstream django {
    ip_hash;
    server web:8000;
}

server {
    listen      80;
    server_name 127.0.0.1;
    charset     utf-8;

    location / {
        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-Proto  $scheme;
        proxy_read_timeout          1m;
        proxy_connect_timeout       1m;
        proxy_pass                          http://nuxt:3000; 
    }

    location ~* /api|admin/ {
        uwsgi_pass  django;
        include     /etc/nginx/uwsgi_params;
    }
}

server_tokens off;
```

##uWSGIの導入
Django側にuWSGIを導入するために設定を変更していきます。
requirement.txtにuwsgiを追加します。

```txt:./dockerfiles/django_docker/requirement.txt
Django>=2.0,<3.0
djangorestframework
django-webpack-loader
django-cors-headers
mysqlclient
uwsgi #追加
```
docker-compose.ymlを編集します。
djangoの起動コマンドを開発用のrunserverからuwsgiに切り替えます。
--socket :8000 --http :8001
としているのは、nginx経由でアクセスする時は8000番にWSGIで、
Nuxtからアクセスする時は8001番にHTTPでアクセスできるようにするためです。

また先ほどの設定ファイルをnginxコンテナから参照できるようにします。

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

services:
  db:
    image: mysql:latest
    restart: always
    container_name: mysql
    environment:
      MYSQL_ROOT_PASSWORD: test
      MYSQL_USER: test
      MYSQL_DATABASE: test
      MYSQL_PASSWORD: test
    ports:
      - 3306:3306
    expose:
      - 3306
    volumes:
      - mysqldata:/var/lib/mysql
      - ./mysql/conf.d:/etc/mysql/conf.d

  web:
    container_name: django
    build: ./dockerfiles/django_docker
    ####runserverからuwsgiコマンドに変更
    command: 
      uwsgi --socket :8000 --http :8001  --module config.wsgi --py-autoreload 1 -b 32768
    ####
    volumes:
      - ./django:/code
    ports:
      - "8000:8000"
    depends_on:
      - db

  front:
    container_name: nuxt
    build: ./dockerfiles/nuxt_docker 
    tty: true
    ####nuxtの起動コマンドを追加
    command: >
      bash -c 'cd front &&
      yarn dev'
    ####
    volumes:
      - ./nuxt:/code
    ports:
      - "3000:3000"

  nginx:
    image: nginx:1.13
    container_name: nginx
    ports:
      - "80:80"
    ####以下追加
    volumes:
      - ./nginx/conf:/etc/nginx/conf.d
      - ./nginx/uwsgi_params:/etc/nginx/uwsgi_params
    depends_on:
      - web
    ####

volumes:
  mysqldata:
```

setting.pyのALLOWED_HOSTSに下記を追加します。

```python:./django/composeexample/setting.py
ALLOWED_HOSTS = ["localhost","django"]
```

##Nuxt修正
前回投稿時はSPAにて作成していましたが、SSRさせるためにmountedをasync dataに変更します。

```vue:./nuxt/front/pages/index.vue
//~略~

<script>
import Logo from '~/components/Logo.vue'
import axios from 'axios' //追記

export default {
  components: {
    Logo
  },

//ここから
  data() {
    return {
      dat: []
    }
  },
  async mounted(){
    const url = "/api/get_person/" 
    const response = await this.$axios.get(url)
    this.dat = response.data
  }
//ここまで

//↓↓↓に変更

//ここから
  async asyncData({ $axios }){
    const url = "api/get_person/"
    const response = await $axios.get(url)   
    return { dat: response.data}
//ここまで
}
</script>
//~略~
```

またnuxt.conf.jsも修正します。
axiosのbaseURLをhttp://django:8001 に、browserBaseURLをhttp://localhost に変更します。
baseURLはNuxtコンテナからSSR時(サーバ側)にAPIを叩くときに使用し、
browserBaseURLはブラウザ(クライアント側)でAPIを叩くときに使用します。
NuxtでAPIを叩くときはDjangoに直接向かうようにし、
ブラウザの場合はNginxに向かうようにします。

```javascript:./nuxt/front/nuxt.config.js
export default {
//~略~
  axios: {
    baseURL: "http://django:8001",
    browserBaseURL: "http://localhost",
  },
//~略~
```

この状態でビルドします。
4つコンテナが立ち上がれば成功です。

```bash:
> docker-compose up -d --build      

> docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                                                                      NAMES
400297780ac2        qiita1_web          "uwsgi --socket :800…"   9 minutes ago       Up 9 minutes        0.0.0.0:8000->8000/tcp                                                     django
a7ca824f3526        nginx:1.13          "nginx -g 'daemon of…"   37 minutes ago      Up 37 minutes       0.0.0.0:80->80/tcp                                                         nginx
c96258eee9b0        qiita1_front        "docker-entrypoint.s…"   37 minutes ago      Up 37 minutes       0.0.0.0:3000->3000/tcp                                                     nuxt
58377df67b22        mysql:latest        "docker-entrypoint.s…"   37 minutes ago      Up 37 minutes       0.0.0.0:3306->3306/tcp, 33060/tcp                                          mysql                                            
```



http://localhost にアクセスして、TOPページが表示されればnginx経由でアクセスできています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386475/b0ce3797-126e-5ca3-7b27-41257cfe1a58.png)

http://localhost/api/get_person でnginx経由でAPIの結果も取得できます。

```bash:
> curl http://localhost/api/get_person 


StatusCode        : 200
StatusDescription : OK
Content           : {"person_data": [{"id": 1, "person_name": "takashi", "person_age": 26}, {"id": 2, "pe 
                    rson_name": "naoto", "person_age": 32}, {"id": 3, "person_name": "tomoko", "person_ag
                    e": 15}]}
RawContent        : HTTP/1.1 200 OK
                    Connection: keep-alive
                    Allow: GET, HEAD, OPTIONS
                    X-Frame-Options: SAMEORIGIN
                    Vary: Cookie, Origin
                    Content-Length: 179
                    Content-Type: application/json; charset=utf-8
                    Date: Fri, 08...
Forms             : {}
Headers           : {[Connection, keep-alive], [Allow, GET, HEAD, OPTIONS], [X-Frame-Options, SAMEORIGIN] 
                    , [Vary, Cookie, Origin]...}
Images            : {}
InputFields       : {}
Links             : {}
ParsedHtml        : System.__ComObject
RawContentLength  : 179                                        mysql                                            
```

これにて初期表示の部分は完成です。

##API編集
続いてAPI部分を作成していきます。
前回投稿時はGETのエンドポイントしか作成しなかったので、POSTのエンドポイントも作成します。
urls.py及びview.pyを編集していきます。

```python:./django/myapp/views.py
from django.shortcuts import render
from django.views.generic import ListView

from myapp.models import Person_data

from rest_framework import status
from rest_framework.generics import ListAPIView, CreateAPIView #追加
from rest_framework.permissions import AllowAny

from .renderers import PersonJSONRenderer
from .serializers import PersonSerializer

# Create your views here.

class PersonListApiView(ListAPIView):
    model = Person_data # モデルを指定
    queryset = Person_data.objects.all()
    permission_classes = (AllowAny, )
    renderer_classes = (PersonJSONRenderer, ) 
    serializer_class = PersonSerializer 

####以下追加
class PersonCreateApiView(CreateAPIView):
    model = Person_data
    queryset = Person_data.objects.all()
    permission_classes = (AllowAny, )
    renderer_classes = (PersonJSONRenderer, )
    serializer_class = PersonSerializer
    

```

```python:./django/myapp/urls.py
from django.urls import path
from .views import PersonListApiView, PersonCreateApiView #追加


urlpatterns = [
    path('get_person/', PersonListApiView.as_view()),
    path('post_person/', PersonCreateApiView.as_view()), #POST追加
]
```

これだけでAPIを追加できます。
POSTして201が返ることを確認します。
(windows power shellの例です)

```bash:
> curl -Method POST -Body @{person_name="takaishi"; person_age=11;} http://localhost/api/post_person/


StatusCode        : 201
StatusDescription : Created
Content           : {"person_data": {"id": 18, "person_name": "takaishi", "person_age": 11}}   
RawContent        : HTTP/1.1 201 Created
                    Connection: keep-alive
                    Allow: POST, OPTIONS
                    X-Frame-Options: SAMEORIGIN
                    Vary: Cookie, Origin
                    Content-Length: 72
                    Content-Type: application/json; charset=utf-8
                    Date: Fri, 08 ...
Forms             : {}
Headers           : {[Connection, keep-alive], [Allow, POST, OPTIONS], [X-Frame-Options, SAMEO 
                    RIGIN], [Vary, Cookie, Origin]...}
Images            : {}
InputFields       : {}
Links             : {}
ParsedHtml        : System.__ComObject
RawContentLength  : 72
```
TOPページを更新すると追加されていることを確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386475/9dd8cb09-4e0a-0bd3-0f9d-1bc544a320d8.png)

##TOPページにフォームを作る
Webページ上からPOSTできるように簡単なフォームを作成します。

```vue:./nuxt/front/pages/index.vue
<template>
  <div class="container">
    <div>
      <logo />
      <h1 class="title">
        front
      </h1>
      <h2 class="subtitle">
        My doozie Nuxt.js project
      </h2>
      <div class="links">
        <a
          href="https://nuxtjs.org/"
          target="_blank"
          class="button--green"
        >
          Documentation
        </a>
        <a
          href="https://github.com/nuxt/nuxt.js"
          target="_blank"
          class="button--grey"
        >
          GitHub
        </a>

        <div v-for="d in dat.person_data" :key=d.person_name align="center">
          <h2>
            {{d.person_name}} {{d.person_age}} 
          </h2>
        </div>

<!-- ここから追記 -->

        <input type="text" v-model="name">
        <input type="text" v-model="age">
        <button v-on:click="post">ADD</button>

<!-- ここまで追記 -->

      </div>
    </div>
  </div>
</template>

<script>
import Logo from '~/components/Logo.vue'

export default {
  components: {
    Logo
  },

//ここから追記
  data() {
    return {
      name: '',
      age: '',
    }
  },
  methods: {
    post () {
        const url = "api/post_person/"
        axios.defaults.xsrfCookieName = 'csrftoken'
        axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"    
        axios.post(url, {person_name: this.name , person_age: this.age})
    }
  },
//ここまで追記

  async asyncData({ $axios }){
    const url = "api/get_person/"
    const response = await $axios.get(url)   
    return { dat: response.data}
  }
}
</script>
//~略~
```

これでTOPページからPOSTできます。
入力して、ADDボタンを押してPOST送信できます。
ページ更新することで再度レンダリングが走り、表示が更新されます。

![Microsoft-Edge-2020-05-09-06-14-40.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386475/5497eb5b-8c70-9abc-db34-7a053d57fb57.gif)


##参考
[Django+Nginx+MySQLの開発環境をDockerで構築する](https://qiita.com/kenkono/items/6221ad12670d1ae8b1dd)
[ちゃんと運用するときのuWSGI設定メモ](https://qiita.com/yasunori/items/64606e63b36b396cf695)
[EC2上で Django + Nginx + uWSGI を試す](https://qiita.com/melos/items/91dc3b45a589ea5a369b)