技术栈:
- docker
- node
- pm2
- shell
- webhook
docker
docker是什么:docker可以灵活的创建/销毁/管理多个容器 (container) 在容器中可以搭建任何你想要的环境,安装docker后可以自由创建任意多的容器
安装
点击安装docker 下载完成后重启电脑,打开控制台输入docker -v查看是否安装成功
重要概念
docker的三个概念
- 镜像(image)
- 容器(container)
- 仓库(repository)
镜像就是容器模板,一个镜像可以创建多个容器,好比js中类和示例的关系
获取镜像有两种方式
- 创建Dockerfile文件
- 使用dockerHub或者其他来源的镜像
Dockerfile
Dockerfile文件就是一个docker镜像的配置文件,定义如何生成镜像
创建文件
创建文件index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h2>hello docker</h2>
</body>
</html>
创建Dockerfile文件
FROM nginx
COPY index /usr/share/nginx/html/index.html
EXPOSE 80
- FROM nginx:基于官方nginx镜像
- COPY index /usr/share/nginx/html/index.html:当前文件夹下的index.html替换容器的/usr/share/nginx/html/index.html
- EXPOSE 80:容器对外暴露80端口(容器内的端口,真实端口需要在创建容器的时候定义)
创建镜像(image)
当前目录下打开控制台运行以下命令构建镜像:
docker build . -t test-image:latest
- build:创建镜像
- .:使用当前目录下的Dockerfile文件
- -t:使用tag标记当前镜像版本
- test-image:指定镜像名
- :latest:指定tag版本
查看镜像
docker images
REPOSITORY有一个名字为test-image说明镜像已经创建成功
创建容器(container)
docker run -d -p 80:80 --name test-container test-image:latest
- run:创建并运行docker容器
- -d:后台运行容器
- 80:80:将当前服务器的80端口(冒号前)映射到容器的80端口(冒号后)
- --name:指定容器名字
- test-image:latest:使用的镜像名字。
浏览器打开localhost会显示index.html的内容
查看容器
docker ps -a
- ps:查看docker容器(正在运行中的)
- -a:查看所有容器,包括未在运行状态的容器
dockerHub
github是储存代码的仓库,dockerHub是储存镜像的仓库。开发者可以将Dockerfile生成的镜像上传到dockerHub来存储自定义镜像,也可以直接使用官方提供的镜像。
docker pull nginx
docker run -d -p 81:80 --name nginx-container nginx
由于上一步本地80端口已经被占用了,所以这次使用81端口来映射容器的80端口,浏览器访问localhost:81应该就可以看到Welcome tonginx了。
为什么要用docker
- 环境统一 docker解决了一个世纪难题:在我电脑上明明是好的呀 开发者可以将开发环境用docker镜像上传到docker仓库,在生产环境中拉取并运行相同镜像,保持环境一致。 注册dockerHub账号并登录
docker login # 本地docker登录dockerHub账号
docker build . -t 你的账号/docker-image-test:0.1 # 构建用于上传到dockerHub的镜像(需要加上账号前缀)
docker push 你的账号/docker-image-test:0.1 # 提交名为docker-image-test的镜像。
# 打开dockerHub查看有没有上传成功,成功继续往下执行
下面测试拉取镜像
docker images -a # 查看本地镜像并复制刚刚构建的镜像的id
docker rmi 复制的id # 删除本地刚才构建的镜像
docker images -a # 查看并确认已经删除成功
docker pull 你的账号/docker-image-test:0.1 # 拉取账号下的docker-test-image镜像
docker images -a # 确认拉取成功
- 便于回滚 在创建镜像是可以使用tag标记当前版本,如果某个版本的环境有问题,可以快速回滚到之前的版本。
- 环境隔离 使用cocker可以使服务器更干净,构建用到的环境可以都放在容器中。
- 高效/节省资源 相对于真实服务器和虚拟机,容器不包含操作系统。容器的创建和销毁都十分高效。
其他常用命令
- 停止容器:docker stop 容器id
- 启动容器:docker start 容器id
- 删除容器:docker rm 容器id(必须先停止)
- 查看容器日志:docker logs 容器名字
- 进入容器:docker exec(进入容器) -it(分配一个伪终端,即使没有附加也保持STDIN打开) 容器名字 /bin/sh
- 容器与主机间数据拷贝:docker cp 容器名字:容器路径 本地路径
前端自动化部署
在没有使用自动化部署之前,想要更新网站内容需要
- git push提交代码
- 本地运行npm run build生成构建物
- 将构建产物通过ftp等形式上传到服务器
实现自动化部署之后
- git push提交代码
- 服务器自动更新镜像
- 镜像中自动运行npm run build生成构建物
- 服务器自动创建容器
可以发现实现自动化部署后,开发者只需要将代码提交到仓库,其他的事情都可以通过服务器上的的脚本自动化完成。
服务器
首先你得有一台服务器,以下内容以 centos7 系统为例。
安装环境
- docker 本地有了docker环境,服务器也需要安装docker
curl -sSL https://get.daocloud.io/docker | sh # 安装docker
systemctl start docker # 启动docker
- git 自动化部署需要涉及到拉取最新代码,所以这里安装一下git环境
yum install git
- node
curl -sL https://rpm.nodesource.com/setup_10.x | bash - # 设置nodesource
yum -y install nodejs # 安装node
node -v
- pm2 可以后台运行脚本
npm i pm2 -g
创建项目
创建本地项目
npx create-react-app docker-test
打开gitee(github也可,这里我用gitee),创建项目 打开本地项目进入控制台
git remote add origin gitee项目地址
git push -u origin master (可能会弹出登录,输入gitee账号密码即可)
当控制台显示Branch 'master' set up to track remote branch 'master' from 'origin'.时,刷新浏览器可以看到远程仓库已经创建完毕。
webhook
打开远程仓库,点击管理——webhooks——添加webhook
- URL:填写服务器地址,需要加http或https协议,端口号暂定3000
- WebHook 密码/签名密钥:不需要填写
- 选择事件:选择默认的Push,当仓库发生push事件时,gitee会自动向我们填写的url发送POST请求。
- 激活:开启webhook
处理项目更新请求
当服务器接受到项目更新后发送的post请求后,需要创建/更新镜像来实现自动化部署。
创建Dockerfile
打开本地项目 新建一个Dockerfile文件,用于构建镜像
# build stage
FROM node:lts-alpine as build-stage
WORKDIR /app
COPY . .
RUN npm i
RUN npm run build
# production stage
FROM nginx:stable-alpine as production-state
COPY --from=build-stage /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
build stage构建阶段
- FROM node:lts-alpine as build-stage:使用node镜像,版本为lts-alpine(比latest版本小巧,更适合作为docker镜像使用)
- WORKDIR /app:将工作区设置为/app,和其它文件系统隔离
- COPY . . :拷贝文件到容器的/app目录
- RUN npm i:打开控制台执行npm i
- RUN npm run build:打开控制台执行npm run build
production stage部署阶段
- FROM nginx:stable-alpine as production-stage:使用stable-alpine版本的nginx镜像,并将该阶段命名为production-stage
- COPY --from=build-stage /app/build /usr/share/nginx/html:--from=build-stage可以从build-stage阶段读取文件,所以当前命令是从build-stage阶段的/app/build拷贝到本阶段/usr/share/nginx/html下
- EXPOSE 80:容器对外暴露80端口
- CMD ["nginx", "-g", "daemon off;"]:CMD对应的命令结束后容器就会被销毁,容器创建后运行nginx -g daemon off命令,可以使nginx一直在前台运行
本地项目里打开控制台
# 复制当前文件夹下的Dockerfile到远程服务器,我的登录账号为root,服务器地址47.103.74.196,目标目录为/root
scp ./Dockerfile root@47.103.74.196:/root
# Are you sure you want to continue connecting (yes/no)? yes
# root@47.103.74.196's password: 输入连接密码(看不到)
创建.dockerignore
类似于.gitignore,.dockerignore可以在创建镜像复制文件时忽略某些文件。
在本地项目中创建.dockerignore文件
# .dockerignore
node_modules
复制.dockerignore到服务器
scp ./.dockerignore root@47.103.74.196:/root
创建http服务器
或许你创建webhook后已经发现了,远程仓库发送的测试请求超时了,所以需要创建一个http服务来处理webhook发送的post请求 (需要在安全组打开此端口,阿里云栗子:ecs.console.aliyun.com/#/securityG…) 本地项目创建文件index.js
const http = require('http')
http.createServer(async (req, res) => {
if (req.method === 'POST' && req.url === '/') {
// ......
}
res.end('success')
}).listen(3000, () => {
console.log('server is ready')
})
拉取仓库代码
仓库触发push事件,服务器收到post请求之后需要拉取最新代码 修改index.js
const http = require('http')
const path = require('path')
const fs = require('fs')
const { execSync } = require('child_process')
const repository = 'docker-test'
const deleteFolderRecursive = path => {
if (fs.existsSync(path)) { // 路径存在
fs.readdirSync(path).forEach(v => { // 读取该路径下的文件
const curPath = path + '/' + v
if (fs.statSync(curPath).isDirectory()) {
deleteFolderRecursive(curPath) // 如果是一个文件夹,则递归删除
} else {
fs.unlinkSync(curPath) // 直接删除文件
}
})
fs.rmdirSync(path)
}
}
http.createServer(async (req, res) => {
let projectDir = ''
if (req.method === 'POST' && req.url === '/') {
console.log('收到请求', req.url)
projectDir = path.resolve(`./${repository}`) // 定义项目目录
deleteFolderRecursive(projectDir) // 递归删除原来克隆的项目
execSync(`git clone https://gitee.com/zhangfangbiao/${repository}.git`, { // 开始克隆
stdio: 'inherit'
})
}
res.end(projectDir)
}).listen(3000, () => {
console.log('server is ready')
})
创建镜像和容器
上面步骤已经可以成功拉取代码了,现在需要做的就是使用Dockerfile创建镜像,更新容器。 修改index.js
const http = require('http')
const path = require('path')
const fs = require('fs')
const { execSync } = require('child_process')
const repository = 'docker-test'
const deleteFolderRecursive = path => {
if (fs.existsSync(path)) { // 路径存在
fs.readdirSync(path).forEach(v => { // 读取该路径下的文件
const curPath = path + '/' + v
if (fs.statSync(curPath).isDirectory()) {
deleteFolderRecursive(curPath) // 如果是一个文件夹,则递归删除
} else {
fs.unlinkSync(curPath) // 直接删除文件
}
})
fs.rmdirSync(path)
}
}
http.createServer(async (req, res) => {
let projectDir = ''
if (req.method === 'POST' && req.url === '/') {
console.log('Received request', req.url)
projectDir = path.resolve(`./${repository}`) // 定义项目目录
deleteFolderRecursive(projectDir) // 递归删除原来克隆的项目
execSync(`git clone https://gitee.com/zhangfangbiao/${repository}.git`, { // 开始克隆
stdio: 'inherit'
})
// 复制Dockerfile到项目目录
fs.copyFileSync(path.resolve('./Dockerfile'), path.resolve(projectDir, './Dockerfile'))
// 复制.dockerignore到项目目录
fs.copyFileSync(path.resolve('./.dockerignore'), path.resolve(projectDir, './.dockerignore'))
// 创建镜像
execSync(`docker build . -t ${repository}-image:latest`, {
stdio: 'inherit',
cwd: projectDir,
})
// 销毁容器(查找名字为repository-container开头的容器并停止,然后删除)
execSync(`docker ps -a -f "name=^${repository}-container" --format="{{.Names}}" | xargs -r docker stop | xargs -r docker rm`, {
stdio: 'inherit',
})
// 创建新的容器(使用repository-image:latest镜像构建容器并把容器命名为repository-container,本地8888端口映射容器的80端口)
execSync(`docker run -d -p 8888:80 --name ${repository}-container ${repository}-image:latest`, {
stdio: 'inherit'
})
console.log('deploy success')
}
res.end(projectDir)
}).listen(3000, () => {
console.log('server is ready')
})
服务器开启服务
scp ./index.js root@47.103.74.196:/root
然后再服务器运行
# 使用pm2后台运行index.js脚本文件,并命名为webhook
pm2 start /root/index.js --name webhook
pm2 logs webhook
# 查看日志
触发webhook
试一下能否正常运行,提交代码然后push,打开pm2日志,可以看到Received request已经可以正常打印,下面开始克隆代码构建镜像 当打印deploy success后,浏览器打开47.103.74.196:8888就可以看到项目正在运行了。 整个过程
- 提交代码并push
- gitee触发POST请求到服务器的3000端口
- 服务器收到请求
- 定义项目目录,删除原来的项目,并克隆最新代码
- 复制Dockerfile和.dockerignore到项目目录
- 创建镜像
- 使用node镜像
- 设置工作目录为/app
- 复制代码到工作目录
- 安装依赖
- 打包
- 使用nginx镜像
- 复制构建产物,从/app/build到容器的/usr/share/nginx/html文件夹下
- 容器暴露80端口
- 使nginx在容器中保持前台运行
- 更新容器,把容器的80端口映射到本地的8888端口