经过前前后后差不多一周的摸爬滚打,无数次的踩坑,几次想要放弃,终于还是完成了这个部署。
最大的收获还是在遇到问题时要尝试从多个角度思考,反复尝试。网上的文章质量确实良莠不齐,但大多数情况是作者与自己所遇到的问题存在差异,要仔细思考,深刻理解问题的本质是什么。另外,作者与自己的知识背景不同,可能他一笔带过的地方是自己的知识盲点,这就需要另外寻找参考资料。
对于一个复杂的问题而言,往往不可能在网上找到与自己的问题完全相同的参考文章。从自身知识背景和问题实际出发,找到参考文章中对自己有用的地方,经过反复搜索、尝试、拼凑,最终往往能找到属于自己的答案。
目的是:针对 ubuntu16(后续我们将其称为主机) 上运行的一个前后端分离项目,使用 docker 进行打包,之后将其部署到另一台机子(后续我们将其称为服务器)上,服务器上虽然运行的也是 ubuntu 系统,但没有界面,只有命令行可供交互,并且无法连接互联网(要是能联网,会省去很多问题,虽然核心的前后端跨域访问问题不会因此得到解决,但起码可以使用很多工具)。
我们从简单的讲起。虽然这期间也遇到了很多坑,但是对比而言,还是打包前端更恶心一些。
刚着手开始搞的时候,根本不知道 Dockerfile 是个啥东西,想想真是不容易。
简单来讲,Dockerfile 就是一个在构建镜像的时候 docker 会去参考的一个文件,我们只需要了解其中很简单的命令就足够用了。
好的我们正式开始打包:
首先,建议新建一个文件夹,然后把用到的项目代码拷贝出来,这样比较清晰。
对我们的项目而言,前后端项目文件夹分开,后端的代码都存在一个名字叫server
的文件夹中,可以将这个文件夹整体拷贝出来。
然后进入我们拷贝出来的server
文件夹,在里面创建一个名字叫 Dockerfile 的文件,就叫这个名字,也没有后缀,不要奇怪。
我的 Dockerfile 文件内容如下:
FROM node
WORKDIR /app
COPY ./package.json /app/
RUN npm install
COPY . /app/
EXPOSE 3000
CMD node bin/www
下面我们来一行一行解释这个文件。其实每个命令大致的作用也能从它的英文含义猜出来。
docker pull node
拉取一下。npm install
命令,安装相关依赖。在当前文件夹目录下,执行命令:
docker build -t express-demo .
注意最后的点不要忘记,它表示使用当前目录下的 Dockerfile 文件来构建镜像。
镜像的名字如命令中所示,我们指定为 express-demo。
此时镜像已经打包好了,很多教程和博文也都是这样一步一步下来的,接下来可以先在本机测试一下,即不运行原始的程序,而是根据刚刚打包好的镜像生成一个容器供我们访问,命令如下:
docker run -d -p 3333:3000 express-demo
3333 是外部机器的端口,也是我们要去访问的端口,3000是docker容器中暴露的端口。
好了,做到这里,我们兴冲冲地在浏览器输入localhost:3333
,(对于我所打包的项目来说)在浏览器中出现一行提示,表示没有 token 之类的。因此我判断我已经打包成功了,只要再通过前端输入用户名和密码就可以把整个项目连接起来了。
后来的事实证明,我还是太年轻了,等待着我的还有无数的坑。
按照前面打包后端 express 的步骤,看来打包也不是很难,于是我信心满满地开始打包 vue。
与后端不同,在打包前端 vue 项目时,我们首先需要在项目中运行 npm run build:prod
命令,生成 dist 文件,这个文件夹里面是一些静态文件,待会儿需要放到容器中。
就像刚才生成 express 项目镜像之前首先要拉取 node 镜像一样,我们首先需要运行以下代码拉取 nginx 镜像,后续 vue 镜像的构建是依赖于 nginx 镜像的。
docker pull nginx(这里加不加:latest应该都行)
下面我们来编写用于构建 vue 镜像的 Dockerfile 文件。
FROM nginx:latest
COPY /web/front/dist/ /usr/share/nginx/html
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
这里有一个大坑。
首先第一行没啥问题,只是你需要看你自己的 nginx 名字是啥,是有 latest 还是没有。
第二行也没啥问题,就是把刚才生成的 dist 文件夹下的所有内容拷贝到 docker 容器中的相应的目录下。第一个目录看自己的设置,把 dist 文件夹放在哪就写哪儿的目录,后面的目录是固定的,不可改动。
重点是第三行。
第三行命令看上去和第二行差不多,就是将一个文件复制到 docker 容器中,代替一个同名文件。问题在于这个文件是 nginx 设置的核心文件。
一开始我是没有加第三行的,经过一系列折腾,千难万险,踩坑无数后,我终于打包成功并将镜像在服务器上加载,然后生成了一个容器。
怀着激动的心情,我进行了测试。可结果却大失所望,跟我之前遇到过的问题一样,出现了找不到后端接口的情况,就好像我之前解决问题解决了个寂寞。
经过反复查找,苦苦思索之后,我进入服务器上正在运行的容器中查看了当前的 default.conf 文件,发现里面根本就没有配置我后来改动的后端接口信息。
这时候正常的想法应该是改一下这个文件,然后重新启动 nginx 服务。但是前面已经说过,服务器是没网的,虽然服务器里面装了 vim ,但是 docker 容器里面的 vim 要另外装。我搜了半天,也没有发现系统自带的能够编辑文件内容的命令,网上都说用 vim,可是我没有啊!(查看文件我用的是 cat 命令)。
问题在于我们在打包生成镜像、运行容器之后无法去修改里面的 default.conf 文件,或者说,修改是可以,但是当你把镜像导出的时候,它里面的 default.conf 还是你没有修改之前的样子。
所以我们必须要在主机上自己编写一个 default.conf 文件,并在生成镜像的时候就把它复制进镜像里,替换掉默认的配置文件。这个文件我放在了 /nginx 文件夹下,这个文件夹是我自己建的,其实 default.conf 随自己心情放在哪里都可以,只要和 Dockerfile 中的目录对应上就行。这也就是为什么要在 Dockerfile 文件中加上第三行。
default.conf 文件内容先放在这里,后续再进行解释。
server {
listen 80;
listen [::]:80;
server_name localhost;
access_log /var/log/nginx/host.access.log main;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
运行以下命令:
docker build -t web-front:latest .
注意文件目录都要对上。你 Dockerfile 中是怎么写的,外面的目录就怎么安排,而且 Dockerfile 要在运行目录下(前面说过,后面的点表示使用当前目录下的 Dockerfile)。
docker run -itd --name web-front-1 -p 2222:80 web-front:latest
此时容器已经运行,docker ps
查看容器的 id,然后docker exec -it 容器id bash
进入容器,运行/usr/sbin/nginx
开启 nginx 服务。
此时,在浏览器输入 localhost:2222 就可以看到登录界面。
此时后端和前端都已经打包好,并且已经成功运行。
在前端页面点击登录,果不其然,报错了。
其实想想也知道,这时候前端跟后端没有发生任何联系,前端不会自己去找后端,后端虽然在代码中设置了会自己去找 mysql 数据库,但打包完和打包之前还一样吗?心里也会隐隐觉得是有问题的。
这时候就要回过头去看 nginx 的 default.conf 文件,要在这个里面配置跨域转发接口。于是修改 default.conf 文件如下:
server {
listen 80;
listen [::]:80;
server_name localhost;
access_log /var/log/nginx/host.access.log main;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
location ^~ /prod-api/ {
proxy_pass http://192.168.1.25:3333/;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
对比刚才的配置文件,这里增加了下面三行代码:
location ^~ /prod-api/ {
proxy_pass http://192.168.1.25:3333/;
}
这三行是配置跨域转发的关键,一个斜杠都不能错。当然,里面的 ip 地址和端口号要换成对应的。这里面还要注意一点: 如果是在主机上测试,那么 ip 地址要写主机的;如果测试好了要打包传送到服务器上去了,ip 地址要写服务器的。
这时候你可能要说,那我直接写 locahost 不就得了,我试过了是不行的,你可以自己试一下。
这时候前端应该已经没啥问题了。如果运行原项目的后端服务(默认是在 3000 端口),再运行我们打包好的前端,就可以从前端正常访问了。注意要把 default.conf 中的端口号改成3000。
但是如果我们访问打包好的后端,就会发现是访问不到的。
这个问题实在古怪,后来反复查证之后发现问题出在了 mysql 数据库上。
直接运行后端服务和在 docker 中运行的区别在这里表现地极为明显。
问题的解决我参考了这篇文章:https://blog.csdn.net/ii19910410/article/details/88667899
主要意思是说如果你在 docker 中运行的话,后端服务去访问数据库的 ip 是会发生变化的。
const Sequelize = require('sequelize');
const url = 'mysql://root:123456@localhost:3307/student';
const mysql = new Sequelize(url, {
define: {
timestamps: false,
charset: 'utf8'
},
// 连接池设置
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
}
})
需要将上述代码 url 中的地址改成172.17.0.1,这个地址是根据 docker 与宿主机通信的具体情况而定的,一般都是上述地址,但还是建议查看一下。
于是我们修改项目中连接数据库的代码:
const Sequelize = require('sequelize');
const url = 'mysql://root:[email protected]:3307/student';
const mysql = new Sequelize(url, {
define: {
timestamps: false,
charset: 'utf8'
},
// 连接池设置
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
}
})
再按照上面的步骤重新打包,运行。
这时候再通过前端访问,成功。
终于打包成功,在主机上也运行成功了,此时我觉得已经可以开始庆祝了。
因为把打包好的镜像传输到服务器上,再在上面运行,应该只是工作量的问题罢了。但事实证明远没有我想得那么简单。
首先,镜像的导入导出可以参考这篇文章
https://blog.csdn.net/ncdx111/article/details/79878098
用 save 和 load 命令就可以了。
下一步是应该只要运行容器就大功告成。可是发现前端 vue 项目运行不起来,报错是端口占用。还有什么关于 ip6 的奇奇怪怪的错误。反正报什么错就去查,然后解决。我是先把 nginx 服务关掉,然后在 default.conf 中加了关于 ip6 的配置后成功的(上面的 default.conf 是加过的版本)。
在服务器上的 mysql 是运行在 3306 端口的,需要重新运行一个 3307 端口的,反正就是和后端的配置保持一致。
并且像之前说的,要改一下 ip,因为是在 docker 容器中运行后端服务而不是直接在宿主机。
前后端服务都正常运行,并且可以访问了之后,在前端点击登录还是报错,并且容器会自动退出,也就是崩掉了。
最后在容器中查看后端运行数据才发现是因为请求数据库中的一个表还没建。
补充一下,不管是主机和服务器,我的 mysql 都是运行在 docker 容器中的,访问端口是 3307.
感谢下面几篇博文为我指明方向:
欢迎留言交流,祝你们成功。