在今年年初的时候,完成了自己的个Fame博客系统的实现,当时也做了一篇博文Spring-boot+Vue = Fame 写blog的一次小结作为记录和介绍。从完成实现到现在,也断断续续的根据实际的使用情况进行更新。
只不过每次上线部署的时候都觉得有些麻烦,因为我的服务器内存太小,每次即使只更新了前台部分(fame-front)的代码,在执行npm build
的时候都还必须把我的后端服务(fame-server)的进程关掉,不然会造成服务器卡死(惨啊)。
而且这个项目是前后端分离的,博客前台页面还为了SEO用了Nuxt
框架,假如是第一次部署或者要服务器迁移的话,麻烦的要死啊,部署一次的话要以下步骤
- 安装mysql,修改相关配置文件,设置编码时区等,然后重启
- 下载安装java,配置java环境
- 下载安装maven,配置maven环境
- 下载安装nginx,修改配置文件,设计反向代理等
- 启动spring-boot项目
- 打包vue项目,
npm install
,npm run build
等 - 启动nuxt项目,
npm install
,npm run start
等
如果能够顺利的完成这七个步骤算是幸运儿了,假如中间哪个步骤报错出了问题,可能还要回头查找哪个步骤出了问题,然后又重新部署。
在这些需求面前,Docker就是解决这些问题的大杀器。无论是其虚拟化技术隔离各个容器使其资源互不影响,还是一致的运行环境,以及docker-compose的一键部署,都完美的解决了上述问题。
项目地址: Fame
Docker和Docker-compose安装
Docker和Docker-compose的功能和使用可以看线上的一个中文文档 Docker — 从入门到实践
下面是Centos7安装和配置Docker以及Docker-compose的shell脚本,其他操作系统可以参考修改来安装。其中Docker版本为docker-ce
,Docker-compose版本为1.22.0
#!/bin/sh
### 更新 ###
yum -y update
### 安装docker ###
# 安装一些必要的系统工具
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
# 添加软件源信息
sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
# 更新 yum 缓存
sudo yum makecache fast
# 安装 Docker-ce
sudo yum -y install docker-ce
# 启动docker并设置为开机启动(centos7)
systemctl start docker.service
systemctl enable docker.service
# 替换docker为国内源
echo '{"registry-mirrors": ["https://registry.docker-cn.com"],"live-restore": true}' > /etc/docker/daemon.json
systemctl restart docker
# 安装dokcer-compose
sudo curl -L https://github.com/docker/compose/releases/download/1.22.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
# 安装命令补全工具
yum -y install bash-completion
curl -L https://raw.githubusercontent.com/docker/compose/$(docker-compose version --short)/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose
### 安装docker结束 ###
Docker化改造
改造后目录结构
先看一下改造后的项目的结构
├─Fame
│ │ .env // docker-compose环境参数配置文件
│ │ docker-compose.yml // docker-compose文件
│ ├─fame-docker
│ │ │ fame-front-Dockerfile // fame-front的Dockerfile文件
│ │ │ fame-server-Dockerfile // fame-server的Dockerfile文件
│ │ │
│ │ ├─fame-admin
│ │ │ fame-admin-Dockerfile // fame-admin的Dockerfile文件
│ │ │ nginx.conf // fame-admin的nginx服务器配置文件
│ │ │
│ │ ├─fame-mysql
│ │ │ fame-mysql-Dockerfile // mysql的Dockerfile文件
│ │ │ mysqld.cnf // mysql的配置文件mysqld.cnf
│ │ │
│ │ └─fame-nginx
│ │ nginx-Dockerfile // 整个项目的nginx服务器的Dockerfile文件
│ │ nginx.conf // 整个项目的nginx的配置文件
│ │
│ ├─fame-admin // 博客管理后台,基于Vue+elementui
│ ├─fame-front // 博客前端,基于Nuxt
│ └─fame-server // 博客服务端,基于spring-boot
为了不破坏原有项目的结构,无论前端还是后端的docker的相关配置文件全部提取出来,单独放在了fame-docker
文件夹中。
docker-compose.yml
放在项目根目录下,直接在根目录运行命令:docker-compose up -d
[root@localhost Fame]# docker-compose up -d
Starting fame-front ...
Starting fame-admin ...
Starting fame-front ... done
Starting fame-admin ... done
Starting fame-nginx ... done
就启动项目了,再也不用重复繁琐的步骤!
改造后的docker项目结构
改造后的docker-compose.yaml
文件
version: '3'
services:
fame-nginx:
container_name: fame-nginx
build:
context: ./
dockerfile: ./fame-docker/fame-nginx/nginx-Dockerfile
ports:
- "80:80"
volumes:
- ./logs/nginx:/var/log/nginx
depends_on:
- fame-server
- fame-admin
- fame-front
fame-mysql:
container_name: fame-mysql
build:
context: ./
dockerfile: ./fame-docker/fame-mysql/fame-mysql-Dockerfile
environment:
MYSQL_DATABASE: fame
MYSQL_ROOT_PASSWORD: root
MYSQL_ROOT_HOST: '%'
TZ: Asia/Shanghai
expose:
- "3306"
volumes:
- ./mysql/mysql_data:/var/lib/mysql
restart: always
fame-server:
container_name: fame-server
restart: always
build:
context: ./
dockerfile: ./fame-docker/fame-server-Dockerfile
working_dir: /app
volumes:
- ./fame-server:/app
- ~/.m2:/root/.m2
- ./logs/fame:/app/log
expose:
- "9090"
command: mvn clean spring-boot:run -Dspring-boot.run.profiles=docker -Dmaven.test.skip=true
depends_on:
- fame-mysql
fame-admin:
container_name: fame-admin
build:
context: ./
dockerfile: ./fame-docker/fame-admin/fame-admin-Dockerfile
args:
BASE_URL: ${BASE_URL}
expose:
- "3001"
fame-front:
container_name: fame-front
build:
context: ./
dockerfile: ./fame-docker/fame-front-Dockerfile
environment:
BASE_URL: ${BASE_URL}
PROXY_HOST: ${PROXY_HOST}
PROXY_PORT: ${PROXY_PORT}
expose:
- "3000"
docker-compose.yml
的结构和刚才目录结构大体类似,也是分以下几个部分
- fame-nginx
- fame-mysql
- fame-server
- fame-admin
- fame-front
这个docker-compose.yml
中有几个要点
-
fame-mysql
和fame-server
的restart
要设置为always
,因为目前Docker-compose是没有一个方案可以解决容器启动的先后的问题的。即使设置了depends_on
,那也只是控制容器开始启动的时间,不能控制容器启动完成的时间,所以让fame-mysql
和fame-server
这两个容器设置restart
,防止spring-boot在mysql启动完成之前启动而报错启动失败 -
fame-server
,fame-mysql
,fame-nginx
这三个容器都设置了volumes
,把容器里的logs日志文件挂载到宿主机的项目目录里,方便随时看日志文件 -
fame-mysql
容器的mysql存储文件也设置了volumes
挂载在项目目录里(./mysql/mysql_data:/var/lib/mysql
),这个建议大家可以根据实际的情况设置到宿主机的其他目录里,不然不小心删除项目的话那么容器里的数据库数据也都没了
几个镜像的Dockerfile大部分都比较简单,这部分就不全部详细介绍了,可以直接去我项目中了解。
Docker化过程的困难和解决方法
spring-boot双配置切换
为了能够让spring-boot能够在开发环境和Docker环境下快速切换,需要将spring-boot的配置文件进行修改
└─fame-server
...
│ └─resources
│ │ application-dev.properties
│ │ application-docker.properties
│ │ application.properties
在原有的application.properties
基础上增加application-dev.properties
和application-docker.properties
配置文件,把application.properties
里的数据库日志等信息分别放到application-dev.properties
和application-docker.properties
这两个文件中,实现开发环境和Docker环境的快速切换。
# application.properties文件
#端口号
server.port=9090
#mybatis
mybatis.type-aliases-package=com.zbw.fame.Model
#mapper
mapper.mappers=com.zbw.fame.util.MyMapper
mapper.not-empty=false
mapper.identity=MYSQL
#mail
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
#默认properties
spring.profiles.active=dev
# application-docker.properties文件
#datasource
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://fame-mysql:3306/fame?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
#log
logging.level.root=INFO
logging.level.org.springframework.web=INFO
logging.file=log/fame.log
application-dev.properties
的内容和application-docker.properties
文件类似,只是根据自己开发环境的情况修改mysql和log配置。
动态配置axios的baseUrl
地址
在fame-admin
和fame-front
中用了axios
插件,用于发起和获取fame-server
服务器的请求。在axios
要配置服务器url地址baseUrl
,那么通常开发环境和Docker环境以及生产环境的url可能都不一样,每次都去修改有点麻烦。(虽然只需要配置两处,但是代码洁癖不允许我硬编码这个配置)。
-
先修改
fame-admin(Vue)
使其兼容手动部署模式和Docker模式fame-admin
是基于Vue CLI 3
搭建的,相对于cli 2.0官方把webpack的一些配置文件都封装起来了,所以没有config和build文件夹。不过对应的官网也给了一些设置更加方便的配置参数。在官方文档中提到:
只有以
VUE_APP_
开头的变量会被webpack.DefinePlugin
静态嵌入到客户端侧的包中。你可以在应用的代码中这样访问它们:console.log(process.env.VUE_APP_SECRET)
在构建过程中,
process.env.VUE_APP_SECRET
将会被相应的值所取代。在VUE_APP_SECRET=secret
的情况下,它会被替换为"sercet"
。利用这个特性来设置环境变量来动态的设置Docker模式和手动部署模式的
baseUrl
的值在
fame-admin
目录下创建文件server-config.js
,编写以下内容const isProd = process.env.NODE_ENV === 'production' const localhost = 'http://127.0.0.1:9090/' const baseUrl = process.env.VUE_APP_API_URL || localhost const api = isProd ? baseUrl : localhost export default { isProd, api }
那么只要在环境变量中有
VUE_APP_API_URL
的值,且NODE_ENV === 'production'
,baseUrl就等于VUE_APP_API_URL
的值,否则就是localhost
的值。接着在
axios
配置文件中引用该文件设置// fame-admin/src/plugins/http.js ... import serverConfig from '../../server-config' const Axios = axios.create({ baseURL: serverConfig.api + 'api/', ... }) ...
现在只要将docker的环境变量设置一个
VUE_APP_API_URL
的值就行了,只要在对应的Dockerfile中增加一个步骤就可以了。ENV VUE_APP_API_URL http://xx.xxx.xxx.xxx
-
再修改
fame-front(Nuxt)
使其兼容手动部署模式和Docker模式同样的,对于用
Nuxt
搭建fame-front
博客前台修改也是类似的思路。在Nuxt的官方文档中写到:
Nuxt.js 让你可以配置在客户端和服务端共享的环境变量。
例如 (
nuxt.config.js
):module.exports = { env: { baseUrl: process.env.BASE_URL || 'http://localhost:3000' } }
以上配置我们创建了一个
baseUrl
环境变量,如果应用设定了BASE_URL
环境变量,那么baseUrl
的值等于BASE_URL
的值,否则其值为http://localhost:3000
。所以我们只要和官方文档说的一样,在
nuxt.config.js
文件中增加代码就可以了module.exports = { env: { baseUrl: process.env.BASE_URL || 'http://localhost:3000' } }
接着在
server-config.js
文件和axios
的配置文件fame-front/plugins/http.js
以及对应的Dockerfile文件中编写和上面fame-admin
部分一样的代码就可以了
现在已经把baseUrl
的设置从代码的硬编码中解放出来了,但事实上我们只是把这个参数的编码从代码从转移到Dockerfile文件里了,要是想要修改的话也要去这两个文件里查找然后修改,这样也不方便。后面会解决这个问题把所有环境配置统一起来。
Nuxt在Docker中无法访问到宿主机ip问题
先要说明一点,为什么博客前端要单独去使用的Nuxt而不是和博客后台一样用Vue呢,因为博客前端有SEO的需求的,像Vue这样的对搜索引擎很不友好。
所以Nuxt的页面是服务器端渲染(SSR)的
这样就产生了问题
fame-front
的页面在渲染之前必须获取到fame-server
服务器中的数据,但是每个docker容器都是互相独立的,其内部想要互相访问只能通过容器名访问。例如容器fame-front
想要访问容器fame-server
,就设置baseURL = fame-server
(fame-server是服务器的容器的container_name)。
这样设置之后打开浏览器输入网址:http://xx.xxx.xxx.xx可以成功...,但是随便点击一个链接,就会看到浏览器提示错误无法访问到地址http://fame-server/...
vendor.e2feb665ef91f298be86.js:2 GET http://fame-server/api/article/1 net::ERR_CONNECTION_REFUSED
这是必然的结果,在容器里http://fame-server/就是服务器...,但是你本地的浏览器当然是不知道http://fame-server/是个什么鬼...,所以就浏览器就报出无法访问的错误。
什么?可是刚才不是说Nuxt是服务器渲染的页面吗,怎么又让本地浏览器报这个错误了。
原来是因为当通过浏览器链接直接访问的时候,Nuxt的确是从后端渲染了页面再传过来,但是在页面中点击链接的时候是通过Vue-Router跳转的,这时候不在Nuxt的控制范围,而是和Vue一样在浏览器渲染的,这时候就要从浏览器里向服务端获取数据来渲染,浏览器就会报错。
如何解决呢
这个问题开始的时候一直想要尝试配置Docker容器的网络模式来解决,可是都没有解决。直到后面我看axios
文档的时候才注意到axios
的代理功能,其本质是解决跨域的问题的,因为只要在axios
设置了代理,在服务端渲染的时候就会使用代理的地址,同时在浏览器访问的时候会用baseUrl
的地址,这个特点完美解决我的问题啊。
在server-config.js
文件里增加以下代码(在nuxt.config.js
里获取环境变量里的proxyHost
和proxyPort
)
...
const localProxy = {
host: '127.0.0.1',
port: 9090
}
const baseProxy = {
host: process.env.proxyHost || localProxy.host,
port: process.env.proxyPort || localProxy.port
}
exports.baseProxy = isProd ? baseProxy : localProxy
...
然后在axios
配置文件里增加代码
// fame-front/plugins/http.js
const Axios = axios.create({
proxy: serverConfig.baseProxy
...
})
...
就可以完美的解决问题了。
Dockerfile的环境参数统一设置
在上文解决动态配置axios地址
的部分把baseUrl
的设置放在了Dockerfile中,现在就再把Dockerfile中的硬编码提取出来,放到统一的配置文件中。
首先在docker-compose.yml
文件目录下(即项目跟目录)创建环境文件.env
并编写一下内容
BASE_URL=http://xx.xxx.xxx.xxx
PROXY_HOST=fame-nginx
PROXY_PORT=80
这个是docker-compose
的env_file
参数,从文件中获取环境变量,可以为单独的文件路径或列表,如果同目录下有.env
文件则会默认读取,也可以自己在docker-compose
里设置路径。
已经在.env
设置了环境变量BASE_URL
的值,就能在docker-compose.yml
里直接使用了。修改docker-compose.yml
的fame-front
部分:
fame-front:
...
environment:
BASE_URL: ${BASE_URL}
PROXY_HOST: ${PROXY_HOST}
PROXY_PORT: ${PROXY_PORT}
...
这样在fame-front
的容器里就有对应的BASE_URL
,PROXY_HOST
,PROXY_PORT
环境变量,Nuxt也能够成功获取并设置。
不过对于fame-admin
容器来说就要稍微复杂一点点了。先来看一下fame-admin
容器的Dockerfile文件fame-admin-Dockerfile
# build stage
FROM node:10.10.0-alpine as build-stage
#中间一些操作省略...
RUN npm run build
# production stage
FROM nginx:1.15.3-alpine as production-stage
COPY ./fame-docker/fame-admin/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
这里用了多阶段构建容器,如果直接通过docker-compose
设置环境变量只会在后面一个阶段生效,但是npm run build
是在第一个阶段执行的,所以环境变量不能应用到Vue当中。为了让环境变量在第一阶段就应用,必须要在构建的时候就把变量从docker-compose
传到fame-admin-Dockerfile
中,然后在Dockerfile中的第一阶段把这个环境变量应用到容器里。下面修改docker-compose.yml
的fame-admin
部分:
fame-admin:
...
build:
context: ./
dockerfile: ./fame-docker/fame-admin/fame-admin-Dockerfile
args:
BASE_URL: ${BASE_URL} # 这里把环境变量当做ARG传给Dockerfile
...
然后在fame-admin-Dockerfile
的第一阶段增加步骤
# build stage
FROM node:10.10.0-alpine as build-stage
ARG BASE_URL # 必须申明这个ARG才能从docker-compose里获取
ENV VUE_APP_API_URL $BASE_URL
# 以下省略...
这样就可以在构建阶段一镜像的时候就把环境变量传入到阶段一的镜像里,让Vue里的变量生效了。
总结
现在网上很多复杂一点的项目即使用了docker-compose部署,也多少依赖shell脚本来操作,比如复制文件设置环境等,我觉得这样会降低docker-compose的意义。如果都使用了shell脚本,那不如直接不用docker-compose而全用shell来构建和启动镜像。
所以在Docker化的过程中虽然遇到一些坎坷,但坚持实现了只用docker-compose部署,以后上线和下线就及其方便了。也希望我的Docker化思路可以给其他项目做一些参考。
对比以前恐怖的步骤,现在Fame博客的上线和下线只需要两行命令,真的十分的便捷。
docker-compose up
docker-compose down
源码地址:doodle