Closed8

Frourio + MySQL開発環境をDocker上に作成する

Makoto IshidaMakoto Ishida

試した環境

  • macOS Catalina v10.15.7
  • Docker Desktop Version 3.5.2

フォルダ作成

mkdir frourio-docker
cd frourio-docker
mkdir app
touch docker-compose.yml

docker-compose.yml ファイルを作成

docker-compose.yml

version: '3'
services:
  db:
    image: mysql:5.7
    env_file: ./mysql/mysql.env
    environment:
      - TZ=Asia/Tokyo
    ports:
      - '3306:3306'
    volumes:
      - ./mysql/conf:/etc/mysql/conf.d/:ro
      - mysqldata:/var/lib/mysql
    networks:
      - backend

  app:
    image: node:16
    environment:
      - TZ=Asia/Tokyo
    tty: true
    ports: # Host port:Container port
      - '5001:5001'
      - '8000:8000'
    volumes:
      - ./app:/app
    working_dir: /app
#    command: npm run dev  ## create-frourio-appが完了するまでコメントアウト。
    networks:
      - backend
    depends_on:
      - db

networks:
  backend:

volumes:
  mysqldata:

MySQL用の設定ファイルを作成

mkdir mysql
touch mysql/mysql.env

mysql.env

MYSQL_ROOT_HOST=%
MYSQL_ROOT_PASSWORD=mysqlroot
MYSQL_USER=mysqlusr
MYSQL_PASSWORD=mysqlpwd
MYSQL_DATABASE=todo
mkdir mysql/conf
touch mysql/conf/my.cnf

my.cnf

[client]
default-character-set=utf8mb4

[mysql]
default-character-set=utf8mb4

[mysqldump]
default-character-set=utf8mb4

[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_bin
lower_case_table_names=1

# Enable access from the host machine.
bind-address=0.0.0.0

コンテナを起動

docker-compose up -d

MySQLとアプリケーションの2つのコンテナが立ち上がる。

Appコンテナ内でcreate-frourio-appを実行

GUI無しでCLIでオプションを指定して実行。

docker-compose exec app npx create-frourio-app \
  --answers '{"dir": ".", "daemon": "pm2", "orm": "prisma", "db": "mysql", "mysqlDbHost": "db", "mysqlDbUser": "root", "mysqlDbPass": "mysqlroot", "mysqlDbName": "todo", "pm": "npm", "ci": "none" }'

create-frourio-app --answersのパラメータはこちらを参照:
https://frourio.io/docs/installation/cui/

ただし2021年7月15日現在このドキュメントが古く、記載されているパラメータ名とソースコードで使っているパラメータ名に食い違いがある模様。ソースコードを参照した方が確実。
https://github.com/frouriojs/create-frourio-app/blob/ed3b7b85d93cbef89c76fb81b9968a288428d613/server/common/prompts.ts#L573

create-frourio-appの処理が完了(1分〜数分かかるので注意)したのを確認して、ブラウザで http://localhost:8000 を開いてみる。この段階ではポート番号の関係でAPIにつながらないためエラーになるはず。CTRL+Cで一旦 docker-compose exec を停止する。

appフォルダ以下にFrourioアプリケーションが生成されている。

app/server/.envファイルの「API_SERVER_PORT」がランダムな番号になっているので、docker-compose.ymlで指定した番号に変更。3行目の「API_ORIGIN」も合わせておく。

API_SERVER_PORT=5001
API_BASE_PATH=/api
API_ORIGIN=http://localhost:5001
API_JWT_SECRET=supersecret
API_USER_ID=id
API_USER_PASS=pass
API_UPLOAD_DIR=upload

docker-compose.ymlを編集して、30行目のコメントアウトを解除して有効にする。

(変更前)# command: npm run dev
(変更後)command: npm run dev

コンテナを停止。

docker-compose down

あらためてコンテナを起動。

docker-compose up -d

しばらく(1〜2分)待ってブラブザで http://localhost:8000 を開くとTodoアプリケーションが動いているはず。

Makoto IshidaMakoto Ishida

ユーザー認証について、要件としてJWTをクライアント側のどこか(LocalStorageなど)に持ってAPI呼出しのたびにHeaderに付加するという方法ではなくhttp-onlyかつsecure属性の付いたCookieを使って実装したい。

ログイン時に id, pass を受け取って検証した後、defineController内からどうやって reply.setCookie() するのかが分からない。コントローラからreturnしたものがレスポンスのBodyとして返されることは分かったのだけど、BodyではなくHeaderにSet-Cookieを追加したい。

コントローラ内で replyオブジェクトへの参照をどうやって取得すればよいのだろう(?)

Makoto IshidaMakoto Ishida

defineControllerではなくdefineHooksを使ってpreHandlerフックに処理を書くことでなんとかJWTを含むCookieを返すことが出来た。自信はないけどひとまずこれで動いた。

export const hooks = defineHooks((fastify) => ({
  preHandler: async (req, reply, done) => {
    if (req.method !== 'POST') return 
    if (!req.body) return  
    const { id, pass } = req.body as any

    if (await validateUser(id, pass)) {
      const token = fastify.jwt.sign({ id })
      reply.setCookie('token', token, {
        path: '/',
        secure: req.protocol === 'https', 
        httpOnly: true,
        sameSite: true, 
        signed: false,
        expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 3)
      })
    }
  }
}))
Makoto IshidaMakoto Ishida

前提としてserverフォルダで、 npm i fastify-cookie をしておく必要がある。

その後、server/service/app.ts でfastify-jwt を register している箇所にCookieの指定を追加する。

これをすることで、ログイン以降のAPI認証時にAuthorizationヘッダだけでなくCookieもチェックしてくれるようになる。

server/service/app.ts

  app.register(fastifyJwt, {
    secret: API_JWT_SECRET,
    cookie: {
      cookieName: 'token',
      signed: false
    }
  })
  app.register(fastifyCookie)
Makoto IshidaMakoto Ishida

コントローラからレスポンスにヘッダを追加する方法が分かった。単に戻り値のbodyと同レベルにheaders: { キー:値 } という形で追加するだけだった。(ドキュメントには載ってなかった気が。)

reply.setCookie()メソッドを呼ぶ方法はまだ分からないけれど、下の通り直接キーと値を追加することで同様の効果があるのでまあひとまずこれで良しとしよう。

export default defineController((fastify) => ({
  post: ({ body }) => {
    if (validateUser(body.id, body.pass)) {
      const token = fastify.jwt.sign({ id: body.id })

      const cookieExpireStr = new Date(
        Date.now() + 1000 * 60 * 60 * 24 * 3
      ).toUTCString()

      return {
        status: 201,
        headers: {
          'set-cookie': `token=${token}; Path=/; Expires=${cookieExpireStr}; HttpOnly; SameSite=Strict`
        },
        body: { result: 'success' },
      }
    } else {
      return { status: 401 }
    }
  }
}))
Makoto IshidaMakoto Ishida

ただ、このままだとバックエンドとフロントエンドでポート番号が違うので別サイト扱いとなってAPIから返したCookieがフロントエンドのアプリでは有効にならない。

対応策としては、NginxかApacheのコンテナを追加してリバースプロキシする必要がある。単一のドメインで URLが /api/ の場合はバックエンドに、それ以外はフロントエンドにリクエストを流すようにしてみる。

Makoto IshidaMakoto Ishida

docker-compose.yml にnginxコンテナの設定を追加した。

version: '3'
services:
  db:
    image: mysql:5.7
    env_file: ./mysql/mysql.env
    environment:
      - TZ=Asia/Tokyo
    ports:
      - '3306:3306'
    volumes:
      - ./mysql/conf:/etc/mysql/conf.d/:ro
      - mysqldata:/var/lib/mysql
    networks:
      - backend

  app:
    image: node:16
    env_file: ./app.env
    environment:
      - TZ=Asia/Tokyo
    tty: true
    ports: # Host:Container
      - '5001:5001'
      - '8000:8000'
    volumes:
      - ./app:/app
    working_dir: /app
    command: npm run dev
    networks:
      - backend
    depends_on:
      - db
    restart: 'always'

  web:
    image: nginx:stable-alpine
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
    ports:
      - 8080:80
    networks:
      - backend
    depends_on:
      - app
    restart: always

networks:
  backend:

volumes:
  mysqldata:
Makoto IshidaMakoto Ishida

nginxフォルダに nginx.conf ファイルを作成。

nginx/nginx.conf

events {
    worker_connections  16;
}
http {
    server {
        listen 80;
        server_name localhost;
        location /api {
            proxy_pass http://app:5001;
        }
        location / {
            proxy_pass http://app:8000;
        }
    }
}

これでブラウザで http://localhost:8080 を開くとFrourioアプリケーションが開くようになった。バックエンドのAPI呼出しもCookieによる認証がちゃんと動いている。良かった!

このスクラップは2022/03/15にクローズされました