容器化 React 应用
这节课让我们来容器化上面完成的 React 应用。不过在开始前,我们先重构下我们的项目吧。
重构¶
在项目根目录下面新增一个叫services
的文件夹,然后将nginx
文件夹移动到 services 目录下面,将client
文件夹移动到 services 目录下面,然后在 services 目录下面新建一个 users
文件夹,将根目录下面的project
文件夹、Dockerfile、manage.py、requirements.txt 文件都移动到services/users
目录下面,操作完成后,你看到的我们的代码结构应该是这样的:
$ flask-microservices-users [master] ⚡ ls -la total 88 drwxr-xr-x 12 ych staff 384 1 26 12:12 . drwxr-xr-x 4 ych staff 128 12 28 16:08 .. drwxr-xr-x 14 ych staff 448 1 26 12:27 .git -rw-r--r-- 1 ych staff 23 1 5 17:56 .gitignore -rw-r--r-- 1 ych staff 5 12 28 15:19 .python-version -rw-r--r-- 1 ych staff 645 1 5 18:13 .travis.yml -rw-r--r-- 1 ych staff 11357 12 29 10:44 LICENSE -rw-r--r-- 1 ych staff 206 1 5 18:23 README.md -rw-r--r-- 1 ych staff 922 1 26 12:11 docker-compose-prod.yml -rw-r--r-- 1 ych staff 755 1 26 12:15 docker-compose.yml drwxr-xr-x 6 ych staff 192 1 26 12:09 services $ flask-microservices-users [master] ⚡ ls -la services total 16 drwxr-xr-x 6 ych staff 192 1 26 12:09 . drwxr-xr-x 12 ych staff 384 1 26 12:12 .. drwxr-xr-x 10 ych staff 320 1 16 17:37 client drwxr-xr-x 4 ych staff 128 12 30 15:15 nginx drwxr-xr-x 7 ych staff 224 1 26 12:14 users $ flask-microservices-users [master] ⚡ ls -la services/users total 40 drwxr-xr-x 7 ych staff 224 1 26 12:14 . drwxr-xr-x 6 ych staff 192 1 26 12:09 .. -rw-r--r-- 1 ych staff 329 12 29 10:47 Dockerfile -rw-r--r-- 1 ych staff 1366 1 5 17:46 manage.py drwxr-xr-x 9 ych staff 288 12 31 10:56 project -rw-r--r-- 1 ych staff 159 1 13 14:28 requirements.txt
然后我们来更新docker-compose.yml
文件:
version: '3.3' services: users-db: container_name: users-db build: context: ./services/users/project/db dockerfile: Dockerfile ports: - 3307:3306 environment: - MYSQL_ROOT_PASSWORD=root321 users-service: container_name: users-service build: context: ./services/users dockerfile: Dockerfile-dev volumes: - './services/users:/usr/src/app' ports: - 5001:5000 # 暴露端口 - 主机:容器 environment: - APP_SETTINGS=project.config.DevelopmentConfig - DATABASE_URL=mysql+pymysql://root:root321@users-db:3306/users_dev - DATABASE_TEST_URL=mysql+pymysql://root:root321@users-db:3306/users_test depends_on: - users-db links: - users-db
同样的,更新docker-compose-prod.yml
文件:
version: '3.3' services: users-db: container_name: users-db build: context: ./services/users/project/db dockerfile: Dockerfile ports: - 3307:3306 environment: - MYSQL_ROOT_PASSWORD=root321 users-service: container_name: users-service build: context: ./services/users dockerfile: Dockerfile-prod expose: - '5000' # 只暴露容器端口 environment: - APP_SETTINGS=project.config.ProductionConfig - DATABASE_URL=mysql+pymysql://root:root321@users-db:3306/users_prod - DATABASE_TEST_URL=mysql+pymysql://root:root321@users-db:3306/users_test command: gunicorn -b 0.0.0.0:5000 manage:app depends_on: - users-db links: - users-db nginx: container_name: nginx build: ./services/nginx restart: always ports: - 8080:80 depends_on: - users-service links: - users-service
注意上面的docker-compose
文件我们更新到3.3
版本,注意看和之前的2.1
版本是有部分区别的。然后重新构建下我们的容器:
$ docker-compose -f docker-compose.yml up -d --build
确保上面的构建过程是正确的,我们可以通过查看users-service
服务的日志来确认:
$ docker-compose logs -f users-service Attaching to users-service users-service | * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit) users-service | * Restarting with stat users-service | * Debugger is active! users-service | * Debugger PIN: 170-528-240 users-service | 172.18.0.1 - - [26/Jan/2018 04:22:08] "GET /ping HTTP/1.1" 200 -
然后我们可以跑一遍我们的测试用例,确保的业务逻辑也是符合我们的预期的:
$ docker-compose -f docker-compose.yml run users-service python manage.py test
如果一切正常,上面的测试也会正常通过的。
本地开发环境¶
在services/client
目录下面添加Dockerfile-dev
文件:
FROM node:9.4 MAINTAINER "qikqiak@gmail.com" # 设置工作目录 RUN mkdir /usr/src/app WORKDIR /usr/src/app # 添加node_modules/.bin目录到 $PATH 环境下 ENV PATH /usr/src/app/node_modules/.bin:$PATH # 安装并缓存依赖包(用silent选项可以减少一些不必要的错误) ADD package.json /usr/src/app/package.json RUN npm install --silent RUN npm install react-scripts@1.0.17 -g --silent # 启动应用 CMD ["npm", "start"]
然后我们在docker-compose.yml
中新增 React 应用的服务:
client: container_name: client build: context: ./services/client dockerfile: Dockerfile-dev volumes: - './services/client:/usr/src/app' ports: - '3007:3000' environment: - NODE_ENV=development - REACT_APP_USERS_SERVICE_URL=${REACT_APP_USERS_SERVICE_URL} depends_on: - users-service links: - users-service
在确保users-service
服务正常启动后,在终端中导入环境变量:
$ export REACT_APP_USERS_SERVICE_URL=http://127.0.0.1:5001
然后重新构建镜像启动新的容器:
$ docker-compose -f docker-compose.yml up --build -d client
新容器启动完成后,运行 React 客户端测试代码:
$ docker-compose -f docker-compose.yml run client npm test
测试通过后,我们可以在本地浏览器中打开http://127.0.0.1:3007
访问 React 应用了,当然如果测试没通过的话,我们得根据错误信息去修复我们的测试代码,直到测试通过为止。
更新services/nginx/flask.conf
配置:
server { listen 80; location / { proxy_pass http://client:3000; proxy_redirect default; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $server_name; } location /users { proxy_pass http://users-service:5000; proxy_redirect default; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $server_name; } }
上面的nginx
配置文件做了什么工作?
- 首先在
location
块中定义了反向代理规则。 - 当请求的 URI 与
location
区域中的 URI 匹配的时候,Nginx
将请求转发给了后面的 React 服务(client)或者 Flask 服务(users-service)。
同样的,我们将Nginx
服务也定义到docker-compose.yml
文件中:
nginx: container_name: nginx build: ./services/nginx restart: always ports: - 8080:80 depends_on: - users-service - client links: - users-service
然后更新容器:
$ docker-compose -f docker-compose.yml up -d --build
注意我们这里用的主机端口为8080,因为我们本机的80端口已经被占用,在实际线上部署的时候应该用80端口,然后再根据域名进行匹配进行测试。
容器启动完成后,打开浏览器测试下我们的服务,确保下面的两个请求都是正常的:
- http://127.0.0.1:8080
- http://127.0.0.1:8080/users
我们可以通过下面的命令查看运行中的容器日志:
$ docker-compose -f docker-compose.yml logs -f
然后我们可以改变下 App 组件的 state 对象:
this.state = { users: [], username: 'test001', email: '' };
当我们保存的时候可以看到日志中输出了编译成功的信息,而且这个时候浏览器也自动刷新了,并且将 test001 填充在了用户名的输入框中。 当然测试过后,要记得将上面的 state 对象的值更改回去哦~
生产环境¶
在将 React 应用上到生产环境之前我们需要将应用进行打包构建用于生成一系列的静态文件,我们在services/client
目录下面的package.json
文件中可以看到有一个build
的脚本,该脚本就是用于我们对 React 应用进行打包的,在该目录下面执行命令:
$ npm run build > client@0.1.0 build /Users/ych/devs/workspace/yidianzhishi/tdd100/flask-microservices-users/services/client > react-scripts build Creating an optimized production build... Compiled successfully.
构建完成后我们可以发现在该目录下面多了一个build的目录,该目录下面打包过后的静态资源文件。我们需要一个静态服务器来访问这些静态资源,执行下面的命令:
$ npm install -g serve
$ serve -s build
┌──────────────────────────────────────────────────┐
│ │
│ Serving! │
│ │
│ - Local: http://localhost:5000 │
│ - On Your Network: http://192.168.31.9:5000 │
│ │
│ Copied local address to clipboard! │
│ │
└──────────────────────────────────────────────────┘
我们可以根据上面的提示访问http://localhost:5000
,然后我们可以在进行一次相关的测试,确保没有任何错误。
然后在client
的根目录下面新增一个用于生产环境的 Dockerfile 文件:(Dockerfile-prod)
# build environment FROM node:9.4 as builder RUN mkdir /usr/src/app WORKDIR /usr/src/app ENV PATH /usr/src/app/node_modules/.bin:$PATH ARG REACT_APP_USERS_SERVICE_URL ARG NODE_ENV ENV NODE_ENV $NODE_ENV ENV REACT_APP_USERS_SERVICE_URL $REACT_APP_USERS_SERVICE_URL COPY package.json /usr/src/app/package.json RUN npm install --silent RUN npm install react-scripts@1.1.0 -g --silent COPY . /usr/src/app RUN npm run build # production environment FROM nginx:1.13.5-alpine COPY --from=builder /usr/src/app/build /usr/share/nginx/html EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]
我们可以看到上面的Dockerfile
文件和之前的不太一样,出现了两个基础镜像,这是Docker
提供的多阶段构建功能,详细的可以参考我之前的一篇博文:Docker 的多阶段构建。
当构建镜像的时候,参数通过ARG
指令传递给Dockerfile
,然后就可以将其用做环境变量。npm run build
命令将生成静态文件,然后通过Nginx暴露80端口提供服务。
接下来我们不使用Docker Compose
来进行一些简单的测试。首先,在services/client
目录下面构建镜像,记得使用--build-arg
标记来传递合适的参数:
$ docker build -f Dockerfile-prod -t "test" ./ --build-arg NODE_ENV=development --build-arg REACT_APP_USERS_SERVICE_URL=http://127.0.0.1:5001
记得将上面的REACT_APP_USERS_SERVICE_URL替换成你自己的实际的 IP。
上面的构建命令是用services/client
下面的Dockerfile-prod
文件来构建一个叫 test 的镜像。
镜像构建完成后我们使用上面的 test 镜像来启动一个名为 test01 的容器,将主机的9000端口映射到容器的80端口:
$ docker run --name test01 -d -p 9000:80 test
容器启动成功后,我们可以在浏览器中打开链接http://localhost:9000/来进行相关的测试。 然后我们可以停止容器:
$ docker stop test01 $ docker rm test01
最好,删除上面的镜像:
$ docker rmi test
上面我们对Dockerfile-prod
进行了相关的设置和测试,现在我们将该服务添加到docker-compose-prod.yml
:
client: container_name: client build: context: ./services/client dockerfile: Dockerfile-prod args: - NODE_ENV=production - REACT_APP_USERS_SERVICE_URL=${REACT_APP_USERS_SERVICE_URL} ports: - '3007:80' depends_on: - users links: - users
注意这里我们使用的是args
,而不是环境变量了。另外,我们的这个 client 服务需要在 nginx 服务启动之前启动,所以我们相应的更新docker-compose-prod.yml:
nginx: container_name: nginx build: ./services/nginx restart: always ports: - 80:80 depends_on: - users - client
扫描下面的二维码(或微信搜索iEverything
)添加我微信好友(注明python
),然后可以加入到我们的python
讨论群里面共同学习