DockerでDjango+Vue+NginxのSPAを作る

Docker composeの復習がてらSPAサイトの雛形コンテナ構成を最速ビルドしてみる

可能な限り初期ファイル構成はシンプルかつ最小限で。

## ディレクトリ＆コンテナ構成

```
docker_spa/
  ┣━ back/
  ┃  ┣━ scripts/
  ┃  ┃  ┣━ django-appstart.sh  # Django新規アプリ作成用スクリプト
  ┃  ┃  ┣━ django-settings.sh  # Djangoセットアップ用スクリプト
  ┃  ┃  ┗━ start.sh  # コンテナ起動関連スクリプト
  ┃  ┣━ Dockerfile  # バックエンド用Dockerfile
  ┃  ┗━ requirements.txt   # pythonパッケージ一覧
  ┣━ front/
  ┃  ┣━ scripts/
  ┃  ┃  ┣━ start.sh  # 「settings.py」修正用スクリプト
  ┃  ┃  ┗━ vue-settings.sh  # vue用スクリプト
  ┃  ┗━ Dockerfile  # フロントエンド用Dockerfile
  ┣━ web/
  ┃  ┣━ conf/
  ┃  ┃  ┗━ app_nginx.conf  # アプリ用Nginx設定ファイル
  ┃  ┣━ logs/nginx
  ┃  ┃  ┗━ access.log  # アクセスログ
  ┃  ┃  ┗━ error.log  # エラーログ
  ┃  ┣━ Dockerfile  # webサーバー用Dockerfile
  ┃  ┗━ uwsgi_params  # アプリサーバー設定ファイル
  ┃   
  ┗━ docker-compose.yml

```

### docker-compose.yml
```docker-compose.yml
version: '3.7'
 
services:
  web:
    container_name: web-container
    build:
      context: ./
      dockerfile: ./web/Dockerfile
    restart: unless-stopped   # コンテナが異常停止した場合は再起動する
    ports:
      - "8000:8000"
    environment:
      TZ: "Asia/Tokyo"
    volumes:
      - ./web/conf:/etc/nginx/conf.d 
      - ./web/logs/nginx/:/var/log/nginx/
      - ./web/uwsgi_params:/etc/nginx/uwsgi_params
      - ./web/static:/static
      - django_statics:/var/www/vhosts/localhost/static:ro
    networks:
      - django_net
    depends_on:
      - back
 
  back:
    container_name: back-container
    build:
      context: ./back
      dockerfile: ./Dockerfile
    command: 'sh /code/scripts/start.sh'
    restart: unless-stopped
    volumes:
      - ./back:/code
      - ./back/app/static:/static
    expose:
      - "8001"
    networks:
      - django_net
 
  front:
    container_name: front-container
    build:
      context: ./front
      dockerfile: ./Dockerfile  # ./front/Dockerfileを参照
    ports:
      - 8080:8080
    expose:
      - "3000"
    command: 'sh /code/scripts/start.sh'
    volumes:
      - ./front:/code
      - ./static:/static
    networks:
      - django_net
    depends_on:
      - back
 
networks:
  django_net:
    driver: bridge
 
volumes:
  django_statics:
    driver: local
```

![docker-spa-image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230956/422a4b06-1a6a-faf1-560c-b21c31cee933.png)

詳細な導線は異なるところはあると思いますが、上記がComposeのコンテナ構成イメージです。

- ページアクセスのリクエストをwebサーバーコンテナ内のNginxがプロキシサーバーとして中継
- バックエンドコンテナ内のアプリサーバーのuWSGIでバックエンドとNginxとの疎通
- POSTリクエストをAPIサーバーとしてDjango（REST Framework）で処理
- Vueのaxiosのプロキシサーバーと連結してレスポンスオブジェクトをVueの非同期で受け取る

こんな流れで最小限のSPA環境をビルドします。

今回はwebアプリとしてVueからしか繋いでないのに無駄に複雑な構成になってますが、専用コンテナをcomposeに追加してそこからのリクエストをAPIサーバーに処理してもらってNginx経由でアクセスを振り分けてもらえば将来的にネイティブアプリやデスクトップアプリなどクロスプラットフォーム対応した際にもコンテナ同士の構造を意識しなくても良くなる・・・ハズ・・・。


## バックエンド側
APIサーバー処理用のバックエンド側のコンテナ構成(Dockerfile)

``` Dockerfile
FROM python:3.8
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
EXPOSE 8000
 
# 開発ディレクトリ設定
ENV WORKDIR /code
WORKDIR ${WORKDIR}
COPY ./scripts/ ${WORKDIR}/
 
# コンテナ内でパッケージを一括インストール
COPY requirements.txt ${WORKDIR}
RUN pip install --upgrade pip && pip install -r requirements.txt && \
pip install coreapi django-cors-headers python-jose
 
COPY . ${WORKDIR}/
```

### コンテナ起動関連スクリプト

- 初回ビルド時にappフォルダ＆新規プロジェクトを作成
- 同時に各種セットアップのスクリプトを実行
- migrationとuWSGIサーバーを起動（※2回目以降はここのみ実行）

```back/scripts/start.sh
#!/bin/bash
set 'echo -e -o pipefail'

unix_today=$(date +'%s')
unix_today=$((unix_today+32400))
jst_ymd_today=$(date '+%Y/%m/%d %H:%M:%S' --date "@$unix_today")

# メインプロジェクトフォルダが無ければセットアップ実行
if [ ! -d /code/app ]; then
    echo "${jst_ymd_today}｜プロジェクト初期化"
    mkdir app && cd app
    django-admin startproject config .

    # djangoセットアップ＆サーバー起動
    bash /code/scripts/django-settings.sh
    bash /code/scripts/django-appstart.sh main
    echo "${jst_ymd_today}｜初期設定完了"

    # python manage.py createsuperuser
else
    echo "${jst_ymd_today}｜コンテナ更新"
fi


cd app
python manage.py makemigrations
python manage.py migrate
# python manage.py collectstatic

# 静的ファイルディレクトリをstatic指定ディレクトリに集約コピー(--noinputで対話プロンプトを無視)
python manage.py collectstatic --noinput
# configアプリuWSGIに接続。「--py-autoreload 1」でファイル等に変更があった際は自動リロード
uwsgi --socket :8001 --module config.wsgi --py-autoreload 1 --logto /tmp/uwsgi.log

# python manage.py runserver 0.0.0.0:8001

```

### Djangoセットアップ用スクリプト
- 「config/settings.py」の各種セットアップ
    - タイムゾーンと言語を日本語対応
    - REST Framework設定を追記
    - staticディレクトリ設定
- 「config/url.py」のセットアップ
- 静的ファイル用「template」ディレクトリ作成


```back/scripts/django-settings.sh
#!/bin/sh
# シェル実行が失敗したら終了してエラー出力
set 'echo -e -o pipefail'
 
# ルートディレクトリ取得
ROOT_DIR=$(cd $(dirname $0)/..;pwd)
echo ${ROOT_DIR}
 
# 各種モジュールインポートを追記
if ! grep -q "import os" "${ROOT_DIR}/app/config/settings.py" ;then
    sed -i -e "s/from pathlib import Path/from pathlib import Path \nimport os\nimport json\nfrom six.moves.urllib import request\nfrom cryptography.x509 import load_pem_x509_certificate\nfrom cryptography.hazmat.backends import default_backend/" ${ROOT_DIR}/app/config/settings.py
fi
 
# BASE_DIR変更
sed -i -e "s/BASE_DIR = Path(__file__).resolve().parent.parent/BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))/" ${ROOT_DIR}/app/config/settings.py
 
# INSTALLED_APPSにrest_frameworkとrest_framework_jwtとcorsheadersを追加
if ! grep -q "'rest_framework'," "${ROOT_DIR}/app/config/settings.py" ;then
    sed -i -e "s/'django.contrib.staticfiles',/'django.contrib.staticfiles',\n    'rest_framework', \n    'rest_framework_jwt', \n    'corsheaders',/" ${ROOT_DIR}/app/config/settings.py
fi
 
# MIDDLEWAREにcorsheadersミドルウェアを追記
if ! grep -q "'corsheaders.middleware.CorsMiddleware'," "${ROOT_DIR}/app/config/settings.py" ;then
sed -i -e "s/'django.middleware.common.CommonMiddleware',/'corsheaders.middleware.CorsMiddleware', \n    'django.middleware.common.CommonMiddleware', \n/" ${ROOT_DIR}/app/config/settings.py
fi
 
# テンプレートディレクトリ設定
sed -i -e "s/'DIRS': \[\],/'DIRS': \[os.path.join(BASE_DIR, \"app\/templates\")\],/" ${ROOT_DIR}/app/config/settings.py
 
# 言語をjaに変更
sed -i -e "s/LANGUAGE_CODE = 'en-us'/LANGUAGE_CODE = 'ja'/" ${ROOT_DIR}/app/config/settings.py
 
# タイムゾーンをAsia/Tokyoに変更
sed -i -e "s/TIME_ZONE = 'UTC'/TIME_ZONE = 'Asia\/Tokyo'/" ${ROOT_DIR}/app/config/settings.py
 
# DATEBASEパス変更
sed -i -e "s/'NAME': BASE_DIR \/ 'db.sqlite3',/'NAME': BASE_DIR+'\/app\/db.sqlite3',/" ${ROOT_DIR}/app/config/settings.py
 
# ALLOWED_HOSTSにlocalhostと127.0.0.1追加
sed -i -e "s/ALLOWED_HOSTS = \[\]/ALLOWED_HOSTS = \[\n  '127.0.0.1', \n    'localhost', \]/" ${ROOT_DIR}/app/config/settings.py
 
# STATIC_ROOT追記
sed -i -e "s/STATIC_URL = '\/static\/'/STATIC_URL = '\/static\/'\nSTATIC_ROOT = os.path.join(BASE_DIR, 'static')/" ${ROOT_DIR}/app/config/settings.py
 
# REST_FRAMEWORK設定追記
if ! grep -q "REST_FRAMEWORK = {" "${ROOT_DIR}/app/config/settings.py" ;then
echo "# REST_FRAMEWORK設定
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
    ),
}
 
# クロスオリジン許可
CORS_ORIGIN_WHITELIST = (
    'http://localhost:8080',
)
" >> ${ROOT_DIR}/app/config/settings.py
fi
 
# importにincludeモジュールを追加＆napp_name設定
sed -i -e "s/from django.urls import path/from django.urls import path, include \nfrom rest_framework.documentation import include_docs_urls\n/" ${ROOT_DIR}/app/config/urls.py
# APIドキュメントルート追加
sed -i -e "s/path('admin\/', admin.site.urls),/path('admin\/', admin.site.urls),\n    path('docs\/', include_docs_urls(title='API Document')),\n/" ${ROOT_DIR}/app/urls.py
 
# templates＆staticフォルダ階層作成
mkdir ${ROOT_DIR}/app/templates
mkdir ${ROOT_DIR}/app/static && mkdir ${ROOT_DIR}/app/static/css ${ROOT_DIR}/app/static/js ${ROOT_DIR}/app/static/images
 
# base.html作成
cat > ${ROOT_DIR}/app/templates/base.html << "EOF"
<!DOCTYPE html>
{% load tz %}
{% load static %}
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}{% endblock %}</title>
    {% block style %}{% endblock %}
</head>
<body>
    {% if messages %}
    <ul class="pl-0 ml-3">
    {% for message in messages %}
        <li class="alert alert-{{ message.tags }}">{{ message }}<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <span aria-hidden="true">×</span> </button></li>
    {% endfor %}
    </ul>
    {% endif %}
    {% block content %}
    {% endblock %}
    {% block script %}{% endblock %}
</body>
</html>
EOF
 
# index.html作成
cat > ${ROOT_DIR}/app/templates/index.html << "EOF"
{% extends "base.html" %}
{% load static %}
{% block title %}ページタイトル{% endblock %}
{% block style %}
{% endblock %}
{% block breadcrumb %}
{% endblock %}
{% block content %}
インデックスページのコンテンツを表示中
{% endblock %}
{% block script %}
{% endblock %}
EOF
 
# template.html作成
cat > ${ROOT_DIR}/app/templates/template.html << EOF
{% extends "base.html" %}
{% load static %}
{% block title %}ページタイトル{% endblock %}
{% block style %}
{% endblock %}
{% block breadcrumb %}
{% endblock %}
{% block content %}
コンテンツテンプレートです
{% endblock %}
{% block script %}
{% endblock %}
EOF
 
```

### pythonパッケージ一覧

```back/requirements.txt
Django==3.2
psycopg2-binary>=2.8
scrapy-djangoitem==1.1.1
scrapy==2.4
scrapyd==1.2.0
djangorestframework==3.12.4
djangorestframework-jwt==1.11.0
django-cors-headers==3.7.0
django-webpack-loader==1.0.0
uwsgi==2.0.19.1
markdown==3.3.4
django-filter==2.4.0
Pygments==2.9.0
```

### 新規アプリ作成用スクリプト

``` back/scripts/django-appstart.sh
#!/bin/sh
# シェル実行が失敗したら終了してエラー出力
set -e -o pipefail
 
# 実行ディレクトリ取得
ROOT_DIR=$(cd $(dirname $0)/..;pwd)
echo $ROOT_DIR
 
# "新規アプリ作成"
# 新規ディレクトリ＆アプリ作成
mkdir $ROOT_DIR/app/${1}
django-admin startapp ${1} $ROOT_DIR/app/${1}
 
# INSTALLED_APPSに新規アプリを追加
sed -i -e "s/'rest_framework',/'rest_framework', \n    '${1}', /" ${ROOT_DIR}/app/config/settings.py
 
# websiteルーティング追加
sed -i -e "s/\]/    path('${1}\/', include('${1}.urls')),\n\]/" ${ROOT_DIR}/app/config/urls.py
 
 
# APP_NAME/urls.py作成
cat > $ROOT_DIR/app/${1}/urls.py << EOF
from django.urls import include, path
# from . import views
from .views import *
app_name = '${1}'
 
urlpatterns = [
    path('api/jp', JPTestAPI.as_view()),
    path('api/us', USTestAPI.as_view()),
    path('api/br', BRTestAPI.as_view()),
    path('', index, name='index'),
]
EOF
 
# APP_NAME/views.py修正
cat > $ROOT_DIR/app/${1}/views.py << "EOF"
from uuid import uuid4
from urllib.parse import urlparse
from django.core.validators import URLValidator
from django.core.exceptions import ValidationError
from django.views.decorators.http import require_POST, require_http_methods
from django.shortcuts import render
from django.utils import timezone
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import ListView, TemplateView, DetailView
from django.shortcuts import render
from rest_framework.views import APIView
from rest_framework import authentication, permissions
from rest_framework import status, viewsets, filters
from rest_framework import permissions
from rest_framework.response import Response
# from django.db.models import Q
import pytz
from datetime import datetime
 
# REST API 通信テスト用
class JPTestAPI(APIView):
    permission_classes = (permissions.AllowAny,)
    def get(self, request, format=None):
        # return Response(data={'status': dt_now}, status=status.HTTP_200_OK)
        jp = pytz.timezone('Asia/Tokyo')
        return JsonResponse(data={'status': datetime.now(tz=jp)}, status=status.HTTP_200_OK)
 
class USTestAPI(APIView):
    permission_classes = (permissions.AllowAny,)
    def get(self, request, format=None):
        us = pytz.timezone('US/Eastern')
        return JsonResponse(data={'status': datetime.now(tz=us)}, status=status.HTTP_200_OK)
 
class BRTestAPI(APIView):
    permission_classes = (permissions.AllowAny,)
    def get(self, request, format=None):
        br = pytz.timezone('Brazil/East')
        return JsonResponse(data={'status': datetime.now(tz=br)}, status=status.HTTP_200_OK)
 
def index(request):
    return render(request, 'index.html'
EOF
 
# "テンプレート作成"
# templates＆staticフォルダ階層作成
mkdir $ROOT_DIR/app/templates/${1}
 
# "「config/urls.py」にルーティングを追加"
sed -i -e "s/]/    path('${1}', include('${1}.urls')),\n]/" $ROOT_DIR/config/urls.py
 
```

初回実行時に「main」ディレクトリを作成するスクリプトですが、他にも新規でアプリディレクトリを追加したい場合は以下のコマンドで **APPNAME** を好きなアプリ名で実行するとスクリプトが実行され、新規ディレクトリが作成されます

``` bash
$ docker-compose run back bash /code/scripts/django-appstart.sh APPNAME
```

- 新規ディレクトリにアプリファイル一覧を作成
- 「app/config/settings.py」にアプリ名設定追記
- 「app/config/urls.py」にアプリルーティング追記
- 「app/templates/」に新規アプリ名ディレクトリを作成
- 「views.py」と「urls.py」に設定追記

---

## フロントエンド側

webアプリとしてのフロントエンド側のコンテナ構成

``` Dockerfile
FROM node:16.2.0
 
# 開発ディレクトリ設定
ENV WORKDIR /code
WORKDIR ${WORKDIR}
COPY ./scripts/ ${WORKDIR}/
 
# コンテナ内でパッケージを一括インストール
RUN npm update npm && npm install -g @vue/cli && \
    npm install -g @vue/cli-init && npm install -S axios
 
COPY . ${WORKDIR}/
```

### コンテナ起動関連スクリプト

- 初回ビルド時は新規Vueプロジェクト作成コマンドを催促
- 初回以降にappディレクトリ作成済の場合は各種スクリプト実行

``` front/scripts/start.sh
#!/bin/bash
set 'echo -e -o pipefail'
 
unix_today=$(date +'%s')
unix_today=$((unix_today+32400))
jst_ymd_today=$(date '+%Y/%m/%d %H:%M:%S' --date "@$unix_today")
app_name='app'
 
# プロジェクトディレクトリが存在していればサーバー起動
if [ -d /code/${app_name} ]; then
    # vueプロジェクト設定済みではなかったら
    if [ ! -f /code/${app_name}/vue.config.js ]; then
        echo "${jst_ymd_today} | Vue directory set up"
        # vueテンプレート作成
        bash /code/scripts/vue-settings.sh
    else
        echo "${jst_ymd_today} | Start the project server"
        # プロジェクトディレクトリに移動してサーバー起動
        cd ${app_name}
        npm run serve
    fi
else
    echo "「${jst_ymd_today}」｜Create a new project"
    #***********************************************************************
    echo "Vue axios設定ファイル作成"
    #***********************************************************************
    mkdir /code/${app_name}
    if [ ! -f /code/${app_name}/vue.config.js ]; then
        cat > /core/${app_name}/vue.config.js << "EOF"
        module.exports = {
            devServer: {
                proxy: {
                    '^/api/': {
                        target: 'http://localhost:8000',
                        logLevel: 'debug',
                        pathRewrite: { "^/api/": "/api/" }
                    }
                }
            }
            }
EOF
    fi
    echo "-----------------------------------------------------------"
    echo "Run the command 'docker-compose run front vue create ${app_name} .' on the host machine"
    echo "------------------------------------------------------------"
fi
 
```

### Vue.js設定用スクリプト
- 「HelloWorld.vue」にテスト用コード追記
- 「main.js」にaxiosとrourer設定追記
- 「router.js」作成
- 「components」内にルーティングテンプレートファイル作成

``` front/scripts/vue-settings.sh
#!/bin/bash
set 'echo -e -o pipefail'
 
# ルートディレクトリ取得
ROOT_DIR=$(cd $(dirname $0)/..;pwd)
app_name='app'
 
#************************************************************************************************
echo "「HelloWorld.vue」修正"
#************************************************************************************************
 
sed -i -e "s/<h1>{{ msg }}<\/h1>/<h1>{{ msg }}<\/h1>\n<h2>現在の時間：{{ result }}<\/h2>\n<button @click=\"getAPI()\">クリック！<\/button>/" ${ROOT_DIR}/${app_name}/src/components/HelloWorld.vue
 
sed -i -e "N;s/}\n<\/script>/, data () {\n    return {\n      result: 'No Result',\n      url: 'http:\/\/localhost:8000\/api\/'\n    }\n  },\n  methods: {\n    getAPI () {\n      this.\$axios.get(this.url).then(response => {\n        this.result = response.data.status\n      })\n    }\n  }\n}\n<\/script>/" ${ROOT_DIR}/${app_name}/src/components/HelloWorld.vue
 
 
#************************************************************************************************
echo "main.js修正"
#************************************************************************************************
cat > ${ROOT_DIR}/${app_name}/src/main.js << "EOF"
import Vue from 'vue'
import App from './App.vue'
import axios from 'axios'
import router from './router.js'
 
Vue.config.productionTip = false
Vue.prototype.$axios = axios
 
new Vue({
  router,
  render: h => h(App),
 
}).$mount('#app')
EOF
 
 
#***********************************************************
echo "コンテンツテンプレート作成"
#**********************************************************
cat > ${ROOT_DIR}/${app_name}/src/router.js << "EOF"
import Vue from 'vue'
import Router from 'vue-router'
import top from '@/components/top'
import page2 from '@/components/page2'
import page3 from '@/components/page3'
 
 
Vue.use(Router)
export default new Router({
    mode: 'history',
    routes: [
        {
            path: '/',
            component: top
        },
        {
            path: '/page2',
            component: page2
        },
        {
            path: '/page3',
            component: page3
        }
    ]
})
EOF
 
cat > ${ROOT_DIR}/${app_name}/src/components/top.vue << "EOF"
<template>
    <div>
        <p>ここはトップページです。</p>
        <h2>現在の日本時間：{{ result }}</h2>
        <button @click="getAPI()">クリック！</button>
    </div>
</template>
 
<script>
export default {
    name: 'jp-time',
    props: {
        msg: String
    },
    data () {
        return {
        result: '不明',
        url: 'http://localhost:8000/api/jp'
        }
    },
    methods: {
        getAPI () {
            this.$axios.get(this.url).then(response => {
                this.result = response.data.status
            })
        }
    }
}
</script>
<style>
</style>
EOF
 
cat > ${ROOT_DIR}/${app_name}/src/components/page2.vue << "EOF"
<template>
    <div>
        <p>ここは２ページ目です。</p>
        <h2>現在のアメリカ時間：{{ result }}</h2>
        <button @click="getAPI()">クリック！</button>
    </div>
</template>
 
<script>
export default {
    name: 'us-time',
    props: {
        msg: String
    },
    data () {
        return {
        result: '不明',
        url: 'http://localhost:8000/api/us'
        }
    },
    methods: {
        getAPI () {
            this.$axios.get(this.url).then(response => {
                this.result = response.data.status
            })
        }
    }
}
</script>
EOF
 
cat > ${ROOT_DIR}/${app_name}/src/components/page3.vue << "EOF"
<template>
    <div>
        <p>ここは３ページ目です。</p>
        <h2>現在のブラジル時間：{{ result }}</h2>
        <button @click="getAPI()">クリック！</button>
    </div>
</template>
 
<script>
export default {
    name: 'br-time',
    props: {
        msg: String
    },
    data () {
        return {
        result: '不明',
        url: 'http://localhost:8000/api/br'
        }
    },
    methods: {
        getAPI () {
            this.$axios.get(this.url).then(response => {
                this.result = response.data.status
            })
        }
    }
}
</script>
EOF
 
#*********************************************************
# "「App.vue」修正"
#*********************************************************
 
cat <<EOF > ${ROOT_DIR}/${app_name}/src/App.vue
<template>
  <div id="app">
    <div id="header">
      <!--リンクタグを生成します。-->
      <router-link to="/">top</router-link>41
      <router-link to="/page2">2</router-link>
      <router-link to="/page3">3</router-link>
    </div>
    <router-view></router-view>
  </div>
</template>
 
<!--コンポーネントの名前を定義します。-->
<script>
export default {
  name: 'App'
}
</script>
 
<!--スタイルの指定をします-->
<style>
body {
  margin: 0;
}
 
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}
#header {
  height: 40px;
  background: white;
  box-shadow: 0px 3px 3px rgba(0,0,0,0.1);
  display: flex;
  justify-content: center;
  align-items: center;
}
 
#header a {
  text-decoration: none;
  color: #2c3e50;
  margin: 0 10px;
  padding: 3px 10px;
  background: #5ccebf;
}
</style>
EOF
```

---

## Webサーバー
すべてのアクセスをさばくプロキシサーバー役のコンテナ構成

``` Dockerfile
ROM nginx:1.11.7
 
RUN mkdir /var/www
RUN mkdir /var/www/app
RUN mkdir /var/www/static
```

### アプリ用Nginxファイル
``` web/conf/app_nginx.conf
upstream back {
  ip_hash;
  server back:8001;  # uWSGIでDjangoとnginxとが通信するためのポート
  server front:3000;     # Vueとnginxとが通信するためのポート
}
 
server {
  listen      8000;     # 待ち受けポート
  server_name 127.0.0.1;
  charset     utf-8;
 
  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 /static {
    alias /var/www/static;
  }
 
  # バックエンドサーバー
  location / {
    include /etc/nginx/uwsgi_params;
    uwsgi_pass back;  # バックエンド
    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 back;
    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;
  }
  client_max_body_size 75M;
}
server_tokens off;
```

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

---

## コンテナ起動

まずコンテナ全体を起動。

``` bash
$ cd docker_spa
$ docker-compose up -d
```

バックエンドにappディレクトリが作成＆Djangoアプリ全体の設定ディレクトリ「config」とメインアプリディレクトリの「main」が作成されます。

webサーバー側のNginxも立ち上がるので `http:localhost:8000` にアクセスで「インデックスページのコンテンツを表示中」のテキストが表示されていればDjangoとNginxの疎通はOK

``` bash
$ docker-compose run front vue create app .
? Please pick a preset: Default (Vue 2) ([Vue 2] babel, eslint)
? Pick the package manager to use when installing dependencies: NPM
```

続いて上記コマンドで新規Vueプロジェクトは初期設定の対話コマンドがあるので「 **Vue2** 」と「 **npm** 」を選択してインストールします。


結構時間かかりますがインストールが完了したら再度 `docker-compose -d` でビルドするとフロント側のセットアップスクリプトが実行されます


```
docker_spa/
  ┣━ back/
  ┃  ┣━ app/
  ┃  ┃  ┣━ config/
  ┃  ┃  ┣━ main/
  ┃  ┃  ┣━ static/
  ┃  ┃  ┣━ templates/
  ┃  ┃  ┣━ db.sqlite3
  ┃  ┃  ┗━ manage.py
  ┃  ┣━ scripts/
  ┃  ┣━ Dockerfile
  ┃  ┗━ requirements.txt
  ┣━ front/
  ┃  ┣━ app/
  ┃  ┃  ┣━ node_modules
  ┃  ┃  ┣━ public/
  ┃  ┃  ┣━ src/
  ┃  ┃  ┣━ babel.config.js
  ┃  ┃  ┣━ package-lock.json
  ┃  ┃  ┣━ package.json
  ┃  ┃  ┗━ vue.config.js  # proxy設定ファイル
  ┃  ┣━ scripts/
  ┃  ┣━ Dockerfile
  ┃  ┣━ package-lock.json
  ┃  ┗━ package.json
  ┣━ web/
  ┃  ┣━ conf/
  ┃  ┃  ┗━ app_nginx.conf
  ┃  ┣━ logs/nginx
  ┃  ┃  ┗━ access.log
  ┃  ┃  ┗━ error.log
  ┃  ┣━ Dockerfile
  ┃  ┗━ uwsgi_params
  ┃   
  ┗━ docker-compose.yml
```

上手くいけば上記の構成が出来上がってると思います。
`http:localhost:8080` にアクセスするとVueのトップページが表示されます。


![app-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230956/218d8022-c914-36c5-d50f-cb2b90f0356e.png)

「top」「2」「3」でページが切り替わり、「クリック！」ボタンをクリックするとバックエンドとの通信を行い、現在の時刻を表示します。