背景
在新公司待得这段时间,学到了很多知识,毕竟之前自己在开发工作上一直是单打独斗(自己设计架构,自己管理项目,自己处理问题,自己学习技能),虽然听上去不错,自由自在的建设管理前端项目,但是,当我来到大一些的互联网公司工作后,我发现以前真的是在浪费时间,在大一些的技术团队工作,你可以更高效率的学习很多没接触过的技能、工具、架构、语言以及规范化。之后我会慢慢在这里写一些我学到的新知识,分享给各位。
Docker
Docker是做什么的?
通俗的理解:Docker 是一个开源的应用容器引擎,可以灵活创建/销毁/管理多个容器 (container),这些容器完全使用沙箱机制,相互之间不会有任何接口(类似 iPhone 的 app),且性能开销极低。在容器中你可以做任何服务器可以做的事,例如在有 node 环境的容器中运行 npm run build 打包项目,在有 nginx 环境的容器中部署项目,在有 mysql 环境的容器中做数据存储等等。当服务器安装了 Docker ,就可以自由创建任意多的容器. Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。
安装Docker
以 Mac pro 笔记本电脑来讲解。
登录阿里云
打开终端,输入登录阿里云命令:
ssh -t 阿里云用户名@阿里云公网IP地址 -p 端口
输入阿里云服务器密码,当你看到如下提示就说明登录阿里云远程服务器成功!
Welcome to Alibaba Cloud Elastic Compute Service !
根据官方安装教程,执行如下命令:
执行安装命令
# Step 1: 安装必要的一些系统工具
sudo yum install -y yum-utils
# Step 2: 添加软件源信息,使用阿里云镜像
sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
# Step 3: 安装 docker-ce
sudo yum install docker-ce docker-ce-cli containerd.io
# Step 4: 开启 docker服务
sudo systemctl start docker
# Step 5: 运行 hello-world 项目
sudo docker run hello-world
若出现如下图示,表示安装成功!
安装 git
自动化部署涉及到拉取最新的代码,因此需要在服务器中安装 git
yum install git
安装 node
既然是前端自动化部署,云服务器上相关处理逻辑用 js 编写,所以需要安装 node 环境。
wget https://nodejs.org/dist/v14.17.1/node-v14.17.1-linux-x64.tar.xz
tar xvf node-v14.17.1-linux-x64.tar.xz
ln -s /root/node-v14.17.1-linux-x64/bin/node /usr/local/bin/node
ln -s /root/node-v14.17.1-linux-x64/bin/npm /usr/local/bin/npm
node -v npm -v
至此,Node.js环境已安装完毕。软件默认安装在/root/node-v14.17.1-linux-x64/目录下。如果需要将该软件安装到其他目录(如:/opt/node/)下,请进行如下操作:
mkdir -p /opt/node/
mv /root/node-v14.17.1-linux-x64/* /opt/node/
rm -f /usr/local/bin/node
rm -f /usr/local/bin/npm
ln -s /opt/node/bin/node /usr/local/bin/node
ln -s /opt/node/bin/npm /usr/local/bin/npm
安装 pm2
node 安装完成后,还需要安装 pm2,它能使你的 js 脚本能在云服务器的后台运行
npm i pm2 -g
如果出现pm2找不到的问题,执行:
ln -s /root/node-v14.17.1-linux-x64/lib/node_modules/pm2/bin/pm2 /usr/local/bin
github 配置 webhook
理解:webhook其实就是一个钩子,当你的仓库被提交更新后,就会触发webhook钩子调用 Payload URL 配置好的接口地址,告诉阿里云服务器 Docker 创建的对应容器,根据 DockerFile 文件里面的内容执行一系列操作部署。
登录 github
点击你要部署的前端项目仓库
点击右上角的 Add webhook 按钮
测试webhook
配置完成后,可以向仓库提交一个 commit,推送后打开 webhook,就会发现如图,说明配置成功
项目根目录创建 Dockerfile
理解:根据Dockerfile的内容,创建镜像。
# dockerfile 两步
# 第一步:构建
FROM node:lts-alpine as build-stage
# 将工作区设为 /app,和其他系统文件隔离
WORKDIR /app
# 利用镜像缓存,package.json不改变不会从新拉取依赖
COPY package*.json ./
COPY yarn.lock .
RUN yarn cache clean
# 下载依赖包
RUN yarn
COPY . .
# 构建
RUN yarn build
# 第二步:生产 这个nginx是容器里面的,并不是阿里云服务器的
FROM nginx:stable-alpine as production-stage
# 将build-stage产物从app/dist文件夹复制到/usr/share/nginx/html/
COPY --from=build-stage /app/dist /usr/share/nginx/html/
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
scp ./Dockerfile root@118.89.244.45:/root
根目录创建 .dockerignore
结束:类似 .gitignore,.dockerignore 可以在创建镜像复制文件时忽略复制某些文件
# .dockerignore
node_modules
由于需要保持本地和容器中 node_module 依赖包一致,在创建 Dockerfile 时用了两次 COPY 命令 第一次只复制 package.json 和 package-lock.json,并安装依赖 第二次复制除 node_modules的所有文件 接着将 .dockerignore 文件也复制到云服务器上
scp ./.dockerignore root@118.89.244.45:/root
创建镜像和容器
在创建新容器前,需要先把旧容器销毁,这里先介绍几个用到的 docker 命令:
docker ps -a -f "name=^docker" --format="{{.Names}}"
查看所有 name 以 docker 开头的 docker 容器,并只输出容器名
docker stop docker-container
停止 name 为 docker-container 的容器
docker rm docker-container
删除 name 为 docker-container 的容器(停止状态的容器才能被删除)
创建 http 服务器
根目录新建 index.js 此处的端口 3000 必须与 webhook 配置的 Payload URL 保持一致!
const http = require("http")
const {execSync} = require("child_process")
const fs = require("fs")
const path = require("path")
// 递归删除目录
function deleteFolderRecursive(path) {
if( fs.existsSync(path) ) {
fs.readdirSync(path).forEach(function(file) {
const curPath = path + "/" + file;
if(fs.statSync(curPath).isDirectory()) { // recurse
deleteFolderRecursive(curPath);
} else { // delete file
fs.unlinkSync(curPath);
}
});
fs.rmdirSync(path);
}
}
const resolvePost = req =>
new Promise(resolve => {
let chunk = "";
req.on("data", data => {
chunk += data;
});
req.on("end", () => {
resolve(JSON.parse(chunk));
});
});
http.createServer(async (req, res) => {
console.log('receive request')
console.log(req.url)
if (req.method === 'POST' && req.url === '/') {
const data = await resolvePost(req);
const projectDir = path.resolve(`./${data.repository.name}`)
deleteFolderRecursive(projectDir)
// 拉取仓库最新代码 注意!! 这里的仓库地址要把https改成git
execSync(`git clone git://github.com/仓库路径/${data.repository.name}.git ${projectDir}`,{
stdio:'inherit',
})
// 复制 Dockerfile 到项目目录
fs.copyFileSync(path.resolve(`./Dockerfile`), path.resolve(projectDir,'./Dockerfile'))
// 复制 .dockerignore 到项目目录
fs.copyFileSync(path.resolve(__dirname,`./.dockerignore`), path.resolve(projectDir, './.dockerignore'))
// 创建 docker 镜像
execSync(`docker build . -t ${data.repository.name}-image:latest `,{
stdio:'inherit',
cwd: projectDir
})
// 销毁 docker 容器
execSync(`docker ps -a -f "name=^${data.repository.name}-container" --format="{{.Names}}" | xargs -r docker stop | xargs -r docker rm`, {
stdio: 'inherit',
})
// 创建 docker 容器
execSync(`docker run -d -p 8002:80 --name ${data.repository.name}-container ${data.repository.name}-image:latest`, {
stdio:'inherit',
})
console.log('deploy success')
res.end('ok')
}
}).listen(3000, () => {
console.log('server is ready')
})
通过 scp 复制到云服务器上,在vscode终端执行即可。
scp ./index.js root@118.89.244.45:/root
宝塔放行端口
如果是用宝塔面板安装的nginx,记得放行一下端口
运行 node 脚本
通过之前安装的 pm2 将 index.js 作为后台脚本在云服务器上运行(此时记得关掉翻墙的工具软件如蓝灯等,避免链接失败)
pm2 start index.js
启动成功后,访问云服务器 80 端口看到部署的 demo 项目(访问前确保服务器已开放 8888 端口)
运行日志
云服务器上运行 pm2 logs 查看 index.js 输出的日志,随后本地添加 hello docker 文案,并推送至 github
写在后面
上述 demo 只创建了单个 docker 容器,当项目更新时,由于容器需要经过销毁和创建的过程,会存在一段时间页面无法访问情况 而实际投入生产时一般会创建多个容器,并逐步更新每个容器,配合负载均衡将用户的请求映射到不同端口的容器上,确保线上的服务不会因为容器的更新而宕机
另外基于 github 平台也有非常成熟的 CI/CD 工具,例如
travis-ci circleci
通过 yml 配置文件,简化上文中注册 webhook 和编写更新容器的 index.js 脚本的步骤
.travis.yml language: node_js node_js:
- 8
branchs:
only:
- master cache: directories:
- node_modules install:
- yarn install scripts:
- yarn test
- yarn build 复制代码另外随着环境的增多,容器也会逐渐增加,docker 也推出了更好管理多个容器的方式 docker-compose