GraphQL 本質只能算是 Middleware,因此也能如 Node + Express 般包進 Docker,本文將介紹使用 Babel 後 Apollo GraphQL 如何打包成 Docker Image。
Version
Node 10.16.3
Apollo GraphQL 2.9.6
Babel 7.6.4
GraphQL Project
在根目錄下新增以下檔案:
- dockerfile
- src/index.js
- .babelrc
- nodemon.json
- package.json
- .env
- docker-compose.yml
Node
src/index.js
import { ApolloServer, gql } from 'apollo-server'
let data = [
{ title: 'FP in JavaScript', category: 'FP'},
{ title: 'RxJS in Action', category: 'FRP'},
{ title: 'Speaking JavaScript', category: 'JS'}
]
let typeDefs = gql`
type Query {
books(category: BookCategory!): [Book]
}
type Book {
title: String
category: BookCategory
}
enum BookCategory {
FP
FRP
JS
}
`
let books = (_, { category }) => data.filter(x => x.category === category)
let resolvers = {
Query: {
books
}
}
let apolloServer = new ApolloServer({ typeDefs, resolvers });
apolloServer
.listen()
.then(({ url }) => `GraphQL Server ready at ${ url }`)
.then(console.log)
在 project 根目錄下新增 src
目錄放 GraphQL,有別於 Babel 編譯後在 dist
目錄所產生的 js
。
index.js
為 GraphQL 主程式。
第 1 行
import { ApolloServer, gql } from 'apollo-server'
從 apollo-server
import 進 ApolloServer
與 gql
。
第 3 行
let data = [
{ title: 'FP in JavaScript', category: 'FP'},
{ title: 'RxJS in Action', category: 'FRP'},
{ title: 'Speaking JavaScript', category: 'JS'}
]
原始資料為 array of object,包含 title
與 category
兩個 property。
第 9 行
let typeDefs = gql`
type Query {
books(category: BookCategory!): [Book]
}
type Book {
title: String
category: BookCategory
}
enum BookCategory {
FP
FRP
JS
}
`;
在 typeDefs
內宣告 books
query、Book
type 與 BookCategory
enum。
28 行
let resolvers = {
Query: {
books
}
}
在 resolvers
內宣告 books
query。
26 行
let books = (_, { category }) => data.filter(x => x.category === category)
定義 books
query 實現方式。
34 行
let apolloServer = new ApolloServer({ typeDefs, resolvers })
建立 ApolloServer
,將 typeDefs
與 resolvers
組合成 Object 傳進其 constructor。
36 行
apolloServer
.listen()
.then(({ url }) => `GraphQL Server ready at ${ url }`)
.then(console.log)
以 apolloServer.listen()
啟動 Apollo GraphQL,其回傳為 Promise,將其 url
destructure 之後以 console.log
顯示。
Apollo Server 預設啟動在
4000
port
Babel Configuration
.babelrc
{
"presets": [
"@babel/preset-env"
]
}
在 project 根目錄下建立 .babelrc
設定 Babel,@babel/preset-env
為預設 Babel 組態。
Nodemon Configuration
nodemon.json
{
"exec": "cross-env NODE_ENV=development yarn dev",
"watch": ["src/*"],
"ext": "js, json"
}
- exec:當有變動時,將執行
yarn dev
- watch:Nodemon 將持續觀察的目錄
- ext:Nodemon 將持續觀察的 extension
watch
與ext
可視實際需求加以修改
NPM Configuration
package.json
{
"name": "apollo-gql",
"version": "0.1.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"serve": "nodemon",
"start": "node ./dist/index.js",
"dev": "babel-node ./src/index.js",
"prod": "yarn clean && cross-env NODE_ENV=production yarn build && yarn start",
"clean": "rimraf dist",
"build": "babel ./src --out-dir dist",
"docker:build": "yarn clean && cross-env NODE_ENV=production yarn build && docker build -t $npm_package_name:$npm_package_version .",
"docker:up": "docker-compose up -d",
"docker:down": "docker-compose down"
},
"dependencies": {
"apollo-server": "^2.9.7",
"graphql": "^14.5.8"
},
"devDependencies": {
"@babel/cli": "^7.6.4",
"@babel/core": "^7.6.4",
"@babel/node": "^7.6.3",
"@babel/preset-env": "^7.6.3",
"nodemon": "^1.19.4",
"rimraf": "^3.0.0"
}
}
17 行
"dependencies": {
"apollo-server": "^2.9.7",
"graphql": "^14.5.8"
},
"devDependencies": {
"@babel/cli": "^7.6.4",
"@babel/core": "^7.6.4",
"@babel/node": "^7.6.3",
"@babel/preset-env": "^7.6.3",
"nodemon": "^1.19.4",
"rimraf": "^3.0.0"
}
GraphQL 專案會用到的 package,包含 Apollo GraphQL、與 Babel。
第 7 行
"serve": "nodemon",
yarn serve
啟動 Nodemon,由 Nodemon 執行 yarn dev
。
yarn dev
與yarn serve
差異在 js 變動後是否重啟 Apollo GraphQL,實務上開發時建議使用yarn serve
,yarn dev
留給 Nodemon 使用
第 8 行
"start": "node ./dist/index.js",
yarn start
使用 node
執行 Babel 轉譯後的 js 並啟動 Apollo Server。
第 9 行
"dev": "babel-node ./src/index.js",
yarn dev
使用 babel-node
將 ES6+ 轉譯成 Node 能執行的 js 在記憶體內,並啟動 Apollo Server。
10 行
"prod": "yarn clean && cross-env NODE_ENV=production yarn build && yarn start",
yarn prod
設定 NODE_ENV
為 production
,且依序先執行:
yarn clean
刪除dist
目錄yarn build
使用 Babel 編譯成 Node 能執行的 js 在dist
目錄下yarn start
使用 Node 執行 Babel 編譯後的 js 並啟動 Apollo Server
若想在本機測試 Babel 編譯後結果是否正常,則要執行
yarn prod
而非yarn serve
11 行
"clean": "rimraf dist",
yarn clean
刪除 dist
目錄。
12 行
"build": "babel ./src --out-dir dist",
yarn build
使用 Babel 將 src
目錄下的 ES6+ 轉譯成 Node 能執行的 js 到 dist
目錄下。
將來要包進 Docker 的 js 也是
dist
目錄,而非src
目錄
13 行
"docker:build": "yarn clean && cross-env NODE_ENV=production yarn build && docker build -t $npm_package_name:$npm_package_version .",
yarn docker:build
與 yarn prod
類似,差異只在最後 docker:build
包成 image 而非啟動 Apollo GraphQL。
$npm_package_version
回隨著package.json
的version
而變,因此改版只要改version
即可
14 行
"docker:up": "docker-compose up -d",
使用 docker-compose
啟動 Apollo GraphQL。
稍後即將建立
docker-compose.yml
15 行
"docker:down": "docker-compose down"
使用 docker-compose
結束 Apollo GraphQL。
Dockerfile
FROM node:lts-alpine
WORKDIR /usr/app
COPY package.json .
RUN yarn install --production
COPY dist/* ./
CMD [ "node", "index.js" ]
在 project 目錄下建立 dockerfile
,由於我們要另外安裝 Apollo GraphQL,且將自己寫的 js 包進 Docker image,因此不可直接使用 docker-compose.yml
。
第 1 行
FROM node:lts-alpine
使用最新 LTS 版的 node:lts-alpine
為基底建立 image。
建議使用
alpine
為 production image,size 會小很多
第 2 行
WORKDIR /usr/app
將 working directory 切換到 /usr/app
,相當於:
mkdir /usr/app
cd /usr/app
稍後 COPY
、RUN
與 CMD
都會在此目錄下。
第 3 行
COPY package.json .
將根目錄的 package.json
複製到 Docker 內的 /usr/app
,因為要使用 yarn install
安裝 Apollo GraphQL。
第 4 行
RUN yarn install --production
執行 yarn install
安裝 dependencies
下的 package。
第 5 行
COPY dist/* ./
將 Babel 編譯過的 js 複製進 Docker 內的 /usr/app
。
第 6 行
CMD [ "node", "index.js" ]
執行 Docker 內的 /usr/app/index.js
啟動 Apollo GraphQL。
Docker Compose
docker-compose.yml
version: "3"
services:
graphql:
image: apollo-docker:${GQL_TAG}
container_name: MyGraphQL
restart: always
ports:
- ${GQL_PORT}:4000
dockerfile
只能建立 Docker image,要啟動 Apollo GraphQL 則要靠 docker-compose.yml
。
第 4 行
image: apollo-docker:${GQL_TAG}
在 image
定義 graphql
service 要使用的 Docker image 與 tag 版本,其中 GQL_TAG
稍後可由 .env
設定。
第 5 行
container_name: MyGraphQL
在 container_name
定義 container 名稱。
第 6 行
restart: always
當 container crash 時,會自動重啟。
第 7 行
ports:
- ${GQL_PORT}:4000
MyGraphQL
container 內部使用 4000
port,可透過 GQL_PORT
定義在 host os 所使用的 port 避免與其他 service 衝突。
.env
GQL_TAG=0.1.0
GQL_PORT=4000
定義 GQL_TAG
與 GQL_PORT
。
對於經常變動部分,應該放在
.env
,避免 user 直接修改docker-compose.yml
Build Image
$ yarn docker:build
使用 yarn docker:build
產生 Docker image。
Start Container
$ yarn docker:up
使用 yarn docker:up
啟動 Apollo GraphQL。
GraphQL Playground
query {
books(category: FP) {
title
category
}
}
使用 books
query 查詢資料,category
argument 傳入 FP
,並回傳 title
與 category
兩個 property。
Stop Container
$ yarn docker:down
使用 yarn docker:down
結束 Apollo GraphQL。
Conclusion
- 僅管使用 Babel 寫 GraphQL,只要將 Babel 編譯過的 js 包進 Docker 即可
Reference
Kian Wallace, Building Microservices and a GraphQL API with Docker