项目部署采坑记录

项目部署整体流程

背景

本来是写业务的, 突然说有客户想要部署项目到自家的服务器上.
那我就懵了啊, 以前可没搞过这种SaaS服务啊.
于是结合网上以及之前部署的流程, 组织了一套原始的部署方式.
	

简介

主要流程为:
	1. 项目由docker容器化部署
	2. 使用docker-compose 进行容器服务编排
	3. 后端项目中使用环境变量获取数据库信息
	4. 环境变量由docker-compose进行注入
	5. 由 .env 来组织docker-compose的环境变量配置
	6. 由shell脚本聚合compose启动指令及其他操作.

主要涉及知识点:
	- docker
	- docker-compose
	- nginx
	- shell
	

具体流程

项目配置

nginx配置
主要想使用环境变量来替换nginx转发的服务器ip端口与地址, 利用了shell的envsubst 来替换文件中的变量名达成.
当然, docker中也有这种操作, 可以直接利用template来替换实现, 我这种方式相对迂回了一些.(待修改, 目前已经部署完了, 所以就不想再折腾了, 下次如果有部署再优化吧)

记录了在这边:
	https://blog.csdn.net/fwzzzzz/article/details/119423811?spm=1001.2014.3001.5502
前端项目

不是很熟悉前端, 为了部署项目, 也只是看了一些相关的build知识.

我们的前端项目使用的是react构建的, 所以我着重了解了下基础, 以及运行方式. 

在我看来, 这次的前端部署, 只要将前端代码build出来的静态文件放到服务器中的某个位置, 然后nginx进行路由就行了.

问题点:
	一是, nginx使用docker启动, 而前端项目的build过程是在本地还是在容器中一起构建, 一起构建的话就需要采用容器的二次构建来减少镜像大小了. 

	第二个需要注意的是, 前端与后端的接口跨域问题, 这个也是需要用nginx来解决的, 具体步骤是通过匹配api路由来proxy转发一次.
	
  • Dockerfile 文件
FROM nginx:1.20.0
COPY ./build /opt/web_dist
WORKDIR /opt/web_dist

EXPOSE 80
ENTRYPOINT nginx -g "daemon off;"
后端项目
后端项目需要更改的是配置文件中的环境变量获取, 因为我们项目中的数据库等连接在换了服务器的情况下, 一定会发生变化. 所以最好就是使用环境变量, 由外部来注入统一管理.

具体的语言, 获取环境变量的方法不一样, 可按需要修改.

由于后端也是采用docker启动, 所以我们只需要添加Dockerfile文件即可, 注意依赖等

  • Dockerfile
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.6
ADD requirements.txt requirements.txt
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.douban.com/simple
COPY . /usr/src/app
WORKDIR /usr/src/app
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

数据库配置

由于项目采用了mysql与redis, 所以这边使用了两个容器来分别启动服务

mysql
因为不确定客户后期是否会更换数据库的连接, 所以在docker-compose中没有使用服务名称来替换数据库的连接

启动mysql容器时, 采用了最简单的数据服务挂载到宿主机的方式, 为的是能快速部署(避免冷启动以及编写sql脚本插入数据).

  • 解决冷启动问题
由于我们新部署的项目里面是没有任何数据的, 所以就需要避免冷启动带来的一些未知错误, 两种方式:
	
1. 利用shell脚本进行数据的写入
	可以直接执行shell 脚本来插入数据
	
	简单来说, 就是在宿主机的shell脚本中添加指令, 不进入容器执行:
		docker exec -i msyql bash -c '/data/initdb/initdb.sh'
	
	以上命令进入容器, 执行initdb.sh文件, 而此文件会连接mysql数据进行数据的插入:
		mysql -u${MYSQL_USER} -p${MYSQL_PASSWORD} -e "source /data/dbinit/initdb.sql"
		当然此处的 MYSQL_USER 等变量, 也都是由compose进行环境变量的注入
	
	而执行的initdb.sql文件中, 是对数据库的插入数据操作:
		INSERT INTO `user_role` VALUES (1, 'admin', '管理员:拥有后台全部权限');
		.....

2. 直接本地生成好容器挂载出目录, 以及插入数据
	这样, 在测试的宿主机文件中就出现了完整的数据, 之后就可以一同打包代码文件夹中, 到客户的服务器中进行部署了.
	这样的操作比较简单, 方便.

我在哼哧哼哧的写完shell脚本之后, 还是选择了第二种方式. 

哦对, 第一种方式运行时, 遇到了个问题. 是关于宿主机与容器之间的交互的: 
	目前正确的方式是: 宿主机的sh文件执行容器内的sh文件 -> 容器内sh文件执行mysql指令 -> 插入数据
	
	刚开始使用的是: 宿主机sh文件直接执行 exec -c "mysql -u .... -e "source" " -> 宿主机内的sql文件.  这样就会导致找不到文件, 应该是宿主机与容器之间的文件系统的隔离问题, 我整了一会子, 就采用了上一个方式解决了.
	
redis
没什么好说的
注意环境变量

DockerFile编写规范

这里列出了收集到的以及在应用中踩坑发现的, dockerfile的一些编写规范.

- FROM 有线使用最小功能集的基础镜像
- 依赖具体的版本镜像, 禁用latest
- LABEL 增加镜像元数据
- 使用 WORKDIR 指定工作目录, 避免绝对路径扩散
- 不要使用 ENV与ARG传递敏感信息
- 敏感信息使用  mount secret 方式
- RUN 上下文依赖关系 需要在同一个RUN 内执行(缓存机制)
- 优先使用 COPY, 语义明了, 简洁
- 禁用 root 用户运行应用, 为应用创建用户与用户组
- 应用程序运行用户设置Shell为 /sbin/nologin 
- 显式的使用EXPOSE指明端口与协议 EXPOSE 80/tcp
- 持久化数据 VOLUME 
- 将标准日志与错误日志分别输出到stdout与stderr
- 使用ENTRYPOINT封装镜像的固定行为,使用CMD配合输入可变参数
- 使用ENTRYPOINT执行运行前准备工作
- 尽量选择体积的镜像(也可能会导致一些未知错误)
- 分阶段构建是控制镜像大小的一种方式
- 禁用 chmod -R
- dockerfile 书写顺序按更新频率升序排列(缓存机制)推荐将变化频度低的层尽量放上层,变化频度高的层放下层
- 相关度高一致的命令, 写在同一个RUN指令中

.env配置文件

# 确定服务器ip
VMP_SERVER_IP=47.x.x.13

# 自定义服务开放端口
WEB_PORT=8111	# 前端端口
APP_PORT=8222	# app后端端口
API_PORT=8333	# api后端接口

# mysql服务
MYSQL_ROOT_PASSWORD=vmp_admin
MYSQL_HOST=47.x.x.13
MYSQL_PORT=8306
MYSQL_USERNAME=vmp
MYSQL_PASSWORD=dev_admin
MYSQL_DATABASE=vmp

# redis服务
REDIS_HOST=47.x.x.13
REDIS_PORT=8307
REDIS_PASSWORD=

# 容器名称
MYSQL_CONTAINER=vmp_mysql
REDIS_CONTAINER=vmp_redis
WEB_CONTAINER=vmp_web
SERVER_TS_CONTAINER=vmp_server_ts
SERVER_PY_CONTAINER=vmp_server_py

# 镜像名称
WEB_IMAGE=vmp_web_image
SERVER_TS_IMAGE=vmp_app_image
SERVER_PY_IMAGE=vmp_api_image

# 项目环境变量
PROJ_ENV=production

docker-compose 配置

version: "3.6"

services:
  # mysql服务
  mysql:
    image: mysql:5.7
    restart: always
    container_name: ${MYSQL_CONTAINER}
    ports:
      - ${MYSQL_PORT}:3306
    networks:
      - vmp_service
    volumes:
      - ${PWD}/service/mysql:/var/lib/mysql
    command: [
        "--character-set-server=utf8mb4", 
        "--collation-server=utf8mb4_unicode_ci",
        ]
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
      - MYSQL_USER=${MYSQL_USERNAME}
      - MYSQL_PASSWORD=${MYSQL_PASSWORD}
      - MYSQL_DATABASE=${MYSQL_DATABASE}
      - LANG=C.UTF-8
   
  # redis服务
  redis:
    image: redis:alpine3.14
    container_name: ${REDIS_CONTAINER}
    ports:
      - ${REDIS_PORT}:6379
    networks:
      - vmp_service 
    volumes:
      - ${PWD}/service/redis:/data

  # 前端服务
  vmp_web:
    image: ${WEB_IMAGE}
    build: vmp_web
    restart: always
    container_name: ${WEB_CONTAINER}
    ports:
      - ${WEB_PORT}:80
    networks:
      - vmp_service 
    volumes:
      - ${PWD}/service/nginx/default.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - vmp_api
      - vmp_app
    links:
      - vmp_app
      - vmp_api
    
    
  # ts后端应用
  vmp_app:
    image: ${SERVER_TS_IMAGE}
    build: vmp_server
    restart: always
    container_name: ${SERVER_TS_CONTAINER}
    networks:
      - vmp_service 
    ports:
      - ${APP_PORT}:2999
    volumes:
      - ${PWD}/server_logs/ts_app:/usr/src/app/logs
    environment:
      - MYSQL_HOST=${MYSQL_HOST}
      - MYSQL_PORT=${MYSQL_PORT}
      - MYSQL_USERNAME=${MYSQL_USERNAME}
      - MYSQL_PASSWORD=${MYSQL_PASSWORD}
      - MYSQL_DATABASE=${MYSQL_DATABASE}
      - NODE_ENV=${PROJ_ENV}
      - OSS_KEY=${OSS_KEY}
      - OSS_SECRET=${OSS_SECRET}
      - OSS_BUCKET=${OSS_BUCKET}
      - OSS_REGION=${OSS_REGION}
      - OSS_PATH=${OSS_PATH}
      - PY_API_URL=${VMP_SERVER_IP}:${API_PORT}
    depends_on:
      - redis
      - mysql


  # py后端接口项目
  vmp_api:
    image: ${SERVER_PY_IMAGE}
    build: vmp_server_py
    restart: always
    container_name: ${SERVER_PY_CONTAINER}
    ports:
      - ${API_PORT}:8000
    networks:
      - vmp_service 
    volumes:
      - ${PWD}/server_logs/py_api:/usr/src/app/app/logs
      - ${PWD}/service/py_pkg:/usr/src/app/package
    environment:
      - REDIS_HOST=${REDIS_HOST}
      - REDIS_PORT=${REDIS_PORT}
      - REDIS_PASSWORD=${REDIS_PASSWORD}
      - MYSQL_HOST=${MYSQL_HOST}
      - MYSQL_PORT=${MYSQL_PORT}
      - MYSQL_USERNAME=${MYSQL_USERNAME}
      - MYSQL_PASSWORD=${MYSQL_PASSWORD}
      - PY_ENV=${PROJ_ENV}
    depends_on:
      - redis
      - mysql


networks:
    vmp_service:

启动文件 start.sh

start.sh 文件中有很多是我没有来得及写的功能函数, 只是写了一些占位的输出(反正也没用到, 后续再优化吧)

主要的逻辑就是利用了getopts来实现输入的参数化启动, 然后再根据输入的参数执行不同的脚本命令, 这样把对docker-compose的一些指令封装到了shell脚本的指令后面.

格式问题
本来是在win下编写的脚本, 在上传到服务器中进行测试的时候, 异常报错提示是格式的问题, 需要进行手动的替换操作:
	
	sudo sed -i "s/\r//" .start.sh
	
参数化启动
想要实现那种 -a -b 传递参数的形式, 看起来高端些, shell中可以使用 getopts 来实现这种方式:

# -a 用于指定执行动作; -t 指定容器名称, 默认全部; -h 查看帮助
while getopts "a:t::" opt;
do
	case $opt in 
	a)
		ACTION=$OPTARG
		;;
	t)
		TARGET=$OPTARG
		;;
	\?)
		echo "No such command...${OPTARG}"
		usage
		;;
	esac
done


main ${ACTION} ${TARGET}

其中 `while getopts "a:t::" opt;` 表示接受两个参数, -a 与 -t
a是必填的(a:), t是选填(t::), 那我们使用的时候就可以直接:
	start.sh -a install 	
	或者
	start.sh -a install  -t msyql

看起来果然就高档了起来呢(dogs

start.sh 文件
#!/bin/bash
#=============================================================================
#
# Author: hhhhh
#
# Last modified:	2021-08-01 00:00
#
# Filename:		start.sh
#
# Description: 
#
#=============================================================================
source ./.env


INITDB_FILE_NAME="${PWD}/service/database/"		# 初始化mysql数据库脚本位置
NGINX_CONF_FILE="${PWD}/service/nginx"		# nginx conf 配置文件位置


# 打印方法 
function echo_red() {
    echo -e "\033[1;31m$1\033[0m"
}

function echo_green() {
    echo -e "\033[1;32m$1\033[0m"
}

function echo_yellow() {
    echo -e "\033[1;33m$1\033[0m"
}

function echo_done() {
    echo "$(gettext 'complete')"
}



# 操作执行
function build_project()
{
	echo $1
	if [ ! $1 ]; then
		echo_green "部署全部项目"
		alter_nginx_conf
		docker-compose build && docker-compose up -d
	else
		if [ $1 == 'vmp_web' ]; then
			alter_nginx_conf
		fi
		
		echo_green "部署单独的项目: $1"
		docker-compose build $1 && docker-compose up -d $1

	fi
}


function remove_project()
{
	if [ ! $1 ]; then
		echo_red "删除全部项目容器"
		echo "docker-compose stop && docker-compose rm -f"
	else
		echo_red "删除指定容器: $1"
		echo "docker-compose stop $1 && docker-compose rm -f $1"
	fi
}


function restart_project()
{
	if [ ! $1 ]; then
		echo_green "重启全部项目容器"
		alter_nginx_conf		
		docker-compose restart
	else
                if [ $1 == ${WEB_CONTAINER} ]; then
                	alter_nginx_conf
		fi

		echo_green "重启指定容器: $1"
		docker-compose restart $1

	fi
}


# 保存容器镜像
function save_project_image()
{
	echo $1
	if [ ! $1 ]; then
		echo_green "保存全部容器镜像: mysql, redis, web, ts, py"
		echo "docker save -o mysql.tar mysql:5.7"
		echo "docker save -o redis.tar redis:alpine3.14"
		echo "docker save -o ${WEB_IMAGE}.tar ${WEB_IMAGE}:latest"
		echo "docker save -o ${SERVER_TS_IMAGE}.tar ${SERVER_TS_IMAGE}:latest"
		echo "docker save -o ${SERVER_PY_IMAGE}.tar ${SERVER_PY_IMAGE}:latest"
	else
		echo_green "重启指定容器及版本号: $1"
		echo "docker save -o $1.tar $1"
	fi
}


# 恢复容器镜像
function load_project_image()
{
	echo $1
	if [ ! $1 ]; then
		echo_green "保存全部容器镜像: mysql, redis, web, ts, py"
		echo "docker load -i mysql.tar"
		echo "docker load -i redis.tar"
		echo "docker load -i ${WEB_IMAGE}.tar"
		echo "docker load -i ${SERVER_TS_IMAGE}.tar"
		echo "docker load -i ${SERVER_PY_IMAGE}.tar"
	else
		echo_green "重启指定容器及版本号: $1"
		echo "docker save -o $1.tar $1"
	fi
}


function init_db {
	# 迁移数据库脚本文件至容器内
	echo "docker cp ${INITDB_FILE_NAME} ${MYSQL_CONTAINER}:/data/dbinit/"
	docker cp ${INITDB_FILE_NAME} ${MYSQL_CONTAINER}:/data/dbinit/
	# 执行数据的插入
	echo_green "insert into db"
	echo "docker exec -i ${MYSQL_CONTAINER} bash -c '/data/dbinit/initdb.sh'"
	docker exec -i ${MYSQL_CONTAINER} bash -c '/data/dbinit/initdb.sh'

	if [ "$?" -ne 0 ];then
		echo_red "数据初始化失败"
		exit 1 
	else
		echo_green "执行初始化成功"
		exit 0
	fi
}


# 对nginx.conf 文件进行修改
function alter_nginx_conf {
	export NGINX_VMP_SERVER="http://${VMP_SERVER_IP}:${APP_PORT}/"
	envsubst '${NGINX_VMP_SERVER}' < ${NGINX_CONF_FILE}/default.conf.template > ${NGINX_CONF_FILE}/default.conf
	unset NGINX_VMP_SERVER
}


# help
function usage {
cat << EOF
Desc: 用于管理项目容器的简易脚本

Author: hzinsights

Options:

	-a action	程序执行指令(build|restart|init_db)
		build: 		部署容器
		restart: 	重启容器
		init_db:	插入数据
		save: 		保存镜像
		load:		恢复镜像
		help		查看帮助

	-t target	容器项目名称(可选)	
				
Examle:
	$0 -a build	

EOF

exit 0
}




function main {
	case "${ACTION}" in 
	build)
		echo_green "进行项目部署..."
		build_project ${TARGET}
		;;
	uninstall)
		echo_green "进行项目删除..."
		remove_project ${TARGET}
		;;
	restart)
		echo_green "重启项目服务..."
		restart_project ${TARGET}
		;;
	save)
		echo_green "保存项目镜像..."
		save_project_image ${TARGET}
		;;
	load)
		echo_green "恢复项目镜像..."
		load_project_image ${TARGET}
		;;
	init_db)
		echo_green "进行数据插入操作"
		init_db
		;;
	help)
		echo_green "查看帮助"
		usage
		;;
	*)
		echo_red "No such command: ${action}"
		usage	
		;;
	esac
}


# -a 用于指定执行动作; -t 指定容器名称, 默认全部; -h 查看帮助
while getopts "a:t::" opt;
do
	case $opt in 
	a)
		ACTION=$OPTARG
		;;
	t)
		TARGET=$OPTARG
		;;
	\?)
		echo "No such command...${OPTARG}"
		usage
		;;
	esac
done


main ${ACTION} ${TARGET}

启动执行部署

我们会在本地先执行测试一遍, 然后连接mysql数据库插入一些基础的数据, 测试功能没问题了, 把这些代码以及挂载的目录一起打包发给客户.

有客户进行上传到服务器上, 登录ssh准备部署

解压安装包, 进入目录

chmod +x ./start.sh	# 赋予可执行权限
./start.sh -a build # 执行容器的部署

然后等待镜像拉取与部署, 中间可能会出现镜像的拉取过慢, 可以使用阿里云的镜像加速服务.

遇到的问题集

从开始学习部署到结束部署的时间:
	0726 - 0812
	三周时间


第一周:
	0726 - 0730
		- 前端项目的启动, build 等技术学习
		- 前端的部署就是在nginx上暴露静态文件的server
		- 前端项目部署到服务器中去.
		- 前后端的部署联调
		- 学习dockerfile 的编写优化				
		- 后端与数据库容器的部署
		- 学习shell基本知识
		- nginx的部署相关问题
			- server 相关
			- 转发的问题
		
		
第二周:
	0802 - 0806
		
		- 根据项目中设置的修改并部署nginx的相关配置
			- nginx 进行端口转发解决跨域问题
			- nginx 转发失败, 还是没法显示页面
				- 项目的orm与数据库字段不一致导致
		- 优化 .sh 文件
			- shell脚本的语言逻辑的优化
			- 使用getopt可以达成参数化输入
		- 解决冷启动问题
			- 使用sh文件进行数据sql的自动写入
				1. 第一种方案是:
					在宿主机的sh文件中, 执行 exec -c 不进入容器执行命令, 这条命令是在mysql容器内执行 -e source sql 文件来插入数据
					但是出现问题, 怎么也找不到文件, 无论是写宿主机的路径还是容器路径
				2. 第二种方案
					必须要进入容器执行 sh 脚本, sh脚本再调用容器内的命令来执行sql写入指令
					宿主机的sh文件 -> 容器内的sh 文件 -> 执行容器内的mysql指令 -> 插入数据
				
			- 直接挂在宿主机数据目录
				最简单, 可以本地执行完, 然后打包代码文件夹一起发给客户
				
		- 解决linux 与 win 文件格式的不同, 导致的问题
			sudo sed -i "s/\r//" ./.env
			
		- nginx 的配置问题:
			nginx 的端口转发, 需要的ip地址肯定是不一样的, 客户的机器不一样, ip也不一样, 所以需要定制化修改
			想通过修改 .env 文件来统一管理, 那就只能搞一搞看了.
			- docker有专门的命令来替换参数(envsubst)
				但是我们使用的compose与dockerfile一起用的, 所以有点问题
			
			- 第二种方案: 
				使用了shell 脚本执行 envsubst 来在本地替换, 最后挂载至容器内
		
		- 部署时的项目修改兼容
			前端项目的不一致修改
			后端的环境变量获取的修改
			py项目兼容客户的py包
			
			
第三周:
	0809 - 0812
	
	- 前端突然出现问题
		build 出错, 解决档案使用本地编译, 上传到服务器中
	
	
	- py 包的编译问题
	
	- 客户的服务部署
		 内网服务不可访问外网数据库
		
	- 优化后端的整体部署
		- 修改ts中固定的代码
		- 修改py中的代码
		- 修改compose文件
		- 修改 .env 文件
	
	- 部署结束  (一共花了 14....)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值