ごくまれにWebアプリ的な物を実装するんですが、毎回忘れちゃうのでメモ。
FastAPIでバックエンドを、フロントエンドをVue.jsで作成し、nginxでリバースプロキシをDockerで組んでみます。
各コンテナはdockerのbridgeネットワークで繋がっており、
nginxのコンテナだけポート80をホストに公開してアクセスしてきた人にアプリを提供します。
いきなり情けない話ですが、上記のイメージはnginxの設定がよくわかってないので実現できていません。
実際は各コンテナ内で公開する予定のURLで稼働してます。nginx難しいです・・
Docker network作成
デフォルトで作られるものでもいいんですが、コンテナ間で共有するネットワークを作っておきます。
docker network create -d bridge app_bridge
バックエンド(FastAPI)
バックエンドはFastAPIを使って作ります。
以前の記事で使用しましたが、pythonで簡単にAPIが作れてかっこいいドキュメントもできちゃうスゴイ奴です。
最終的なファイル構成はこんな感じになります。*.prod
なファイルはリリース用の環境です。
サンプルとして、'/'にアクセスするとランダムで文字列を返すAPIを作成しました。
main.py
app = FastAPI() app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"] ) @app.get('/') def read_root(): series = [ '', 'Stars', 'Friends', 'On parade', 'planet'] i = randrange(len(series)) return {"Message": f'Aikatsu {series[i]}!'} if __name__ == '__main__': uvicorn.run('main:app', host='0.0.0.0', port=80, reload=True)
まずは開発用の環境から。
変更→確認のサイクルを回したいので、
- ホストのソースコードをコンテナにマウント
- uvicornを
reload=True
で起動 - ポート80をホストのポート8000に公開
この条件でコンテナを設定します。
Dockerfile
FROM python:3.9-slim-buster WORKDIR /code/app # 必要なモジュールをインストール COPY ./requirements.txt /code/requirements.txt RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt # Appをデバッグ起動 CMD [ "python", "main.py" ]
docker-compose.yml
version: '3.8' services: api: container_name: sample-api-dev build: ./fastapi restart: always volumes: - ./fastapi/src:/code/app ports: - 8000:80
続いてリリース環境。
ポイントは以下の通り。
- ソースコードは変更しないのでコンテナへコピー
- ポートは公開しない
- Dockerネットワーク
app_bridge
にアタッチ
ホストからネットワーク繋がらないので、後ほど紹介するリバースプロキシを立ち上げないと確認できません。
docker-composeでファイルを指定するには-f
オプションを使います。
docker-compose -f docker-compose.prod.yml up -d --build
Dockerfile.prod
FROM python:3.9-slim-buster COPY ./requirements.txt . RUN pip install --no-cache-dir --upgrade -r requirements.txt COPY ./src /app EXPOSE 80 CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
docker-compose.prod.yml
version: '3.8' services: api: container_name: sample-api build: context: ./fastapi dockerfile: Dockerfile.prod restart: always networks: - app_bridge networks: app_bridge: external: true
フロントエンド1 (Vue.js)
フロントエンドはVue.jsで作成します。
ファイル構成はこんな感じ。
こちらもまずは開発用の環境から。
Dockerfile
FROM node:lts-alpine WORKDIR /app RUN yarn global add @vue/cli USER node
docker-compose.yml
version: '3.8' services: app: container_name: sample-app1-dev build: ./vue ports: - 8001:8080 volumes: - ./vue/src/:/app tty: true
立ち上げたコンテナに入ってプロジェクトを準備していきます。
コンテナのビルドと起動 % docker-compose up -d --build Building app : (中略) : sample-app1-dev is up-to-date コンテナの中に入る % docker-compose exec app ash vueがバージョン確認 /app $ vue -V @vue/cli 4.5.15 カレントディレクトリにVue.jsのプロジェクトを作成 /app $ vue create . ? Your connection to the default yarn registry seems to be slow. Use https://registry.npm.taobao.org for faster installation? (Y/n) Vue CLI v4.5.15 ? Generate project in current directory? (Y/n) ? Please pick a preset: (Use arrow keys) ❯ Default ([Vue 2] babel, eslint) Default (Vue 3) ([Vue 3] babel, eslint) Manually select features ? Pick the package manager to use when installing dependencies: (Use arrow keys) ❯ Use Yarn Use NPM :
Backend APIにアクセスするので axios をインストールします。
$ yarn add axios vue-axios
VueAxiosを読み込むため、src/main.js
を編集。
import Vue from 'vue' import axios from 'axios' // Add! import VueAxios from 'vue-axios'. // Add! import App from './App.vue' Vue.config.productionTip = false Vue.use(VueAxios, axios). // Add! new Vue({ render: h => h(App), }).$mount('#app')
HelloWorld.vue の代わりに HelloAikatsu.vue を追加します。
1000msec毎にAPIからメッセージをGETして表示を更新します。
<template> <div class="hello"> <h1>{{ message }}</h1> </div> </template> <script> export default { name: 'HelloAikatsu', props: { api: String }, data() { return { message: '', } }, mounted() { setInterval(() => { this.axios.get(this.api) .then(response => { this.message = response.data.Message) }) }, 1000) }, } </script>
App.vueも変更してコンポーネントHelloAikatsu
を組み込みます。
データapi
がprocess.env.VUE_APP_API_URL
となっていますが、これは.env
ファイルに記述された変数が読み込まれます。
<template> <div id="app"> <img alt="Vue logo" src="./assets/logo.png"> <HelloAikatsu :api="api"/> </div> </template> <script> import HelloAikatsu from './components/HelloAikatsu.vue' export default { name: 'App', components: { HelloAikatsu }, data() { return { api: process.env.VUE_APP_API_URL, } } } </script>
冒頭に泣き言を言った通り、URLのパスを指定したいのでvue.config.js
を追加してパスを記述します。
vue.config.js
module.exports = { publicPath: '/app1/', }
ここまで準備できたらyarn serve
で好きなだけ動作確認できます。
/app $ yarn serve yarn run v1.22.15 $ vue-cli-service serve INFO Starting development server... 98% after emitting CopyPlugin DONE Compiled successfully in 9788ms 3:13:20 AM App running at: - Local: http://localhost:8080/ It seems you are running Vue CLI inside a container. Access the dev server via http://localhost:<your container's external mapped port>/ Note that the development build is not optimized. To create a production build, run yarn build.
そしてリリース環境。
開発環境では動的に作成していましたが、リリース環境ではyarn build
してできたファイルを静的にサーブします。
そしてBackendと同様にDockerネットワークにアタッチします。
また、APIのURLを指定する.env
はリリース環境では固定なのでdot.env
ファイルを用意してコンテナにコピーする運用としました。
Dockefile.prod
FROM node:lts-alpine as build-stage WORKDIR /app COPY ./src/package*.json ./ COPY ./src/yarn.lock ./ RUN yarn global add @vue/cli RUN yarn install COPY ./src . COPY ./src/dot.env ./.env RUN yarn build FROM nginx:stable-alpine as production-stage COPY --from=build-stage /app/dist /usr/share/nginx/html EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]
docker-compose.prod.yml
version: '3.8' services: app: container_name: sample-app1 build: context: ./vue dockerfile: Dockerfile.prod networks: - app_bridge networks: app_bridge: external: true
フロントエンド2 (Vue.js)
もう一つのAppもVue.jsで作成します。
フロントエンド1と同じ内容でもいいですが、ちょっとだけ内容を変更してみます。
ロード時にAPIからGETした文字列をランダムで大文字小文字切り替えて表示しています。
<template> <div class="hello"> <h1>{{ message }}</h1> </div> </template> <script> export default { name: 'HelloAikatsu2', props: { api: String }, data() { return { message: '', } }, mounted() { this.loadMessage() setInterval(() => { this.message = this.upperLower(this.message) }, 100) setInterval(() => { this.loadMessage() }, 5000) }, methods: { loadMessage() { this.axios.get(this.api) .then(response => this.message = { response.data.Message }) }, upperLower(str) { function* ulg(str) { for (var c of [...str]) { if (c.search(/[^A-Za-z]/) == -1) { yield ((Math.floor(Math.random() * 10) >= 5) ? c.toLowerCase() : c) } else { yield c } } } var s = '' for (var c of ulg(str.toUpperCase())) { s += c } return s } } } </script>
手順はフロントエンド1と同じなので割愛します。
リバースプロキシ
リバースプロキシはnginx
を使います。
プロキシはリリース用だけでいいので、環境は一つだけ作ります。
各コンテナのリリース環境がぶら下がっているDockerネットワークにアタッチして、ポート80をホストに公開します。
version: '3.8' services: proxy: image: nginx:latest container_name: sample-proxy volumes: - ./conf.d/:/etc/nginx/conf.d ports: - "80:80" networks: - app_bridge networks: app_bridge: external: true
nginxのconfファイルはこんな感じです。
コンテナのアドレスはコンテナ名で解決するようです。
server { listen 80; listen [::]:80; server_name localhost; location /app1/ { proxy_pass http://sample-app1/; } location /app2/ { proxy_pass http://sample-app2/; } location /api/ { proxy_pass http://sample-api/; } }
実際に動かしてみました。
Dockerで起動したVue.jsのAppをnginxで振り分ける備忘録的サンプルhttps://t.co/KmFN2sEjJq pic.twitter.com/zrts5wMPCl
— イナバ (@hollyhockberry) 2021年11月21日
http://localhost/app1
とhttp://localhost/app2
でそれぞれのAppが表示できました。
今はこれが精一杯・・・
ソースコード
ソース全体をGithubにアップしています。