因字数限制,只能分开发,接上面 juejin.cn/post/690853…
三、部署篇
上面简单介绍了 docker 的一些用法,借用静态 HTML 文件与 nignx 镜像创建运行了一个容器,但是其远远不止这些,下面就通过部署本博客来作为例子再探探里面的一些知识点。
源码地址:https://github.com/Moon-Future/react-blog,可下载下来看着目录更清晰。
为了统一维护 docker 文件,以下将 docker 相关文件都放在各自目录下 docker 文件下,所以要特别主题构建上下文(context)的确定。
1. 部署前台 blog
在 blog 目录下创建 docker 目录,docker 目录下创建三个文件
- .dockerignore:拷贝文件忽略列表
- Dockefile
- docker-compose.yml
.dockerignore
node_modules
.next
Dockefile
# node 镜像
# apline 版本的node会小很多
FROM node:12-alpine
# 在容器中创建目录
RUN mkdir -p /usr/src/app
# 指定工作空间,后面的指令都会在当前目录下执行
WORKDIR /usr/src/app
# 拷贝 package.json
COPY package.json /usr/src/app
# 安装依赖
RUN npm i --production --registry=https://registry.npm.taobao.org
# 拷贝其他所有文件到容器(除了 .dockerignore 中的目录和文件)
COPY . /usr/src/app
# build
RUN npm run build
# 暴露端口 9000
EXPOSE 9000
# 运行容器时执行命令,每个 Dokcerfile 只能有一个 CMD 命令,多个的话只有最后一个会执行
CMD [ "npm", "start" ]
在 blog 目录下运行以下命令可以生成镜像 react_blog:blog
$ docker build -f docker/Dockerfile . -t react_blog:blog
第一次运行安装依赖时有点慢(有个 sharp 特别慢...),刚开始我使用 node-sass 时,安装总是报错,后来索性就换成了 less,省心。如果想用 yarn 安装的话,这里 Dockerfile 里 npm 相关的命令也可以换成对于的 yarn 命令。
漫长的等待后终于 build 成功,下面一些信息就是 npm run build 生成的文件
看看生成的镜像
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
react_blog blog fef06dfed97f 3 minutes ago 329MB
nginx latest ae2feff98a0c 31 hours ago 133MB
node 12-alpine 844f8bb6a3f8 3 weeks ago 89.7MB
然后来生成并运行容器
$ docker run -itd -p 9000:9000 --name react_blog_blog react_blog:blog
这里参数再说明一下:
-i
参数让容器的标准输入持续打开,--interactive-t
参数让 Docker 分配一个伪终端,并绑定到容器的标准输入上, --tty-d
参数让容器在后台,以守护进程的方式执行,--detach(Run container in background and print container ID)--name
参数指定容器唯一名称,若不指定,则随机一个名称
-it 一般同时加上,-d 参数如果不加的话,运行容器成功时,会进入一个终端命令界面,要想退出的话只能 Ctrl + C,退出之后容器也就退出了,docker ps -a
可以看到容器状态是 Exited (0)
,可以使用 docker start container
再次开启。加上 -d 的话容器就会直接在后台运行,一般的话就加上 -d。大家可以试试,之后再删除容器就可以了。
以上容器运行成功的话,在浏览器通过 服务器ip:9000
就可以访问到页面啦,Mac 或者 Windows 本地的话 localhost:9000
就可以访问啦。
docker-compose.yml
version: '3'
services:
web:
build:
context: ../
dockerfile: ./docker/Dockerfile
image: react_blog:blog
ports:
- 9000:9000
container_name: react_blog_blog
上面咱们通过 docker build
,docker run
等命令先生成容器,再生成并运行容器,是不是有点繁琐,命令不好记,输入也麻烦,这里我们就可以利用 docker-compose 来简化执行命令。
我们看一下文件内容:
- web:服务名
- build:构建相关,后面执行
docker-compose
命令路径要和 docker-compose.yml 同一路径,所以这里 context 构建上下文选择上一层源码目录,dockerfile 就是当前目录里的 Dockerfile - image:镜像名,如果有就直接使用,没有就通过上面的 Dockerfile 生成
- ports:端口映射
- container_name:容器名称,唯一。若不写,则为
当前目录_服务名_index
,index 数字(从1累加),若这里为docker_web_1
可以把上面用 Dockerfile 生成的容器删了 docker rm -f react_blog_blog
,用 docker-compose up 生成试试
在 docker 目录下执行命令
$ docker-compose up -d
要想重新生成镜像可以 docker-compose up -d --build
以上便把 blog 前端页面部署好了,现在只是单独部署学习,后面会删了和后台与接口一起部署。
2. 部署后台 admin
现在来单独部署 admin,在 Docker 篇时,我们已经使用到 admin 来简单部署学习制作镜像和生成容器,这里依然先在 admin 目录下生成生成环境静态文件
$ npm run build
在 admin 下创建 docker 目录用来存放 docker 相关文件,docker 目录下创建以下文件:
Dockerfile
FROM nginx
# 删除 Nginx 的默认配置
RUN rm /etc/nginx/conf.d/default.conf
EXPOSE 80
注意这里和上面的一些区别,
- 这里把 nginx 默认的配置删除了,之后我们自己配置一个
- 没有 COPY 静态文件到容器,在 docker-compose.yml 通过挂在的方式实现
docker-compose.yml
version: '3'
services:
admin:
build:
context: ../
dockerfile: ./docker/Dockerfile
image: react_blog:admin
ports:
- 9001:80
volumes:
- ../build:/www
- ./nginx.conf:/etc/nginx/conf.d/nginx.conf
container_name: react_blog_admin
这里多了 volumes (卷) 项,参数是数组,对应 宿主机文件:容器内文件
- ../build:/www,build 内的文件挂在到容器 /www 目录下
- ./nginx.conf:/etc/nginx/conf.d/nginx.conf,nginx.conf 挂载到容器 /etc/nginx/conf.d/nginx.conf 这个文件
这样做的好处是,当宿主机上的文件变动后,容器内的文件也会自动变动,相应的容器内文件变动,宿主机文件也会变动。这样之后源代码变动,重新打包生成 build 后,只需要放到服务器对应目录下,容器类 /www 下的类容就会是最新的,而不需要一次次的去执行 Dockerfile 拷贝 build 文件到容器内,数据库的数据通常也是这样保存在宿主机内,而防止容器删除时丢失数据。
同理 nginx.conf 配置文件也是一样,不过改动 nginx 配置文件后,要重启以下容器才生效 docker restart container
来运行容器吧,在 docker 目录下执行命令
$ docker-compose up -d
查看容器是否运行成功
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7db8ce1c6814 react_blog:admin "/docker-entrypoint.…" 16 minutes ago Up 16 minutes 0.0.0.0:9001->80/tcp react_blog_admin
运行失败的可以 docker logs container
查看日志
运行成功的话,在浏览器通过 服务器ip:9001
就可以访问到页面啦,Mac 或者 Windows 本地的话 localhost:9001
就可以访问啦。
nginx.conf
server {
listen 80;
sendfile on;
sendfile_max_chunk 1M;
tcp_nopush on;
gzip_static on;
location / {
root /www;
index index.html;
}
}
root 记得和上面挂在目录相同
3. 部署服务接口 service + Mysql
现在我们来部署服务接口,在 service 目录下创建 docker 目录,docker 目录下创建以下文件:
.dockerignore
node_modules
.github
article
article 目录用来存放博客内容文件
Dockerfile
FROM node:alpine
# 配置环境变量
ENV NODE_ENV production
# 这个是容器中的文件目录
RUN mkdir -p /usr/src/app
# 设置工作目录
WORKDIR /usr/src/app
# 拷贝package.json文件到工作目录
# !!重要:package.json需要单独添加。
# Docker在构建镜像的时候,是一层一层构建的,仅当这一层有变化时,重新构建对应的层。
# 如果package.json和源代码一起添加到镜像,则每次修改源码都需要重新安装npm模块,这样木有必要。
# 所以,正确的顺序是: 添加package.json;安装npm模块;添加源代码。
COPY package.json /usr/src/app/package.json
# 安装npm依赖(使用淘宝的镜像源)
# 如果使用的境外服务器,无需使用淘宝的镜像源,即改为`RUN npm i`。
RUN npm i --production --registry=https://registry.npm.taobao.org
# 拷贝所有源代码到工作目
COPY . /usr/src/app
# 暴露容器端口
EXPOSE 9002
CMD npm start
docker-compose.yml
version: '3'
services:
service:
build:
context: ../
dockerfile: ./docker/Dockerfile
image: react_blog:service
ports:
- 9002:9002
depends_on:
- db
environment:
MYSQL_HOST: localhost
MYSQL_USER: root
MYSQL_PASSWORD: 8023
volumes:
- ../article:/usr/src/app/article
container_name: react_blog_service
db:
image: mysql
# volumes:
# - /db_data:/var/lib/mysql
ports:
- 33061:3306
command: --default-authentication-plugin=mysql_native_password
environment:
MYSQL_ROOT_PASSWORD: 8023
# MYSQL_USER: root
MYSQL_PASSWORD: 8023
MYSQL_DATABASE: react_blog
container_name: react_blog_mysql
注意这里有运行了两个服务 service、db
service 服务是后端接口:
-
deponds_on:运行时会先运行 deponds_on 列表里的服务,防止依赖项还没运行,自己会报错
-
command:从 MySQL8.0 开始,默认的加密规则使用的是 caching_sha2_password,此命令可以更改加密规则。不加可能会报错
Client does not support authentication protocol requested by server; consider upgrading MySQL client
-
environment:环境变量,这里会传入代码中,在代码 /config/secret.js(secret-temp.js) 里面可以会使用到
/** * secret.js 模板 */ module.exports = { // mysql 连接配置 mysql: { host: process.env.MYSQL_HOST || 'localhost', port: process.env.MYSQL_PORT || '3306', user: process.env.MYSQL_USER || 'xxx', password: process.env.MYSQL_PASSWORD || 'xxx', database: process.env.MYSQL_DATABASE || 'xxxxxx', }, // jwt tokenConfig: { privateKey: 'xxxxxxxxxxxxxxxxxxxxxxxxx', }, }
-
volumes:这里我把文章写入宿主机了,挂载到容器里
db 服务是 Mysql 数据库:
-
volumes:数据设置存储在宿主机
-
ports:端口映射,宿主机通过 33061 端口可以访问容器内部 Mysql,我们之后就可以通过 Navicat 或其他数据库可视化工具来连接
-
environment:配置数据库
- MYSQL_ROOT_PASSWORD 必须要带上,设置 ROOT 账号的密码
- MYSQL_USER 容器登录 MySQL 用户名,注意,这里如果是 root 会报错
ERROR 1396 (HY000): Operation CREATE USER failed for 'root'@'%'
,根据 github.com/docker-libr… 可知,已经存在一个 root 用户,无法再创建,所以这个可以不带,就默认 root 用户登录,如果带的话就不要是 root,会新建一个账户 - MYSQL_PASSWORD 容器登录 Mysql 密码,对用户名 MYSQL_USER,如果是 ROOT,密码就是 MYSQL_ROOT_PASSWORD,如果是其他,就是设置新密码
- MYSQL_DATABASE 创建一个 react_blog 数据库,也可以不填,后面再进入容器或者 Navicat 创建,但是这里因为后端代码要连接到 react_blog 数据库,不创建的会连接会保存,所以还是加上。(实在不想加也可以后见创建好数据库后,才运行两个容器)
在 service/docker 目录下执行命令
$ docker-compose up -d
运行成功的话,看看 images 和 container
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
react_blog service 89139d833458 About an hour ago 150MB
react_blog admin 1b5d6946f1fe 32 hours ago 133MB
react_blog blog fef06dfed97f 35 hours ago 329MB
nginx latest ae2feff98a0c 2 days ago 133MB
mysql latest ab2f358b8612 6 days ago 545MB
node 12-alpine 844f8bb6a3f8 3 weeks ago 89.7MB
可以看到多了 Mysql 和 react_blog:blog 镜像
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
5878940d7626 react_blog:blog "docker..." 5 seconds ago Up 4 seconds 0.0.0.0:9000->9000/tcp react_blog_blog
3bff0060de19 react_blog:admin "/docker…" 3 minutes ago Up 18 seconds 0.0.0.0:9001->80/tcp react_blog_admin
d8a899232e8c react_blog:service "docker…" About a Exited (1) 5 minutes ago react_blog_service
a9da07ff5cae mysql "docker…" About an hr 33060/tcp, 0.0.0.0:33061->3306/tcp react_blog_mysql
可以看到多了 react_blog_service 和 react_blog_mysql 容器,其中 react_blog_service 容器运行失败了,显示没事失败的先别高兴,咱们来看看日志
$ docker logs react_blog_service
...
errno: "ECONNREFUSED"
code: "ECONNREFUSED"
syscall: "connect"
address: "127.0.0.1"
port: 3306
fatal: true
name: "ECONNREFUSEDError"
pid: 47
hostname: d8a899232e8c
...
可以看出是数据库连接失败了,在上面 docker-compose.yml 中我们定义的环境变量 MYSQL_HOST=localhost
传给后端代码来连接数据库,每个容器都相当一一个独立的个体,localhost 是 react_blog_service 自己的 ip (127.0.0.1),当然是访问不到 react_blog_mysql,这个问题我们在下一节再来解决,先来说说 Mysql。
上面可以看到 Mysql 容器已经成功运行,我们可以进入容器内部连接 Mysql,还记得怎么进入容器吗
$ docker exec -it react_blog_mysql /bin/sh
$ ls
bin boot dev docker-entrypoint-initdb.d entrypoint.sh etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
$ mysql -uroot -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 12
Server version: 8.0.22 MySQL Community Server - GPL
Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql>
可以看到顺利连接 Mysql,输入 exit
可以退出容器。我们也可以使用可视化工具来连接,我这里使用 Navicat 来连接
注意这里的端口 33061,上面我们通过端口映射,通过宿主机端口 33061 可以访问到 Mysql 容器内端口 3306,所以就连接上啦。
4. 容器互联
上面留了一个问题,service 连接数据库失败,现在我们来尝试解决。参考 Docker 筑梦师系列(一):实现容器互联
4.1 Network 类型
Network,顾名思义就是 “网络”,能够让不同的容器之间相互通信。首先有必要要列举一下 Docker Network 的五种驱动模式(driver):
bridge
:默认的驱动模式,即 “网桥”,通常用于单机(更准确地说,是单个 Docker 守护进程)overlay
:Overlay 网络能够连接多个 Docker 守护进程,通常用于集群,后续讲 Docker Swarm 的文章会重点讲解host
:直接使用主机(也就是运行 Docker 的机器)网络,仅适用于 Docker 17.06+ 的集群服务macvlan
:Macvlan 网络通过为每个容器分配一个 MAC 地址,使其能够被显示为一台物理设备,适用于希望直连到物理网络的应用程序(例如嵌入式系统、物联网等等)none
:禁用此容器的所有网络
默认情况下,创建的容器都在 bridge 网络下,如下如所示,各个容器通过 dokcer0 可连接到宿主机HOST,并且各自分配到 IP,这种情况下,容器间互相访问需要输入对方的 IP 地址去连接。
查看 network 列表
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
a75e040b03ed bridge bridge local
13545e6a3970 docker_default bridge local
5ec462838a1c host host local
c726e6887f10 none null local
这里有 4 的 network,默认本来只有 3 个,没有 docker_default,我也是写到这里才发现创建了一个 docker_default 网络,查找官网(Networking in Compose)才发现,通过 docker-compose 来生成运行容器时,如果没指定 network,会自动创建一个 network,包含当前 docker-compose.yml 下的所有容器,network 名字默认为 目录_default
,这里目录就是 docker
恰好我们这个几个 docker-compose.yml 都是放在 docker 目录下,所以创建的几个容器都是在 docker_default 网络里。可以一下命令查看网络详细信息
$ docker network inspect docker_default
[
{
"Name": "docker_default",
"Id": "13545e6a39708344b363b7fc16eefeb6775c37773222804ebd5b5fb6f28c38bb",
"Created": "2020-12-16T11:03:37.2152073+08:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.24.0.0/16",
"Gateway": "172.24.0.1"
}
]
},
"Internal": false,
"Attachable": true,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"23891d43187e046eea25936dc0ab703964cc6c7213bb150ae9529da3e2e57662": {
"Name": "react_blog_mysql",
"EndpointID": "649857f928e0444500cfd296035869678bf26162d429a4499b262776b2a1d264",
"MacAddress": "02:42:ac:18:00:03",
"IPv4Address": "172.24.0.3/16",
"IPv6Address": ""
},
"3bff0060de19fc973039c07c1931e2c1efe30c6707bcd77d2ff7ea4dc01aaf63": {
"Name": "react_blog_admin",
"EndpointID": "25d8fa518b0ce27498f562372c3424aee174cb1d8fbf9f2445f1c6af8e6aab7f",
"MacAddress": "02:42:ac:18:00:02",
"IPv4Address": "172.24.0.2/16",
"IPv6Address": ""
},
"5878940d7626a9fb20622cde4002075e390e5036036bafb99d80454d6cba594b": {
"Name": "react_blog_blog",
"EndpointID": "a3f8ee36eda09f524be7ea16a67a1e13e62cf558e5480218bb523f877d478e4a",
"MacAddress": "02:42:ac:18:00:04",
"IPv4Address": "172.24.0.4/16",
"IPv6Address": ""
}
},
"Options": {},
"Labels": {
"com.docker.compose.network": "default",
"com.docker.compose.project": "docker",
"com.docker.compose.version": "1.25.1"
}
}
]
可以看到 docker_default 网关地址为 172.24.0.1
,其他几个容器 IP 分别为 172.24.0.3
,172.24.0.2
,172.24.0.4
,所以这里的情况是这样的
上面说了默认网络 bridge 下容器见访问只能输入 IP 地址来连接,而自定义的网络还可以通过容器名来连接
这就可以避免每次生成容器 IP 会变的问题了。知道了这些,我们在 service 接口里就可已通过 react_blog_mysql 来连接 react_blog_mysql 容器了,service/docker/docker-compose.yml 修改如下:
version: '3'
services:
service:
build:
context: ../
dockerfile: ./docker/Dockerfile
image: react_blog:service
ports:
- 9002:9002
restart: on-failure
depends_on:
- db
environment:
MYSQL_HOST: react_blog_mysql # 此处 localhost 换为 mysql 容器名,在同一个自定义网络下,变会自动解析为 IP 连接
MYSQL_USER: root
MYSQL_PASSWORD: 8023
volumes:
- ./article:/usr/src/app/article
container_name: react_blog_service
db:
image: mysql
ports:
- 33061:3306
restart: on-failure
command: --default-authentication-plugin=mysql_native_password
environment:
MYSQL_ROOT_PASSWORD: 8023
MYSQL_PASSWORD: 8023
MYSQL_DATABASE: react_blog
container_name: react_blog_mysql
在此运行命令
$ docker-compose up -d --build
可以看到服务容器已正常运行,docker logs react_blog_service
查看日志也没有报错,说明已经连接数数据库,在代码你我加了一个 get 测试接口,在浏览器输入 IP:9002/api/test/get
或者 localhost:9002/api/test/get
,会返回一个 json 对象
{"message":"Hello You Got It"}
这里我试了 N 久,一直有问题,
-
Mysql 创建失败,environment 我加了一个 MYSQL_USER: root,结果一直报错
ERROR 1396 (HY000): Operation CREATE USER failed for 'root'@'%'
,根据 github.com/docker-libr… 可知,已经存在一个 root 用户,无法再创建,所以这个可以不带,就默认 root 用户登录,如果带的话就不要是 root,会新建一个账户。这里直接去掉 MYSQL_USER,使用 root 登录 -
service 创建失败,日志报错没连接上 Mysql,我试试了好久,最后发现重启一下 service
docker start react_blog_service
就可以了,所以我觉得应该是 Mysql 创建好后,数据口等一些配置还没搞好,所以 service 还连接不上,就一直报错,等一会重新运行 service 就好了,所以这里加上了 restart 参数,报错就重新启动,这样就不用自己去重启了,等一会,看日志没问题,就是连接成功了。明明使用了 depends_on,为什么还会有这种问题呢,我也不太清楚,不过官网有这段示例:
version: "3.9" services: web: build: . depends_on: - db - redis redis: image: redis db: image: postgres
我们再来看看 docker_default 网络
$ docker network inspect docker_default
[
{
"Name": "docker_default",
"Id": "13545e6a39708344b363b7fc16eefeb6775c37773222804ebd5b5fb6f28c38bb",
"Created": "2020-12-16T11:03:37.2152073+08:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.24.0.0/16",
"Gateway": "172.24.0.1"
}
]
},
"Internal": false,
"Attachable": true,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"3bff0060de19fc973039c07c1931e2c1efe30c6707bcd77d2ff7ea4dc01aaf63": {
"Name": "react_blog_admin",
"EndpointID": "25d8fa518b0ce27498f562372c3424aee174cb1d8fbf9f2445f1c6af8e6aab7f",
"MacAddress": "02:42:ac:18:00:02",
"IPv4Address": "172.24.0.2/16",
"IPv6Address": ""
},
"5878940d7626a9fb20622cde4002075e390e5036036bafb99d80454d6cba594b": {
"Name": "react_blog_blog",
"EndpointID": "a3f8ee36eda09f524be7ea16a67a1e13e62cf558e5480218bb523f877d478e4a",
"MacAddress": "02:42:ac:18:00:04",
"IPv4Address": "172.24.0.4/16",
"IPv6Address": ""
},
"83005eec8d50071a6c23a2be4af8552983c09c532e937f04d79f02f8eb68acc9": {
"Name": "react_blog_mysql",
"EndpointID": "265ed7793c98287a05ccf8997e81671287a02ee8ea464984996083a34abe10dd",
"MacAddress": "02:42:ac:18:00:03",
"IPv4Address": "172.24.0.3/16",
"IPv6Address": ""
},
"937339a37ce726e704ec21b31b4028a97967a00de01438557e5a60d8538a51c8": {
"Name": "react_blog_service",
"EndpointID": "934d26f32a2b23e2cb4691020cb93d26c97b9647108047b492c3f7dd2be6faef",
"MacAddress": "02:42:ac:18:00:05",
"IPv4Address": "172.24.0.5/16",
"IPv6Address": ""
}
},
"Options": {},
"Labels": {
"com.docker.compose.network": "default",
"com.docker.compose.project": "docker",
"com.docker.compose.version": "1.25.1"
}
}
]
可以看到 react_blog_service 也已正常加入网络,IP 为 172.24.0.5
4.2 自定义 Network
docker_default 网络是根据目录来创建的,恰巧我们这几个项目 docker-compose.yml 文件都放在 docker 目录下,所以都在一个网络,如果名称变了就不在一个网络,并且之后项目可能还会有 docker 目录,全部都在一个网络也是不太好的,所以这里我们来自定义本次项目的网络。
blog/docker/docker-compose.yml
version: '3'
services:
blog:
build:
context: ../
dockerfile: ./docker/Dockerfile
image: react_blog:blog
ports:
- 9000:9000
networks:
- react_blog
container_name: react_blog_blog
networks:
react_blog:
admin/docker/docker-compose.yml
version: '3'
services:
admin:
build:
context: ../
dockerfile: ./docker/Dockerfile
image: react_blog:admin
ports:
- 9001:80
volumes:
- ../build:/www
- ./nginx.conf:/etc/nginx/conf.d/nginx.conf
networks:
- react_blog
container_name: react_blog_admin
networks:
react_blog:
service/docker/docker-compose.yml
version: '3'
services:
service:
build:
context: ../
dockerfile: ./docker/Dockerfile
image: react_blog:service
ports:
- 9002:9002
depends_on:
- db
environment:
- MYSQL_HOST=react_blog_mysql # 此处 localhost 换为 mysql 容器名,在同一个自定义网络下,变会自动解析为 IP 连接
- MYSQL_USER=root
- MYSQL_PASSWORD=8023
volumes:
- ./article:/usr/src/app/article
networks:
- react_blog
container_name: react_blog_service
db:
image: mysql
ports:
- 33061:3306
command: --default-authentication-plugin=mysql_native_password
environment:
- MYSQL_ROOT_PASSWORD=8023
- MYSQL_USER=root
- MYSQL_PASSWORD=8023
- MYSQL_DATABASE=react_blog
networks:
- react_blog
container_name: react_blog_mysql
networks:
react_blog:
- 与services 同级的 networks:创建一个新的 network,这里生成的 network 最终名称也会加上目录名,docker_react_blog。
- 服务内部的 networks:加入哪些网络,参数带 “-” 说明是数组,可以加入多个网络,这里我们全部加入 react_blog,不分前后端了
注意:
这样在 dockor-compose.yml 里生成的 network 都会加上当前目录名,若想不带,可以自己先生成一个
$ docker network create my_net
然后在 dockor-compose.yml 里
version: '3'
services:
service:
build:
context: ../
dockerfile: ./docker/Dockerfile
image: react_blog:service
ports:
- 9002:9002
depends_on:
- db
environment:
- MYSQL_HOST=react_blog_mysql # 此处 localhost 换为 mysql 容器名,在同一个自定义网络下,变会自动解析为 IP 连接
- MYSQL_USER=root
- MYSQL_PASSWORD=8023
volumes:
- ./article:/usr/src/app/article
networks:
- my_net
container_name: react_blog_service
db:
image: mysql
ports:
- 33061:3306
command: --default-authentication-plugin=mysql_native_password
environment:
- MYSQL_ROOT_PASSWORD=8023
- MYSQL_USER=root
- MYSQL_PASSWORD=8023
- MYSQL_DATABASE=react_blog
networks:
- my_net
container_name: react_blog_mysql
networks:
my_net:
external: true
加个 external 参数则使用已经创建的 network(my_net),不会再去创建或加上目录名。
我们再来重新创建容器,先删除全部容器
$ docker stop $(docker ps -aq)
$ docker rm $(docker ps -aq)
在进入各个目录分别执行 docker-compose up -d
,在运行第一个时会看到 Creating network "docker_react_blog" with the default driver
这句话,说明创建了一个新的 network,我们来看看
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
a75e040b03ed bridge bridge local
13545e6a3970 docker_default bridge local
e1ceb437a4fd docker_react_blog bridge local
5ec462838a1c host host local
c726e6887f10 none null local
$ docker network inspect docker_react_blog
[
{
"Name": "docker_react_blog",
"Id": "e1ceb437a4fdc5de91e51ff8831e21b565c92754159ad7057de36758e548a92f",
"Created": "2020-12-19T01:39:02.201644444+08:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1"
}
]
},
"Internal": false,
"Attachable": true,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"00da404f6f050b9b2f20e39bbb136fef614e8dfee85ec31bd6000bfd59cc2dab": {
"Name": "react_blog_mysql",
"EndpointID": "1cb966cc731eca3e9721e6d3edcfcac6152b66051faa934557f567e9e36c75c6",
"MacAddress": "02:42:ac:12:00:04",
"IPv4Address": "172.18.0.4/16",
"IPv6Address": ""
},
"ad1480e48e8e7ed160b1d4bcf7eed77d74505aea7581d48d8931206772b5d805": {
"Name": "react_blog_service",
"EndpointID": "8866c3457382d6baa945da09aef40da54c7dfdea0f393485001c35bb37d201a0",
"MacAddress": "02:42:ac:12:00:05",
"IPv4Address": "172.18.0.5/16",
"IPv6Address": ""
},
"b518d40b5021d3fdec7b7e62fbaa47b8a705a38346ccba2b9814174e46b67cd0": {
"Name": "react_blog_admin",
"EndpointID": "9a58ff20dc57d4d1fa6af83482051a68e80e22a5e37cf8e0cb3570b78102f107",
"MacAddress": "02:42:ac:12:00:03",
"IPv4Address": "172.18.0.3/16",
"IPv6Address": ""
},
"db0050257a8e8a0fa430ea04b009ae819dbf04ef001cf1027ec2b5565403b48e": {
"Name": "react_blog_blog",
"EndpointID": "664794ed292871bc7fd8e1c4eaa56f682a6be5d653209f84158f3334a4f30660",
"MacAddress": "02:42:ac:12:00:02",
"IPv4Address": "172.18.0.2/16",
"IPv6Address": ""
}
},
"Options": {},
"Labels": {
"com.docker.compose.network": "react_blog",
"com.docker.compose.project": "docker",
"com.docker.compose.version": "1.25.1"
}
}
]
4.3 调用接口
现在还有一个问题,我们在代码中调用接口形式是 http://localhost:9002/api/xxx
,在 react_blog_blog 容器中调用接口 localhost 是本身自己,没有调到 react_blog_service 里面的接口。
针对 admin
在代码中,我们这样来调接口
const HOST = process.env.NODE_ENV === 'development' ? 'http://localhost:9002' : ''
const API = {
getArticleList: HOST + '/api/getArticleList',
getArticle: HOST + '/api/getArticle',
addArticle: HOST + '/api/addArticle',
delArticle: HOST + '/api/delArticle',
getTagList: HOST + '/api/getTagList',
addTag: HOST + '/api/addTag',
delTag: HOST + '/api/delTag',
register: HOST + '/api/register',
login: HOST + '/api/login',
}
export default API
会发现接口 404,我们通过 nginx 来代理接口请求
admin/docker/nginx.conf
server {
listen 80;
sendfile on;
sendfile_max_chunk 1M;
tcp_nopush on;
gzip_static on;
location /api {
proxy_pass http://react_blog_service:9002;
}
location / {
root /www;
index index.html;
}
}
以 /api 为开头的请求,我们都转发到 react_blog_service 容器 9002 端口,将 nginx.conf 拖到服务器,因为我们是将此文件挂载到容器内部的,所以这里只需要重启一下容器
$ docker restart react_blog_admin
再看看请求接口,可以看到请求 200 成功,返回数据,如果返回 500,说明数据库还没建表,将目录下 react_blog.sql 导入数据库就可以了。
针对 blog
开始我以为通过环境变量(Next 中要存储在运行时变量里 Runtime Configuration)来传递请求 HOST (react_blog_service || localhost) ,但发现 react_blog_service 直接拼在前端接口里访问是不可行的(getServerSideProps 可行),所以最后还是改为 nginx 来代理请求,并且后面我们肯定还是要通过域名来访问网站的,所以还是需要 nginx,那么我们就为前台页面来加一个 nginx 容器。
1、创建环境变量
blog/docker/Dockerfile
# node 镜像
# apline 版本的node会小很多
FROM node:12-alpine
# 在容器中创建目录
RUN mkdir -p /usr/src/app
# 指定工作空间,后面的指令都会在当前目录下执行
WORKDIR /usr/src/app
# 拷贝 package.json
COPY package.json /usr/src/app
# 安装依赖
RUN npm i --production --registry=https://registry.npm.taobao.org
# 拷贝其他所有文件到容器(除了 .dockerignore 中的目录和文件)
COPY . /usr/src/app
# build 阶段获取
ENV HOST react_blog_service ## 增加一个环境变量,在 build 阶段可获取到,一定放在 npm run build 前一行
# build
RUN npm run build
# 暴露端口 9000
EXPOSE 9000
# 运行容器时执行命令,每个 Dokcerfile 只能有一个 CMD 命令,多个的话只有最后一个会执行
CMD [ "npm", "start" ]
代码中,设置运行是变量 blog/next.config.js
const withCSS = require('@zeit/next-css')
const withLess = require('@zeit/next-less')
module.exports = () =>
withLess({
...withCSS(),
// 改为 nginx 代理
publicRuntimeConfig: {
HOST: process.env.HOST || 'localhost', // 如果是 docker build,此处 process.env.HOST,否则就 localhsot,不影响本地运行
},
})
在 blog/config/api
import getConfig from 'next/config'
const { publicRuntimeConfig } = getConfig()
const SSRHOST = `http://${publicRuntimeConfig.HOST}:9002`
const HOST = `http://localhost:9002`
export const SSRAPI = {
getArticleList: SSRHOST + '/api/getArticleList',
getArticle: SSRHOST + '/api/getArticle',
}
export const API = {
getArticleList: HOST + '/api/getArticleList',
getArticle: HOST + '/api/getArticle',
}
这里有点麻烦,我不知道我的理解对不对,但试了多种情况只有这种本地和 docker 部署才都可以。
-
如果是本地运行(不使用 docker),服务端获取数据(getServerSideProps)和页面中获取数据直接使用服务接口地址(localhost:9002)即可
-
如果是 docker 运行,服务端获取数据(getServerSideProps)需要直接带上服务接口容器地址,无法通过 nginx 代理,页面中获取数据调用接口则职能通过 nginx 代理的方式
2、nginx 代理
修改 blog/docker/docker-compose.yml,增加一个 nginx 容器
version: '3'
services:
blog:
build:
context: ../
dockerfile: ./docker/Dockerfile
image: react_blog:blog
# ports:
# - 9000:9000
networks:
- react_blog
container_name: react_blog_blog
nginx:
build:
context: ../
dockerfile: ./docker/Dockerfile-nginx
image: react_blog:nginx
ports:
- 9000:80
volumes:
- ./nginx.conf:/etc/nginx/conf.d/nginx.conf
networks:
- react_blog
container_name: react_blog_nginx
networks:
react_blog:
blog/docker/Dockerfile-nginx
FROM nginx
# 删除 Nginx 的默认配置
RUN rm /etc/nginx/conf.d/default.conf
EXPOSE 80
blog/docker/nginx.conf
server {
listen 80;
sendfile on;
sendfile_max_chunk 1M;
tcp_nopush on;
gzip_static on;
location /api {
proxy_pass http://react_blog_service:9002;
}
location / {
proxy_pass http://react_blog_blog:9000;
}
}
3、生成容器
因为 blog 的内容有变,所以需要重新生成镜像,使用 docker-compose up -d --build
会重新下载 npm node_modules,比较慢,所以还是先生成镜像。
在 blog 目录下执行
$ docker build -f docker/Dockerfile . -t react_blog:blog
在 blog/docker 下执行
$ docker-compose up -d
运行成功的话,再试试接口就可以获取数据啦。
5. 连接宿主机 Mysql
上面遇到一个问题,在上面过程中,我的服务器(宿主机)上的 Mysql 出现了问题,连接时报错 2013 lost connection to mysql server at 'reading initial communication packet'
,我也不知道是什么原因引起的,解决方式是运行命令 systemctl start mysqld.service
启动 Mysql 服务,也不知是哪理影响到了。因为之前其他项目都是单独部署的,没使用 docker,数据都在宿主机 Mysql 上,所以我还是跟倾向于统一管理,自适应宿主机一个 Mysql,下面来看看怎么实现吧。
这里有两种方式
方式一:network_mode: host
修改 service/docker/docker-compose.yml
version: '3'
services:
service:
build:
context: ../
dockerfile: ./docker/Dockerfile
image: react_blog:service
ports:
- 9002:9002
restart: on-failure
# depends_on:
# - db
environment:
# MYSQL_HOST: react_blog_mysql # 此处 localhost 换为 mysql 容器名,在同一个自定义网络下,变会自动解析为 IP 连接
MYSQL_USER: root
MYSQL_PASSWORD: 8023
volumes:
- ../article:/usr/src/app/article
network_mode: host
# networks:
# - react_blog
container_name: react_blog_service
# db:
# image: mysql
# ports:
# - 33061:3306
# restart: on-failure
# command: --default-authentication-plugin=mysql_native_password
# environment:
# MYSQL_ROOT_PASSWORD: 8023
# MYSQL_PASSWORD: 8023
# MYSQL_DATABASE: react_blog
# networks:
# - react_blog
# container_name: react_blog_mysql
networks:
react_blog:
service/docker 下执行命令
$ docker-compose up -d
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
af9e525e7d14 react_blog:service "docker-entrypoint.s…" 28 seconds ago Up 26 seconds react_blog_service
可以看到 service 运行正常,且没有端口映射,docker inspect react_blog_service
也没有分配 IP,这种就相当于一个 Node 应用自己连接到宿主机 Mysql。但是对于页面接口请求来说,因为 react_blog_service 已不在 docker_react_blog ,所以就要使用宿主机 IP 地址来访问了。
nginx.conf
server {
listen 80;
sendfile on;
sendfile_max_chunk 1M;
tcp_nopush on;
gzip_static on;
location /api {
# proxy_pass http://react_blog_service:9002;
proxy_pass http://xxx.xx.xxx.x:9002; # xxx.xx.xxx.x 为宿主机(服务器)IP
}
location / {
proxy_pass http://react_blog_blog:9000;
}
}
服务端渲染接口也是一样
# node 镜像
# apline 版本的node会小很多
FROM node:12-alpine
# 在容器中创建目录
RUN mkdir -p /usr/src/app
# 指定工作空间,后面的指令都会在当前目录下执行
WORKDIR /usr/src/app
# 拷贝 package.json
COPY package.json /usr/src/app
# 安装依赖
RUN npm i --production --registry=https://registry.npm.taobao.org
# 拷贝其他所有文件到容器(除了 .dockerignore 中的目录和文件)
COPY . /usr/src/app
# build 阶段获取,xxx.xx.xxx.x 为宿主机(服务器)IP
ENV HOST xxx.xx.xxx.x
# build
RUN npm run build
# 暴露端口 9000
EXPOSE 9000
# 运行容器时执行命令,每个 Dokcerfile 只能有一个 CMD 命令,多个的话只有最后一个会执行
CMD [ "npm", "start" ]
这种方式是不是很麻烦,还要暴露服务器 IP 地址,所以我选择方式二
方式二:
修改 service/docker/docker-compose.yml
version: '3'
services:
service:
build:
context: ../
dockerfile: ./docker/Dockerfile
image: react_blog:service
ports:
- 9002:9002
restart: on-failure
# depends_on:
# - db
environment:
MYSQL_HOST: 172.17.0.1
MYSQL_USER: root
MYSQL_PASSWORD: 8023
volumes:
- ../article:/usr/src/app/article
networks:
- react_blog
container_name: react_blog_service
# db:
# image: mysql
# ports:
# - 33061:3306
# restart: on-failure
# command: --default-authentication-plugin=mysql_native_password
# environment:
# MYSQL_ROOT_PASSWORD: 8023
# MYSQL_PASSWORD: 8023
# MYSQL_DATABASE: react_blog
# networks:
# - react_blog
# container_name: react_blog_mysql
networks:
react_blog:
这里 MYSQL_HOST 为 172.17.0.1,上面也说了,容器可以通过此 IP 来连接到宿主机,所以这就连接上宿主机的 Mysql 了,其他的地方就不需要改了。
6. 一个 docker-compoer.yml
前面用了 3 个 docker-compose.yml 来启动各自的项目,还是挺繁琐的,我们来写一个汇总的,一个命令运行所以,当然后面某一个项目需要重新跑,也可以进入各自目录去运行自己的 docker-compose.yml
在项目根目录创建 docker/docker-compose.yml,创建 docker 目录,是为了创建的 network 和单个项目运行是创建的一致
version: '3'
services:
blog:
build:
context: ../blog
dockerfile: ./docker/Dockerfile
image: react_blog:blog
networks:
- react_blog
container_name: react_blog_blog
nginx:
build:
context: ../blog
dockerfile: ./docker/Dockerfile-nginx
image: react_blog:nginx
ports:
- 9000:80
volumes:
- ../blog/docker/nginx.conf:/etc/nginx/conf.d/nginx.conf
networks:
- react_blog
container_name: react_blog_nginx
admin:
build:
context: ../admin
dockerfile: ./docker/Dockerfile
image: react_blog:admin
ports:
- 9001:80
volumes:
- ../admin/build:/www
- ../admin/docker/nginx.conf:/etc/nginx/conf.d/nginx.conf
networks:
- react_blog
container_name: react_blog_admin
service:
build:
context: ../service
dockerfile: ./docker/Dockerfile
image: react_blog:service
ports:
- 9002:9002
restart: on-failure
environment:
MYSQL_HOST: 172.17.0.1
MYSQL_USER: root
MYSQL_PASSWORD: 8023
volumes:
- ../service/article:/usr/src/app/article
networks:
- react_blog
container_name: react_blog_service
networks:
react_blog:
停止并删除之前创建的所有容器
$ docker stop $(docker ps -aq)
$ docker rm $(docker ps -aq)
进入 /docker 目录执行,
$ docker-compose up -d
Building nginx
Step 1/3 : FROM nginx
---> ae2feff98a0c
Step 2/3 : RUN rm /etc/nginx/conf.d/default.conf
---> Running in bb163c42c6b5
Removing intermediate container bb163c42c6b5
---> 282cb303dddf
Step 3/3 : EXPOSE 80
---> Running in 9b77ebd39952
Removing intermediate container 9b77ebd39952
---> fbb18dda70af
Successfully built fbb18dda70af
Successfully tagged react_blog:nginx
WARNING: Image for service nginx was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Building admin
Step 1/3 : FROM nginx
---> ae2feff98a0c
Step 2/3 : RUN rm /etc/nginx/conf.d/default.conf
---> Using cache
---> 282cb303dddf
Step 3/3 : EXPOSE 80
---> Using cache
---> fbb18dda70af
Successfully built fbb18dda70af
Successfully tagged react_blog:admin
WARNING: Image for service admin was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Creating react_blog_admin ... done
Creating react_blog_service ... done
Creating react_blog_blog ... done
Creating react_blog_nginx ... done
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1fbb15abdd30 react_blog:service "docker" 13 seconds ago Up 6 seconds 0.0.0.0:9002->9002/tcp react_blog_service
fbee53e25c3a react_blog:admin "/docker" 13 seconds ago Up 6 seconds 0.0.0.0:9001->80/tcp react_blog_admin
70cb25f87d14 react_blog:blog "docker" 13 seconds ago Up 6 seconds 9000/tcp react_blog_blog
aa9fbf2afea4 react_blog:nginx "/docker" 13 seconds ago Up 6 seconds 0.0.0.0:9000->80/tcp react_blog_nginx
运行成功~
7. 域名
我现在是通过宿主机的 nginx 来代理域名访问 IP:9000,然后访问到 react_blog_nginx 容器,本想是直接在 react_blog_nginx 中做代理,但是试了没成功。想了想,访问 react_blog_nginx 是通过端口映射,宿主IP:9000 访问到的,如果在 react_blog_nginx 内部配置域名,总感觉是无法访问,这点还没想过,这几天再试试。
结语
终于写完了,写之前已经学习尝试了好久,以为很有把握了,结果在写的过程中又遇到一堆问题,一个问题可能都会卡好久天,各种百度,Google,油管都用上啦,总算解决了遇到的所有问题,当然这些问题可能只满足了我现在的部署需求,其中还有很多知识点,没有接触到,不过没关系,我就是想成功部署前端项目就可以了。
以上便是 docker 部署前端项目的所有笔记,内容比较啰嗦,希望能帮助后来的同学少走一点坑,因为有些是自己的理解,可能会有错误,还请大家指正,互相学习,over。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论