前端不懂 Docker ?先用它换掉常规的 Vue 项目部署方式

本项目代码已开源,具体见:

前端工程:vue3-ts-blog-frontend

后端工程:express-blog-backend

数据库初始化脚本:关注公众号程序员白彬,回复关键字"博客数据库脚本",即可获取。

为什么需要容器化

当一个项目越来越复杂时,或者部署的环境越来越复杂时,你可能会考虑使用容器化部署来交付项目。因为你必须要考虑这样一些问题:

  • 为什么这个项目在我本地是正常的,部署到生产环境就不正常了?
  • 生产服务器要部署两个 nodejs 项目,但是这两个项目依赖的 nodejs 版本不一样,怎么办?
  • 买了一台新的云服务器,把项目从旧服务器迁移过去就跑不起来了......
  • 服务器部署了两个项目,但是其中一个项目误修改了另一个项目的一些关联文件,导致程序崩溃......

这些都是生产实践中会发生的问题,容器化的出现就是为了解决这些问题。

前端开发不懂容器化很正常,因为我们平时工作很难接触到这些。没关系,我们要创造机会让自己学会它,至少要让自己了解它。

简单认识 Docker

学习容器化肯定离不开 Docker,我们首先要对 Docker 的基础概念有所认识,这些可以去看Docker 官方文档学习,我也花了挺多时间去看文档,这是了解第一手信息最好的方式。

我一直推崇通过官方文档学习,因为这几乎就是最权威的资料。有了这些信息,再翻到一些文章或博客时,对一些观点也能有自己的判断力。

  • Image
  • Container
  • Network
  • Volume

Image 就是镜像,相当于一个类,或者说是一个模板,Image 是通过 Dockerfile 定义和构建的,Dockerfile 描述了制作 Image 的过程。

Container 是容器,它是基于镜像实例化得到的。容器是天然隔离的,容器包含了运行应用程序所需的所有东西,比如代码、依赖库、环境变量等。

Network 是网络,允许容器之间以及容器与宿主机之间进行通信。

Volume 是数据卷,用于挂载文件,解决容器中文件持久化的问题。

Docker 采用经典的CS架构。我们用 Docker Desktop 也好,用命令行也好,都是相当于一个客户端,我们给出的指令,首先会被 Docker daemon 接收,daemon 就是一个监视器进程,相当于一个守门员,所有的指令都要经过它的调度才能被处理,比如镜像操作、容器操作、网络操作、卷操作等等。

将 Vue 项目做成 Docker 镜像

我们在前端上手全栈自动化部署,让你看起来像个"高手"这篇文章中提到过,整个项目的部署架构是这样的:

在我们使用容器化改造全栈博客项目时,可以循序渐进,不必整个前后端全部都容器化,可以先把 Vue 前端部分换掉,也不会影响整个系统架构。我们来试着做一下。

前端项目一般都会用到框架,需要打包后才能交给服务器部署,所以第一步是打包,打包需要一个 NodeJS 环境。下面就是打包相关的 Dockerfile 配置。

csharp 复制代码
# 使用官方Node.js作为基础镜像
FROM node:16 as builder

# 设置工作目录
WORKDIR /app

# 首先复制项目的依赖配置文件
COPY package.json yarn.lock ./

# 安装项目依赖,这一步会生成一个独立的层,并且只有在package.json或yarn.lock变化时才会重新执行
RUN yarn install

# 接着复制项目所有文件,这一步会生成一个新的层
COPY . .

# 构建项目,这一步也会生成一个新的层
RUN yarn build

前端打包后就会得到一堆静态文件,包括 html, js, css, 图片、音频、视频等。

那么怎么让用户访问到网站的这些前端资源呢?这就需要一个 Web 服务器。

Nginx 就可以充当这个 Web 服务器,最后需要把端口暴露出去,并且安全组要放行该端口,不然是没法通过网络访问的。

bash 复制代码
# 使用Nginx镜像来运行构建好的项目
FROM nginx:latest

# 将构建好的项目复制到Nginx镜像的/usr/share/nginx/html目录下
COPY --from=builder /app/dist/ /usr/share/nginx/html

# 暴露端口
EXPOSE 80

整合上面的 Dockerfile 后,我们就可以执行打镜像的命令了。

erlang 复制代码
docker build -t vue3-ts-blog-frontend .

最后的.号代表上下文路径,Docker 会在这个路径下寻找 Dockerfile 及其他文件,根据 Dockerfile 配置打镜像。

打镜像成功后,可以在本地运行一下这个镜像进行验证。

arduino 复制代码
docker run -dp 3000:80 vue3-ts-blog-frontend

3000:80代表把宿主机的 3000 端口转发到容器的 80 端口,vue3-ts-blog-frontend则是我们刚才打出的镜像的名字。

接着我们打开http://localhost:3000访问,发现前端界面显示出来了,但是接口访问是404。

而且可以看到,接口的访问路径是这样的:

bash 复制代码
http://localhost:3000/article/page?pageNo=1&pageSize=6

按道理应该有/api前缀的,因为我们用环境变量配置了 axios 的 baseURL。

我们知道,webpack 在打包阶段会将process.env.VUE_APP_BASE_API的值替换成对应的字符串。本地 yarn build 会得到这样的结果。

这说明是镜像构建的时候出了问题,导致VUE_APP_BASE_API没生效。我看了一下容器运行时的文件,找到了对应的 js 文件,发现这个对象里压根没有VUE_APP_BASE_API

最后费了老大劲发现是 docker 默认模板生成的 .dockerignore 文件把我给坑了,它忽略了 .env,这就导致了打包时找不到 VUE_APP_BASE_API 这个环境变量,真是服了!

修改 .dockerignore 之后重新打镜像,运行容器,/api 的访问路径也正常了。虽然 /api 访问得到的结果是 404 Not Found,但是路径已经对了。后端服务是 404 可以先不管,后面用 nginx 转发一下就行。

此时我们还要做一点事情,那就是 404 的 fallback 处理,这个需要通过 nginx 的 try_files 配置实现。

如果不做任何处理,随便输入一个不存在的路由会被 nginx 返回 404,这是 nginx 默认的 404 页面。但是我们通常希望如果用户随便输入一个路由,应该给一个友好的 404 界面,这个工作可以由 nginx 承接,我们可以修改 nginx 默认的 404 页面;这个工作也可以由前端来承接,将流量转发到前端即可,再由前端路由到 /404。

想要让 /not-found-route 这种找不到的路由最终能被前端处理,就需要一个 try_files 配置。

如果要给 nginx 配置 try_files,就需要覆盖 nginx 镜像的 /etc/nginx/conf.d/default.conf 文件,我们给 Dockerfile 新增一条 COPY 指令。

bash 复制代码
# 复制自定义的Nginx配置到镜像中,覆盖默认配置
COPY nginx/default.conf /etc/nginx/conf.d/default.conf

接着我们在项目中新建一个 nginx/default.conf 文件,配置内容也很简单。

bash 复制代码
server {
    listen 80;
    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }
}

有了 try_files 配置后,无法匹配 nginx 路由的请求就会被转到 index.html,这个 index.html 自然就是 Vue 项目的入口。如果项目中 vue-router 配置了pathMatch,就能将未被定义的路由重定向到 /404 路由。

arduino 复制代码
// 匹配任意路径
const FALLBACK_ROUTE = {
    name: "Fallback",
    path: "/:pathMatch(.*)*",
    redirect: "/404",
}

此时这个镜像的准备工作就算是完成了,可以重新打包镜像。

erlang 复制代码
docker build -t vue3-ts-blog-frontend .

此时可能有朋友脑子里还记得接口访问 404 的问题,没关系,这个会在镜像部署到服务器后解决。我们先接着往下看,不着急。

私有镜像仓库的使用

镜像打好了之后,总得有个地方把它存起来,然后服务器上才能去拉取这个镜像进行部署,这就是镜像仓库做的事情。

镜像仓库有私有的,也有公开的,对于个人项目,我们通常希望使用私有镜像仓库。但是 DockerHub 官方对于未付费用户进行了限制,对单账号只提供一个私有仓库名额,这显然是不够用的。

DockerHub 上有一个 registry 镜像,就是用来搭建镜像仓库的。后续抽空单独出一篇文章分享一下如何用这个 registry 镜像搭建一个私有 Docker Registry。

既然先不着急搭建私有镜像仓库,我们就寻找一个免费的可靠的私有镜像仓库提供商,aliyun 就提供了这个能力。

我们打开 aliyun 控制台,在容器-容器服务这里有个容器镜像服务 ACR,我们打开它。

目前,aliyun 对个人用户提供了 300 个仓库的免费额度,这完全是够用的,我们用它来搭建自己的私有镜像仓库完全足够!

我们按照指引创建命名空间和仓库后,就可以进入仓库查看了,这里提供了登录 registry、推拉镜像的所有命令示例,操作起来非常简单。

私有镜像仓库都是要先登录才能使用的,我们先用 docker login 登录 aliyun registry。

ini 复制代码
docker login --username=itaobao8023 registry.cn-hangzhou.aliyuncs.com
Password: 
Login Succeeded

然后使用 docker tag 给前端项目打一个 aliyun registry 专属的 tag。

bash 复制代码
docker tag vue3-ts-blog-frontend registry.cn-hangzhou.aliyuncs.com/tusi_personal/blog:2.0.4

接着就可以使用 docker push 推送镜像了。

bash 复制代码
docker push registry.cn-hangzhou.aliyuncs.com/tusi_personal/blog:2.0.4

服务器部署改造

接着我们来到服务器,在服务器上拉取这个镜像。当然在服务器上也要先登录,才能 pull 镜像,我们先执行上面的 docker login 操作。

接着进行 pull 操作拉取镜像。

bash 复制代码
docker pull registry.cn-hangzhou.aliyuncs.com/tusi_personal/blog:2.0.4

镜像拉取成功后,通过 docker run 把容器跑起来。

arduino 复制代码
docker run -dp 3000:80 registry.cn-hangzhou.aliyuncs.com/tusi_personal/blog:2.0.4

容器的 80 端口映射到了宿主机器的 3000 端口,此时可以通过 curl 测试一下可访问性。

curl localhost:3000

如果能看到下面的结果,说明容器运行没有异常。

不过此时仅仅是把 Vue 项目的 nginx 容器跑起来了,整个博客项目的部署架构还没有调整呢。我们再回顾一下这张部署架构图,然后思考一下怎么改造它。

我们知道,整个博客是通过 nginx 接入的,/api 和 /socket.io 前缀的流量会转发到 express backend 服务,其余流量会到 /usr/share/nginx/html/vue3-ts-blog-frontend 这个目录下,也就是访问前端的静态资源文件。我们要改变这个行为,让除 /api 和 /socket.io 之外的流量全部转发到我们的前端容器中,这个容器目前是通过 3000 端口访问的。

/api 是用来访问后端 API 接口的,/socket.io 开头的请求是 Socket.IO 相关的。这些都不是前端的范畴,所以要转发到后端。

按照这个部署逻辑,我们修改一下 nginx 配置文件。

ini 复制代码
server {
    listen	80 default_server;
    server_name _;
    return 403;
}

server {
    listen       80;
    server_name  blog.wbjiang.cn;
    rewrite ^(.*)$ https://$server_name$1 permanent;
}

#博客https
server {
    listen 443 ssl http2;
    server_name blog.wbjiang.cn;
    ssl_certificate  /etc/nginx/cert/blog.wbjiang.cn.pem;
    ssl_certificate_key  /etc/nginx/cert/blog.wbjiang.cn.key;
    ssl_session_timeout 5m;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;
    location / {
        proxy_pass http://localhost:3000;
        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-Proto $scheme;
    }
    #api转发
    location /api {
        proxy_pass http://blog_pool;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        rewrite ^/api/(.*)$ /$1 break;
    }
    #websocket转发
    location /socket.io {
        proxy_pass http://blog_pool;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

注意上面的 location / 规则,它就是把默认流量转发到了 localhost:3000,也就是我们用 docker 运行的前端 nginx 容器。这样一来,整个系统的部署架构就变成了下面这样。

最后只需要重启 nginx 服务即可。

nginx -s reload

小结

我们希望在本项目中学习 docker 的使用,但是又不希望破坏整个系统的部署架构,因此做了这样的设计,只针对 vue 前端部分采用了 docker 部署方式。通过实践,我们学会了如何使用 docker 制作镜像、运行容器、上传镜像,也学会了怎么使用私有镜像仓库,最终在完全兼容的前提下成功改造了系统的部署方式。不过,我们还仅仅是跑通了部署流程,未曾修改 CI/CD 配置,这个后续再做分享。

相关推荐
苹果醋32 分钟前
Java8->Java19的初步探索
java·运维·spring boot·mysql·nginx
二十雨辰16 分钟前
[linux]docker基础
linux·运维·docker
qq_3901617737 分钟前
防抖函数--应用场景及示例
前端·javascript
time never ceases1 小时前
使用docker方式进行Oracle数据库的物理迁移(helowin/oracle_11g)
数据库·docker·oracle
雨雪飘零1 小时前
Windows系统使用OpenSSL生成自签名证书
nginx·证书·openssl
John.liu_Test1 小时前
js下载excel示例demo
前端·javascript·excel
Yaml41 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事1 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶1 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json
getaxiosluo1 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx