Docker コンテナに Python ＋ Flask ＋ Vue.js ＋ Vuetify の Webアプリケーションを構築する

# Docker コンテナに Python ＋ Flask ＋ Vue.js ＋ Vuetify の Webアプリケーションを構築する
[前回の記事](https://qiita.com/Yu-Chihuahua/items/c8fa2a9f5c1e4ed9c33a)で、Docker コンテナに Python ＋ Flask の開発環境を構築しました。
フロントエンドはシングルページアプリケーションにしたいので、Python ＋ Flask ＋ Vue.js ＋ Vuetify の Webアプリケーションへ進化させていきます。
なお、Vue.js のフロントエンドツールは vite を使用します。

## 今回のゴール
- Webアプリケーションを Vue.js ＋ Vuetify のシングルページアプリケーションにする
- Windows からローカルホストでアクセスして「Hello World」を表示する

## ディレクトリ構成
今回のディレクトリ構成は下記のようになります。
vite で Vue.js アプリケーションを構築する時にデフォルトで作成されるファイルは残しているため、今回のアプリケーションの動作に必須ではないファイルも含まれている点はご留意ください。
なお、ディレクトリ作成や、ファイルを編集するコマンドは説明から省きます。
```diff_shell:WSL2 Ubuntu
  /home/appuser

  ├── workspace
  │   ├── docker-compose.yaml
  │   └── web_app003
  │       ├── Dockerfile
  │       └── opt
  │           ├── flask_start.sh
  │           └── web_app
+ │               ├── frontend
+ │               │   ├── README.md
+ │               │   ├── env.d.ts
+ │               │   ├── index.html
+ │               │   ├── package-lock.json
+ │               │   ├── package.json
+ │               │   ├── public
+ │               │   │   └── favicon.ico
+ │               │   ├── src
+ │               │   │   ├── App.vue
+ │               │   │   ├── assets
+ │               │   │   │   ├── base.css
+ │               │   │   │   ├── logo.svg
+ │               │   │   │   └── main.css
+ │               │   │   ├── components
+ │               │   │   │   ├── HelloWorld.vue
+ │               │   │   │   ├── TheWelcome.vue
+ │               │   │   │   ├── WelcomeItem.vue
+ │               │   │   │   └── icons
+ │               │   │   │       ├── IconCommunity.vue
+ │               │   │   │       ├── IconDocumentation.vue
+ │               │   │   │       ├── IconEcosystem.vue
+ │               │   │   │       ├── IconSupport.vue
+ │               │   │   │       └── IconTooling.vue
+ │               │   │   ├── main.ts
+ │               │   │   ├── plugins
+ │               │   │   │   └── vuetify.ts
+ │               │   │   ├── router
+ │               │   │   │   └── index.ts
+ │               │   │   └── views
+ │               │   │       ├── AboutView.vue
+ │               │   │       └── HomeView.vue
+ │               │   ├── tsconfig.app.json
+ │               │   ├── tsconfig.json
+ │               │   ├── tsconfig.node.json
+ │               │   └── vite.config.ts
  │               └── src
  │                   ├── app_controller.py
  │                   └── templates
- │                       └── {空ディレクトリ}
```

## 手順
### 資材作成
1. ```docker-compose.yaml```ファイルを修正
    ```docker-compose.yaml```ファイルを下記のように修正します。
    - Flask アプリケーションが自動的に起動しないように```command```を追加
    - ローカル（Ubuntu）のディレクトリと同期するように```volumes```を追加
    - Vue.js の開発サーバーを5173番ポートで公開する
    ```yaml:docker-compose.yaml
    version: '3'
    services:
    
      ##############################################################################
      # Web Application container（Python ＋ Flask ＋ Vue.js ＋ Vuetify）
      ##############################################################################
      web_app003:
        build: "./web_app003/"
        command: "/bin/bash" # Flask アプリケーションが自動的に起動しないようにする設定
        tty: true
        volumes:
          - "./web_app003/opt/web_app/:/opt/web_app/" # ローカル(Ubuntu)のディレクトリと同期する
        ports:
          - "5000:5000" # Flask port
          - "5173:5173" # Vue.js port
        image: "web_app003:develop"
        container_name: "web_app003"
    ```
    <br>

1. ```Dockerfile```ファイルを修正
    ```Dockerfile```ファイルを下記のように修正します。
    - nodejs をインストール対象に追加
    ```shell:Dockerfile
    # Amazon Linux 2023
    FROM public.ecr.aws/amazonlinux/amazonlinux:2023
    
    # ミドルウェアをインストール
    RUN dnf install -y \
        util-linux \
        shadow-utils \
        vim-enhanced-2:9.* \
        sudo-1.9.* \
        python3.11-3.11.* \
        python3.11-pip-22.3.* \
        python3.11-devel-3.11.* \
        nodejs-1:18.*
    
    # Python のライブラリをインストール
    RUN pip3.11 install flask==3.0.*
    RUN pip3.11 install flask_socketio==5.3.*
    RUN pip3.11 install flask-cors==4.0.*
    RUN pip3.11 install flask-wtf==1.2.*
    
    # ホストOS と同じユーザーを追加する
    # 1000 : host OS $(id -g)
    RUN groupadd -g 1000 appuser
    # 1000 : host OS $(id -u)
    RUN adduser -m appuser -u 1000 -g appuser
    # Add SUDO group.
    RUN groupadd sudo
    # Add SUDO authority.
    RUN usermod -aG sudo appuser
    
    # 資材を配置する
    WORKDIR /opt/web_app/
    RUN mkdir -p /opt/web_app/
    COPY ./opt/web_app/ /opt/web_app/
    COPY ./opt/flask_start.sh /opt/
    
    # オーナーとパーミッションを変更する
    RUN chown appuser:appuser /opt/web_app -R
    RUN chmod 775 /opt/web_app -R
    
    # 起動時のコマンドを設定する
    CMD ["sh","/opt/flask_start.sh"]
    ```
    <br>

1. Flask のテンプレートを削除
    Flask のテンプレートファイル```index.html```を削除します。
    （vite でビルド時に、当該ディレクトリに出力するため）
    ```shell:WSL2 Ubuntu
    sudo rm -r /home/appuser/workspace/web_app003/opt/web_app/src/templates/*
    ```
    <br>

1. Docker Compose でビルド＆起動
    Docker Compose でカスタムイメージをビルドして起動します。
    ```shell:WSL2 Ubuntu
    cd /home/appuser/workspace
    docker-compose build
    docker-compose up -d
    ```
    <br>

1. 起動した Docker コンテナに入る
    ```docker exec```コマンドで bash を指定すると、対話型のセッションが開始されるので、起動した Docker コンテナに入ったような操作ができるようになります。
    ```shell:WSL2 Ubuntu
    docker exec -it web_app003 bash
    ```
 
    下記のように出力されれば、対話型のセッションが開始されています。
    ```shell:web_app003
    bash-5.2#
    ```
    <br>

1. Vue.js プロジェクトを作成
    create-vue をインストールし、Vue.js プロジェクトを```frontend```という名前で作成します。
    ```shell:web_app003
    cd /opt/web_app/
    npm install -g create-vue@3.10.4
    ```

    下記のように出力されれば成功です。
    ```shell:web_app003
    added 1 package in 1s
    npm notice
    npm notice New minor version of npm available! 10.5.0 -> 10.9.0
    npm notice Changelog: https://github.com/npm/cli/releases/tag/v10.9.0
    npm notice Run npm install -g npm@10.9.0 to update!
    npm notice
    ```

    対話型で、```frontend```という名前の Vue.js プロジェクトを作成します。
    ```shell:web_app003
    create-vue
    ```

    下記「◆」の通りに入力／選択します。
    ```shell:web_app003
    Vue.js - The Progressive JavaScript Framework

    ✔ Project name: … ◆frontend
    ✔ Add TypeScript? … No / ◆Yes
    ✔ Add JSX Support? … No / ◆Yes
    ✔ Add Vue Router for Single Page Application development? … No / ◆Yes
    ✔ Add Pinia for state management? … ◆No / Yes
    ✔ Add Vitest for Unit Testing? … ◆No / Yes
    ✔ Add an End-to-End Testing Solution? › ◆No
    ✔ Add ESLint for code quality? … ◆No / Yes
    ✔ Add Vue DevTools 7 extension for debugging? (experimental) … ◆No / Yes
    ```

    下記のように出力されれば成功です。
    ```shell:web_app003
    Scaffolding project in /opt/web_app/frontend...

    Done. Now run:

      cd frontend
      npm install
      npm run dev
    ```

    vue.js のパッケージをインストールします。
    ```shell:web_app003
    cd /opt/web_app/frontend/
    npm install
    ```

    下記のように出力されれば成功です。
    ```shell:web_app003
    added 126 packages, and audited 127 packages in 24s

    13 packages are looking for funding
      run `npm fund` for details
    
    found 0 vulnerabilities
    ```
    <br>

1. 追加ライブラリをインストール
    CSS ライブラリの Vuetify をインストールします。
    ```shell:web_app003
    cd /opt/web_app/frontend/
    npm install vuetify@3.7.2
    ```

    下記のように出力されれば成功です。
    ```shell:web_app003
    added 2 packages, and audited 129 packages in 3s
    
    14 packages are looking for funding
      run `npm fund` for details
    
    found 0 vulnerabilities
    ```

    Webフォントの @mdi/font をインストールします。
    ```shell:web_app003
    npm install @mdi/font@7.4.47
    ```

    下記のように出力されれば成功です。
    ```shell:web_app003
    added 1 package, and audited 130 packages in 890ms
    
    14 packages are looking for funding
      run `npm fund` for details
    
    found 0 vulnerabilities
    ```
    <br>

1. Vue.js のプログラムを修正
    ```vite.config.ts```ファイルを下記のように修正します。
    - server：開発サーバーを起動した時に、Windows のブラウザからローカルホストでアクセスできるようにする設定
    - build：ビルド時に、Flask のテンプレートディレクトリに出力するようにする設定
    ```ts:vite.config.ts
    import { fileURLToPath, URL } from 'node:url'
    
    import { defineConfig } from 'vite'
    import vue from '@vitejs/plugin-vue'
    import vueJsx from '@vitejs/plugin-vue-jsx'
    
    // https://vitejs.dev/config/
    export default defineConfig({
      plugins: [
        vue(),
        vueJsx(),
      ],
      resolve: {
        alias: {
          '@': fileURLToPath(new URL('./src', import.meta.url))
        }
      },
      server: {
        host: '0.0.0.0',
        port: 5173
      },
      build: {
        outDir: '../src/templates/',
        assetsDir: './static/'
      }
    })
    ```

    ```App.vue```ファイルを下記のように修正します。
    - Vuetify のテンプレート（ワイヤーフレーム）を使って画面レイアウトを構成する

    ```vue:App.vue
    <script setup lang="ts">
    import { RouterLink, RouterView } from 'vue-router'
    import HelloWorld from './components/HelloWorld.vue'
    </script>
    
    <template>
      <v-app>
        <!-- ヘッダ１ -->
        <v-system-bar color="secondary">
          System Bar
        </v-system-bar>
    
        <!-- メニュー -->
        <v-navigation-drawer>
          <v-container>
            <v-list>
              <v-list-item title="List１" />
            </v-list>
          </v-container>
        </v-navigation-drawer>
    
        <!-- ヘッダ２ -->
        <v-app-bar color="primary">
          <v-app-bar-title>
            Application Bar
          </v-app-bar-title>
        </v-app-bar>
    
        <!-- メインコンポーネント -->
        <v-main>
          <v-container>
            <v-alert type="info">Hello World</v-alert>
          </v-container>
        </v-main>
    
        <!-- フッタ -->
        <v-footer color="primary" app>
          Footer
        </v-footer>
      </v-app>
    </template>

    ～～～～～～～～～～ 以下省略 ～～～～～～～～～～
    ```

    ```vuetify.ts```ファイルを下記のように作成します。
    - @mdi/font を有効化する
    - Vuetify を有効化する
    
    ```ts:vuetify.ts
    // Styles
    import '@mdi/font/css/materialdesignicons.css'
    import 'vuetify/styles'
    
    // Vuetify
    import { createVuetify } from 'vuetify'
    import * as components from 'vuetify/components'
    import * as directives from 'vuetify/directives'
    import { aliases, mdi } from 'vuetify/iconsets/mdi'
    import '@mdi/font/css/materialdesignicons.css'
    
    export default createVuetify({
      components,
      directives,
      icons: {
        defaultSet: 'mdi',
        aliases,
        sets: {
          mdi,
        }
      },
    });
    ```

    ```main.ts```ファイルを下記のように修正します。
    - Vuetify を有効化する
    ```ts:main.ts
    import './assets/main.css'
    
    import { createApp } from 'vue'
    import App from './App.vue'
    import router from './router'
    import vuetify from './plugins/vuetify';
    
    const app = createApp(App)
    
    app.use(router)
    app.use(vuetify)
    
    app.mount('#app')
    ```

### 事前動作確認
1. Vue.js の開発サーバを起動
    コンテナ内で Vue.js の開発サーバを起動します。
    ```shell:web_app003
    cd /opt/web_app/frontend/
    npm run dev
    ```

    下記が出力されれば成功です。
    ```shell:web_app003
    > frontend@0.0.0 dev
    > vite
    
    
      VITE v5.4.8  ready in 727 ms
    
      ➜  Local:   http://localhost:5173/
      ➜  Network: http://172.19.0.2:5173/
      ➜  press h + enter to show help
    ```
    <br>

1. ローカルホストでアクセス
    Windows のブラウザからローカルホストでアクセスします。
    Vue.js の開発サーバ は5173番ポートで公開しているので、URLは```http://localhost:5173/```となります。
    下記の画面が表示されれば成功です。
    ![スクリーンショット.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3767768/46ed93c4-311b-66d9-18fe-5ebb43cec978.png)
    <br>

1. Vue.js を Flask にビルド
    Vue.js の開発サーバを一度停止した後、コンテナ内で Vue.js を Flask のテンプレートディレクトリにプロダクションビルドします。
    ```shell:web_app003
    cd /opt/web_app/frontend/
    npm run build
    ```

    下記のように出力されれば成功です。
    ```shell:web_app003
    > frontend@0.0.0 build
    > run-p type-check "build-only {@}" --
    
    
    > frontend@0.0.0 type-check
    > vue-tsc --build --force
    
    
    > frontend@0.0.0 build-only
    > vite build
    
    vite v5.4.8 building for production...
    
    (!) outDir /opt/web_app/src/templates is not inside project root and will not be emptied.
    Use --emptyOutDir to override.
    
    ✓ 515 modules transformed.
    ../src/templates/index.html                                             0.43 kB │ gzip:   0.28 kB
    ../src/templates/static/materialdesignicons-webfont-Dp5v-WZN.woff2    403.22 kB
    ../src/templates/static/materialdesignicons-webfont-PXm3-2wK.woff     587.98 kB
    ../src/templates/static/materialdesignicons-webfont-B7mPwVP_.ttf    1,307.66 kB
    ../src/templates/static/materialdesignicons-webfont-CSr8KVlo.eot    1,307.88 kB
    ../src/templates/static/AboutView-C6Dx7pxG.css                          0.09 kB │ gzip:   0.10 kB
    ../src/templates/static/index-Dm78o2We.css                            770.43 kB │ gzip: 111.49 kB
    ../src/templates/static/AboutView-Bub0W0gQ.js                           0.23 kB │ gzip:   0.20 kB
    ../src/templates/static/index-C7UOd8v6.js                             518.05 kB │ gzip: 166.74 kB
    
    (!) Some chunks are larger than 500 kB after minification. Consider:
    - Using dynamic import() to code-split the application
    - Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks
    - Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
    ✓ built in 4.92s
    ```
    <br>

1. Flask を起動する
    シェルを実行して Flask を起動します。
    ```shell:web_app003
    sh /opt/flask_start.sh
    ```

    下記のように出力されれば成功です。
    ```shell:web_app003
     * Serving Flask app 'app_controller'
     * Debug mode: off
    Address already in use
    Port 5000 is in use by another program. Either identify and stop that program, or start the server with a different port.
    ```
    <br>

1. ローカルホストでアクセス
    Windows のブラウザからローカルホストでアクセスします。
    Flask は5000番ポートで公開しているので、URLは```http://localhost:5000/```となります。
    下記の画面が表示されれば成功です。
    ![スクリーンショット.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3767768/11ed8bb1-4003-6129-2ebc-277ca88a84e6.png)
    <br>

1. 起動した Docker コンテナから退出
    対話型のセッションから退出します。
    ```shell:web_app003
    exit
    ```

### Docker ビルドに Vue.js のビルド処理を適用
1. Docker Compose を停止
    Docker Compose をいったん停止します。
    ```shell:WSL2 Ubuntu
    cd /home/appuser/workspace
    docker-compose down
    ```
    <br>

1. 不要ファイルを削除
    Docker コンテナ内のファイルが全て同期されたため、プログラムファイルとして管理しない不要ファイルを削除します。
    - Flask のテンプレートファイル（vite でビルド時に、当該ディレクトリに出力するため）
    - node_modules（```package.json```の情報でビルド時にインストールするため）
    ```shell:WSL2 Ubuntu
    sudo rm -r /home/appuser/workspace/web_app003/opt/web_app/frontend/node_modules/
    sudo rm -r /home/appuser/workspace/web_app003/opt/web_app/src/templates/*
    ```
    <br>

1. ```Dockerfile```ファイルを修正
    ```Dockerfile```ファイルを下記のように修正します。
    - vite で Vue.js 環境を構築してプロダクションビルドするコマンドを追加
    ```shell:Dockerfile
    # Amazon Linux 2023
    FROM public.ecr.aws/amazonlinux/amazonlinux:2023
    
    # ミドルウェアをインストール
    RUN dnf install -y \
        util-linux \
        shadow-utils \
        vim-enhanced-2:9.* \
        sudo-1.9.* \
        python3.11-3.11.* \
        python3.11-pip-22.3.* \
        python3.11-devel-3.11.* \
        nodejs-1:18.*
    
    # Python のライブラリをインストール
    RUN pip3.11 install flask==3.0.*
    RUN pip3.11 install flask_socketio==5.3.*
    RUN pip3.11 install flask-cors==4.0.*
    RUN pip3.11 install flask-wtf==1.2.*
    
    # ホストOS と同じユーザーを追加する
    # 1000 : host OS $(id -g)
    RUN groupadd -g 1000 appuser
    # 1000 : host OS $(id -u)
    RUN adduser -m appuser -u 1000 -g appuser
    # Add SUDO group.
    RUN groupadd sudo
    # Add SUDO authority.
    RUN usermod -aG sudo appuser
    
    # 資材を配置する
    WORKDIR /opt/web_app/
    RUN mkdir -p /opt/web_app/
    COPY ./opt/web_app/ /opt/web_app/
    COPY ./opt/flask_start.sh /opt/
    
    # オーナーとパーミッションを変更する
    RUN chown appuser:appuser /opt/web_app -R
    RUN chmod 775 /opt/web_app -R

    # vite で Vue.js 環境を構築してプロダクションビルドする
    WORKDIR /opt/web_app/frontend/
    RUN npm install
    RUN npm run build

    # 起動時のコマンドを設定する
    CMD ["sh","/opt/flask_start.sh"]
    ```
    <br>

1. ```docker-compose.yaml```ファイルを修正
    ```docker-compose.yaml```ファイルを下記のように修正します。
    - Flask アプリケーションが自動的に起動しないようにする```command```をコメントアウト
    - ローカル（Ubuntu）のディレクトリと同期する```volumes```をコメントアウト
    ```yaml:docker-compose.yaml
    version: '3'
    services:
    
      ##############################################################################
      # Web Application container（Python ＋ Flask ＋ Vue.js ＋ Vuetify）
      ##############################################################################
      web_app003:
        build: "./web_app003/"
      #  command: "/bin/bash" # Flask アプリケーションが自動的に起動しないようにする設定
        tty: true
      #  volumes:
      #    - "./web_app003/opt/web_app/:/opt/web_app/" # ローカル(Ubuntu)のディレクトリと同期する
        ports:
          - "5000:5000" # Flask port
          - "5173:5173" # Vue.js port
        image: "web_app003:develop"
        container_name: "web_app003"
    ```

### 動作確認
1. Docker Compose でビルド＆起動
    Docker Compose でカスタムイメージをビルドして起動します。
    ```shell:WSL2 Ubuntu
    cd /home/appuser/workspace
    docker-compose build
    docker-compose up -d
    ```
    <br>

1. ローカルホストでアクセス
    Windows のブラウザからローカルホストでアクセスします。
    Flask は5000番ポートで公開しているので、URLは```http://localhost:5000/```となります。
    下記の画面が表示されれば成功です。
    ![スクリーンショット.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3767768/11ed8bb1-4003-6129-2ebc-277ca88a84e6.png)

## 最後に
最初は対話型に Vue.js のプロジェクトを作成しましたが、2回目以降は Docker Compose を起動するだけで、Vue.js をプロダクションビルドし、Flask アプリケーションとして起動するところまでを自動化することができました。
プロジェクトメンバーが増えても、開発環境構築をスムーズに行うことが期待できますね。