This website requires JavaScript.

使用 Docker 部署 Spring-Boot+Vue 博客系统

源码地址:Fame


在今年年初的时候,完成了自己的个 Fame 博客系统的实现,当时也做了一篇博文 Spring-boot+Vue = Fame 写 blog 的一次小结 作为记录和介绍。从完成实现到现在,也断断续续的根据实际的使用情况进行更新。

只不过每次上线部署的时候都觉得有些麻烦,因为我的服务器内存太小,每次即使只更新了前台部分 (fame-front) 的代码,在执行npm build的时候都还必须把我的后端服务 (fame-server) 的进程关掉,不然会造成服务器卡死(惨啊)。

而且这个项目是前后端分离的,博客前台页面还为了 SEO 用了Nuxt框架,假如是第一次部署或者要服务器迁移的话,麻烦的要死啊,部署一次的话要以下步骤

  1. 安装 mysql,修改相关配置文件,设置编码时区等,然后重启
  2. 下载安装 java,配置 java 环境
  3. 下载安装 maven,配置 maven 环境
  4. 下载安装 nginx,修改配置文件,设计反向代理等
  5. 启动 spring-boot 项目
  6. 打包 vue 项目,npm install,npm run build
  7. 启动 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 项目结构

fame-structure

改造后的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的结构和刚才目录结构大体类似,也是分以下几个部分

  1. fame-nginx
  2. fame-mysql
  3. fame-server
  4. fame-admin
  5. fame-front

这个docker-compose.yml中有几个要点

  • fame-mysqlfame-serverrestart要设置为always,因为目前 Docker-compose 是没有一个方案可以解决容器启动的先后的问题的。即使设置了depends_on,那也只是控制容器开始启动的时间,不能控制容器启动完成的时间,所以让fame-mysqlfame-server这两个容器设置restart,防止 spring-boot 在 mysql 启动完成之前启动而报错启动失败
  • fame-serverfame-mysqlfame-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.propertiesapplication-docker.properties配置文件,把application.properties里的数据库日志等信息分别放到application-dev.propertiesapplication-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-adminfame-front中用了axios插件,用于发起和获取fame-server服务器的请求。在axios要配置服务器 url 地址baseUrl,那么通常开发环境和 Docker 环境以及生产环境的 url 可能都不一样,每次都去修改有点麻烦。(虽然只需要配置两处,但是代码洁癖不允许我硬编码这个配置)。

  1. 先修改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
    
  2. 再修改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里获取环境变量里的proxyHostproxyPort

...
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-composeenv_file参数,从文件中获取环境变量,可以为单独的文件路径或列表,如果同目录下有.env文件则会默认读取,也可以自己在docker-compose里设置路径。

已经在.env设置了环境变量BASE_URL的值,就能在docker-compose.yml里直接使用了。修改docker-compose.ymlfame-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.ymlfame-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

原文地址:使用 Docker 部署 Spring-Boot+Vue 博客系统

0条评论
avatar