Frourio + MySQL開発環境をDocker上に作成する
試した環境
- 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のパラメータはこちらを参照:
ただし2021年7月15日現在このドキュメントが古く、記載されているパラメータ名とソースコードで使っているパラメータ名に食い違いがある模様。ソースコードを参照した方が確実。
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アプリケーションが動いているはず。
ユーザー認証について、要件としてJWTをクライアント側のどこか(LocalStorageなど)に持ってAPI呼出しのたびにHeaderに付加するという方法ではなくhttp-onlyかつsecure属性の付いたCookieを使って実装したい。
ログイン時に id, pass を受け取って検証した後、defineController内からどうやって reply.setCookie()
するのかが分からない。コントローラからreturnしたものがレスポンスのBodyとして返されることは分かったのだけど、BodyではなくHeaderにSet-Cookieを追加したい。
コントローラ内で replyオブジェクトへの参照をどうやって取得すればよいのだろう(?)
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)
})
}
}
}))
前提として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)
コントローラからレスポンスにヘッダを追加する方法が分かった。単に戻り値の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 }
}
}
}))
ただ、このままだとバックエンドとフロントエンドでポート番号が違うので別サイト扱いとなってAPIから返したCookieがフロントエンドのアプリでは有効にならない。
対応策としては、NginxかApacheのコンテナを追加してリバースプロキシする必要がある。単一のドメインで URLが /api/ の場合はバックエンドに、それ以外はフロントエンドにリクエストを流すようにしてみる。
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:
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による認証がちゃんと動いている。良かった!