在今年年初的时候,完成了自己的个 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
原文地址: 使用 Docker 部署 Spring-Boot+Vue 博客系统