前情提要
之前在阿里云以近乎白嫖的价格买了一台云服务器,但是快到期了,如果按这个配置续费的话,费用比较高,所以又以新用户的身份在腾讯云白嫖了一台云服务器。
新服务器有了,接下来的问题就是如何把之前部署在阿里云的服务和代码迁移到新买的腾讯云上了。
老的服务器上是按传统方式安装的 node、nginx、jenkins 等,虽然最终实现了相对便捷的项目发布流程,不过其安装、配置是挺繁琐的。由于 Docker 的存在,可以使这一切变的相对简单了,所以就想在新服务器上使用 Docker 来部署一个完整的前端项目发布系统。
此次部署以这个站点为例:ssr.mimei.net.cn ,这是一个 VUE 的服务端渲染项目,功能简单,但包含了 node、php 服务,要使它正常的运行起来,需要使 git
、nodejs
、php
、mysql
、nginx
、jenkins
这些软件相互配合工作,可以说是一个比较完整的项目,用来做演示再合适不过了。
这篇文章的主要内容
总的来说要想完成站点的部署、代码的发布,需要分以下几个部分:
- 搭建 Git 服务器
- 生成 Mysql 容器并迁移数据库
- 生成 php 容器
- 生成 nodejs 容器
- 生成 Nginx 容器并进行配置
- 生成 Jenkins 容器并初始化数据
- 使用 docker-compose 管理多个容器
- 配置 jenkins 实现项目自动化发布
以上项目依次进行,最终配置一个完整的前端项目发布系统。
发布流程
最终将要实现的是,提交本地代码至远程(git私服 or github),登录 Jenkins 手动发布项目。
发布流程图如下:
graph LR
修改本地代码 --> push至远程 --> Jenkins操作发布 --> 发布完成
约定
由于涉及到代码迁移,这里做一个约定
老机器
- 指我的阿里云的机器,即将过期
新机器
- 指腾讯云的机器,新买的;会把数据从老机器
迁移至这里,新的服务也会部署在这里。
其他
关于这个站点 https://ssr.mimei.net.cn
、关于 vue ssr,我之前还写过一篇 文章,回头一看,已经快3年了~
这篇文章会涉及一点点的 Linux、 nginx、docker 知识,如果不熟悉的话可以先找找其他博文了解一下。
下面开始正式内容
搭建Git服务器
项目开始之需要考虑的一个问题是:代码存在哪里?
通常以下两个方向:
- 免费托管开源代码的远程仓库,如:github,码云等
- 自己抢建一台Git服务器
为提供更全面的演示,我把前端的 vue 代码存在 guthub,服务端 api 代码(php)存在了自建的Git服务器上。
略过 guthub 部分操作。
下面开始在服务器上创建 Git 服务,登录 新机器
:
创建 git 用户
adduser git # 创建一个git用户,用来运行git服务
passwd git # 设置用户密码,后期pull,push等操作通过密码进行
# 创建的git用户不允许登录shell,这可以通过编辑/etc/passwd文件完成。找到类似下面的一行:
git:x:1001:1001:,,,:/home/git:/bin/bash
# 改为:
git:x:1001:1001:,,,:/home/git:/usr/bin/git-shell
# 通过这个操作,这个 git 用户只能用来进行资源的操作,而不能通过shell登录机器
复制代码
安装 git
yum install git # 安装 git
mkdir /opt/git && cd /opt/git
# 创建 git 目录,后续所有的 git 项目文件都放在这个目录下
# 进入到 /opt/git 目录
# 为方便管理,我在 /opt/git 下又创建了子目录 mimei.net,专门存放这个项目相关的文件
git init --bare mimei.api.git
# --bare 创建一个没有当前工作目录的裸仓库
chown -R git:git mimei.api.git
# 把 owner 改为 git
# 至此,我们创建好了一个裸仓库,他的地址是:git@82.156.7.129:/opt/git/mimei.net/mimei.api.git
复制代码
代码存取
回到 老机器
,原始的 api 代码存在这里,需要把这部分代码转移到新机器
上:
# 进入 api 项目(php 代码)目录
git init # 初始华 git 项目
git add .
git commit -m 'init'
git remote add origin git@http://82.156.7.129:/opt/git/mimei.net/mimei.api.git # 添加远程地址
git push origin master # 将代码 push 至远程 新机器
# 以下为提示内容,输入之前设定的 git 用户的密码,代码推送成功
git@82.156.7.129's password:
枚举对象: 155, 完成.
对象计数中: 100% (155/155), 完成.
使用 4 个线程进行压缩
压缩对象中: 100% (122/122), 完成.
写入对象中: 100% (155/155), 145.70 KiB | 36.42 MiB/s, 完成.
总共 155 (差异 18),复用 155 (差异 18)
To 82.156.7.129:/opt/git/mimei.net/mimei.api.git
* [new branch] master -> master
# 至此,已经将服务端项目的代码放到新机器上了,只要有网络,随时都可以撸代码了~
复制代码
小结
项目的所需要的代码都准备好了:
VUE 代码:https://github.com/wfzong/mimei.web.ssr.git
,github 的常规操作这里就不再赘述。
服务端代码(php):git@82.156.7.129:/opt/git/mimei.net/mimei.api.git
接下来就看如何让项目跑起来了。
生成 Mysql 容器并迁移数据库
备份数据
接下来继续安装基础服务mysql,首先登录老机器
,将 mysql 数据进行备份
# https://www.jianshu.com/p/5ec717d629aa
Server version: 5.5.64-MariaDB MariaDB Server
MariaDB [(none)]> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| #mysql50#.git |
| SZBPY |
| limpid |
| mysql |
| performance_schema |
| phpmyadmin |
| wfzong |
| wordpress |
+--------------------+
# 备份数据库
mysqldump -u root -p --quick --databases DB1 DB2 > /data/db.sql
复制代码
备份完成以后,将数据从 老机器
,拷贝至 新机器
# 将数据拷贝至目标机器
scp /data/db.sql root@82.156.7.129:/data/fuzong.wang/
复制代码
生成容器
在 新机器
上运行 mysql
服务(除非特殊说明,本篇以下内容基本都在 新机器
进行)
docker run \
-it \
-d \
--rm \
--name minei.net-mysql \
-v /data/fuzong.wang/mysql/data:/var/lib/mysql \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=123456 \
mysql:5.7
# -it 参数:容器的 Shell 映射到当前的 Shell,然后你在本机窗口输入的命令,就会传入容器。
# -d 容器启动后,在后台运行。
# --rm 停止运行后,自动删除容器文件。
# --name minei.net-mysql:容器的名字叫做 minei.net-mysql。
# --v:将目录(/data/fuzong.wang/mysql/data)映射到容器的 /var/lib/mysql。因此,指定目录的任何修改,都会反映到容器里面,进而被外部访问到。
# -p 将容器的 3306 端口映射到 宿主机的的 3306 端口。
# -e 设置初始密码为 123456
# 运行起来以后,通过 docker cp 将sql文件(/data/fuzong.wang/db.sql)拷贝至 container 里。
# 然后在container里,通过 source 命令将 sql文件导入,完成数据库迁移。
复制代码
至此,已经可以通过新机器
的 ip 访问 mysql 服务资源了。
这里对各个参数做了解释,后面就不再一一注释了。
生成 php 容器
制作 php image
由于需要用到 php 的 mysql 扩展,所以默认的 php image 就不能满足需求了,需要自己制作 image 并添加相应的扩展。
新建一个 Dockerfile,写入如下内容:
FROM php:7.2-fpm
RUN apt-get update \
&& docker-php-ext-install pdo_mysql mysqli \
&& docker-php-ext-enable pdo_mysql mysqli
# 主要干了两件事:
# 1、引用php 7.2;
# 2、引入 mysql 扩展
# 有了 Dockerfile 文件以后,就可以使用 docker image build 命令创建 image 文件了
docker image build -t php7.2-with-extension:0.0.1
复制代码
生成容器
准备好了带有 mysql 扩展的 php image 后,创建 php 容器并运行:
docker run --name php.host \
-d --rm \
-v /var/www:/usr/share/nginx/html \
-p 9000:9000 \
php7.2-with-extension:0.0.1
复制代码
正常情况下,php 容器就已经运行起来了。
生成 nodejs 容器
vue ssr 需要一个 node 运行环境,所以通过 Dorcker ,在新机器
上搭建一个程序所需要的运行环境:
# 运行 docker
docker run --name minei.net-node.ssr \
-it -d \
--rm -p 8088:8088 \
-v /var/www/mimei.net.cn/ssr:/home/node \
node:10.10.0
# -v 目录(/var/www/mimei.net.cn/ssr)是宿主机存放 vue 代码的位置
复制代码
生成 Nginx 容器并进行配置
nginx 容器相对来说是改动比较频繁的,他有很多的配置文件要处理,需要将常规的 conf 文件映射到宿主机,还需要装日志文件、ssl 的 key 文件等,都分别映射出来,方便后期改动及查看。
创建容器
docker run --name mimei.net-nginx \
-d \
-p 80:80 \
-p 443:443 \
--link php.host:php.host \
--link minei.net-node.ssr:minei.net-node.ssr \
--privileged=true \
-v /var/www:/usr/share/nginx/html \ # 网站文件目录,映射至宿主机
-v /opt/webservice/nginx/log:/var/log/nginx \ # 日志文件
-v /opt/webservice/nginx/nginx.conf:/etc/nginx/nginx.conf \ # 默认的配置文件
-v /opt/webservice/nginx/conf.d:/etc/nginx/conf.d \ # nginx vhost 文件
-v /opt/webservice/nginx/cert:/etc/nginx/cert \ # 用于存放 https 的
nginx:1.16
复制代码
和之前相比,这里的命令多了两个参数 --link
,表示 nginx 容器要连接到 php 和 nodejs 容器,冒号后表示该容器的别名,在当前容器里可以通过别人直接访问相应的容器。
下面对 nginx 进行配置,将请求转发至相应的服务(nodejs、php)
# /opt/webservice/nginx/nginx.conf 这里有一行
include /etc/nginx/conf.d/*.conf; # nginx 会加载 conf.d 目录下的所有 .conf 文件
复制代码
配置 - 处理 php 请求
# 所以我们在 conf.d 目录下创建相应的文件
# api.mimei.net.cn_https.conf
server {
listen 443 ssl http2;
server_name api.mimei.net.cn;
...
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
location ~ .php$ {
fastcgi_pass php.host:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
...
}
# 大部分的配置都是常规的 nginx 配置
# fastcgi_pass php.host:9000; 会将 php 请求转发至 php 容器进行处理。
复制代码
配置 - 处理前端页面请求(vue ssr)
# 在 conf.d 目录下创建 ssr.mimei.net.cn_https.conf
server {
listen 443 ssl;
server_name ssr.mimei.net.cn;
...
location / {
proxy_pass http://minei.net-node.ssr:8088;
}
...
}
# proxy_pass http://minei.net-node.ssr:8088;将请求转发至 nodejs 容器
复制代码
小结
这里 nginx 主要作用就是对请求的进行转发,将 api 请求转发至 php 容器,将页面请求转发至 nodejs 容器,最终拼合成一个可运行的网站。
有两点需要注意一下:
- nginx 端口一定要映射至宿主机,我在做的时间忘了,总是找不到服务...
- php 容器的 www 目录要和 nginx 映射的一致,要不有可能找不到目标文件。
生成 Jenkins 容器并初始化数据
当一切的基础设施搭建好以后,Jenkins 其实会变成我们的主战场。
生成 Jenkins 容器
首先,下载 jenkins image
docker pull jenkins/jenkins:latest
docker inspect IMAGE_ID # docker inspect 获取容器/镜像的元数据
"ExposedPorts": {
"50000/tcp": {},
"8080/tcp": {}
},
...
"Volumes": {
"/var/jenkins_home": {}
}
# 可以看到 jenkins 的端口、工作目录等信息
# 为后面运行 jenkins 作准备
复制代码
下面开始让 jinkins 容器运行起来。
由于 docker 结束运行(删除container)后,相关的运行数据也会一并删除,所以需要将数据保存在宿主机,防止丢失。
mkdir /data/jenkins_home # 创建工作目录
docker run -d \
--name jenkins \
-p 8081:8080 \
-p 50000:50000 \
-v /data/jenkins_home:/var/jenkins_home \
jenkins/jenkins:latest
# -v 数据卷挂载映射(/data/jenkins_home:宿主主机目录,另外一个即是容器目录)
# jenkins:2.60.3 Jenkins镜像(当前最新版)
复制代码
正常情况下,jenkins 应当就可以运行起来了。
但是~
人生总是充满了意外
# 查看 container 的运行状态
docker container ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c9d351da530e jenkins:2.60.3 "/bin/tini -- /usr/l…" 4 minutes ago Exited (1) 4 minutes ago jenkins
# jenkins 运行后退出了
#进一步查看日志
docker logs jenkins
touch: cannot touch '/var/jenkins_home/copy_reference_file.log': Permission denied
Can not write to /var/jenkins_home/copy_reference_file.log. Wrong volume permissions?
# 权限问题
# 调整目录权限
# 容器中jenkins用户的 uid 为 1000
chown -R 1000:1000 /data/jenkins_home/
复制代码
重新 docker start CONTAINER_ID
,然后通过docker ps
可以看到,jenkins 运行起来了。
初始化数据
由于我将 jenkins 的商品映射到宿主机的 8081 端口,所以我需要在云服务器的管理后台开放这个端口
容器运行起来以后,可以通过 ip 地址来访问 Jenkins,基本就可以看到如下界面了。
按着提示进行操作,写入默认数据,Jenkins 就算初始化完成了。
注意,这里的 /var/jenkins_home/secrets/initialAdminPassword
是 container 内部的路径,我们已经将路径映射到宿主机了,实际的查看地址是:
cat /data/jenkins_home/secrets/initialAdminPassword
PASSWORD_STRING
复制代码
使用 docker-compose 管理多个容器
docker 虽然简化了各种环境的配置,但对于这个项目来说,这多个容器需要管理,都有不同的配置,时间一长可能有些具体内容就记不清了,其实管理起来还挺麻烦的。
docker-compose 给我们提供了一种便捷的管理方式,可以把相关容器的配置写进一个 docker-compose.yml
文件,写好多个容器之间的调用关系,然后只要一个命令,就能同时启动/关闭这些容器。
以咱们这个项目为例,将之前所有的配置信息,以 .yml 的形式写入配置文件中。
新建docker-compose.yml
文件,写入如下内容:
# 配置文档 https://docs.docker.com/compose/compose-file/compose-file-v3/
version: "3"
services:
mysql:
image: mysql:5.7
container_name: minei.net-mysql
networks:
- net
privileged: true
volumes:
- /data/fuzong.wang/mysql/data:/var/lib/mysql
ports:
- 3306:3306
nodejs:
image: node:10.10.0
container_name: minei.net-node.ssr
networks:
- net
stdin_open: true
tty: true
volumes:
- /var/www/mimei.net.cn/ssr:/home/node
# 启动成功以后,进入 container 里,docker container exec -it [container id] /bin/bash
# 启动 node 服务 cd /home/node && node ./server.js
php:
image: php7.2-with-extension:0.0.1
container_name: php.host
networks:
- net
volumes:
- /var/www:/usr/share/nginx/html
ports:
- 9000:9000
nginx:
image: nginx:1.16
container_name: mimei.net-nginx
networks:
- net
# external_links:
# - php.host:php-process
# - minei.net-node.ssr:node-server
volumes:
- /var/www:/usr/share/nginx/html
- /opt/webservice/nginx/log:/var/log/nginx
- /opt/webservice/nginx/nginx.conf:/etc/nginx/nginx.conf
- /opt/webservice/nginx/conf.d:/etc/nginx/conf.d
- /opt/webservice/nginx/cert:/etc/nginx/cert
ports:
- 80:80
- 443:443
jenkins:
image: jenkins/jenkins:latest
container_name: jenkins
networks:
- net
user: root
volumes:
- /data/jenkins_home:/var/jenkins_home
- /var/www:/var/www
ports:
- 8081:8080
- 50000:50000
networks:
net:
driver: bridge
复制代码
docker-compose.yml
中的内容和之前创建容器的命令基本一致,只是将命令按着 yml 格式重新写了一次。
只有一点不同,在用命令行创建容器的时候,用--link
指令,表示 A 容器要连接到 B 容器。这里使用 networks
将不同容器置于同一网络,这样容器就可被该网络中的其他容器访问。
配置文件写好以后,我们就可以通过一个命令启动和关闭所有任务。
# 启动所有服务
$ docker-compose up -d
# 关闭所有服务
$ docker-compose stop
复制代码
配置 jenkins 实现项目自动化发布
在准备好所有的代码、数据、容器等基础设施以后,最后就要通过 Jenkins 将这些资源串联起来。
创建 api 构建任务
开始之前需要额外操作的一点是,由于 php 项目是基于 thinkPHP 框架的,这个基础库没在放在 git 仓库里,需要手动从老机器
拷贝至新机器
# 进入api源码所在的目录
scp -r ThinkPHP/ root@82.156.7.129:/var/www/mimei.net/api
复制代码
然后进行构建任务的配置
创建一个自由风格的任务,命名为 mimei-api
源码管理选择 git,填入仓库地址,添加凭据信息。
类型选择:SSH Username with private key,填写当初创建 git 用户时的用户名和密码。
api 的构建 shell 相对比较简单,只需要将源码拷贝至 web 服务的目录,让 php 处理程序能访问到就可以了。
这里有一点要注意一下,为什么不同的容器(jenkins, nginx, php)可以直接访问访问相同的目录呢(/var/www
)?因为我们在创建不同容器的时候,将相关的功能目录都映射到宿主机的/var/www
目录了,所以这里可以很方便的访问目录中的文件,并且在其他容器中会同步更新,可以在docker-compose.yml
看到如下指令:
php:
...
volumes:
- /var/www:/usr/share/nginx/html
nginx:
...
volumes:
- /var/www:/usr/share/nginx/html
jenkins:
...
volumes:
- /var/www:/var/www
复制代码
经过如上配置,应当可以正确的构建项目了,下面对项目进行一下小的测试性修改:
$$ vim README.md
$$ git add .
$$ git commit -m 'add test string'
[master dc0c59c] add test string
1 file changed, 2 insertions(+), 1 deletion(-)
$$ git push
git@82.156.7.129's password:
枚举对象: 5, 完成.
对象计数中: 100% (5/5), 完成.
使用 4 个线程进行压缩
压缩对象中: 100% (3/3), 完成.
写入对象中: 100% (3/3), 320 字节 | 320.00 KiB/s, 完成.
总共 3 (差异 2),复用 0 (差异 0)
To 82.156.7.129:/opt/git/mimei.net/mimei.api.git
0b6558b..dc0c59c master -> master
复制代码
点击“立即构建”:
可以看到刚刚提交的代码已经更新。
由于 php 代码的更新不需要重新编译,也不用重启服务,至此,api 的构建过程就完成了。
创建 vue ssr 构建任务
vue 项目的发布和 php 项目类似,但就这个项目页言,有3点不同:
- 源代码存放在 github 上(php源代码存放在私有的git服务器上)
- 需要在服务端重新编译代码
- 重要重启 nodejs 服务
下面来具体看一下构建任务的创建过程
首先,构建一个名为mimei.ssr
的自由风格的软件项目
源码管理填上项目的github
地址,然后添加凭据,指定分支为 master
构建,将源码拷贝至 web 服务的目录。
至此,和上一个项目都基本一致,实现了代码的拉取,然后拷贝至目标目录;这个项目不一样的地方在于,它需要在服务端编译,然后重启服务。
由于在 jenkins 容器内无法直接将指令发送些 nodejs 容器,这里想到的办法是通过远程执行shell的方式,将指令送达至 nodejs 容器。
保存当前配置,返回 Jenkins 首页
Jenkins首页 -> 系统管理 -> 系统配置 -> SSH remote hosts
添加好主机和对应的凭据
再次进入构建任务的配置界面
增加构建步骤
里选择 Execute shell script on remote host using ssh
,并写下如下 shell
,简单做一下说明:
ls -l ~
docker exec -i minei.net-node.ssr /bin/bash \ # 在运行的容器中执行命令,minei.net-node.ssr 是 nodejs 容器的名称
-c \ # 将容器里传递指令
" \
cd /home/node \
&& npm install -g pm2 \ # 我是通过 pm2 来管理 nodejs 程序的
&& pm2 stop all \ # 停止所有的应用程序
&& ls -l \
&& npm install \ # 安装依赖
&& npm run build \ # 根据最新代码,重新 build
&& npm run start:pm2 \ # 启动应用程序
"
复制代码
这一切完成以后,保存然后构建,顺利的话应当可以看到如下的构建信息:
...
由于输出了大量的构建信息,这里我只截屏了一小部分。
这一切完成以后,就可以正常浏览网站了 https://ssr.mimei.net.cn
总结
这是一篇归纳总结似的文章,并不向一篇传统意义上的 ‘教程’,它更多的是记录我在配置这个前端项目发布系统的过程和思路。在实践的过程中,其实遇到很多的问题、很多的坑,边摸索边解决,最终形成的这篇文章,但是遇到的问题、解决的过程,大部分并没有在文章中体现,所以如果你在实践的过程中,相信一定也会遇到这样那样的问题,不要怕,搜索引擎基本都能帮忙我们搞定。
最后列一下对我帮助比较大的几个文档: