从零开始部署自己的服务器
以华为云的 2G2 核的免费云服务器为例 尝试进行完整的部署流程 包括安全组设置 nginx 配置等等
前置工作
服务器选择:Linux 系统最为普遍 选择了 Ubuntu22.04 镜像 自动配一个宝塔面板方便可视化监控和管理
安全组设置:安全组可以理解为以实例为单位的出入规则组 只需要开放安全组内互相出入 以及固定常用端口所有源地址的入方向即可(配置 NGINX 后应该只需要开放 80 和 443)
SSH 连接服务器:使用 MobaXtern 进行连接 输入公网 IP 和密码即可登录
登录宝塔面板:在主机获取宝塔面板的密码后即可登录 后续可以用可视化界面进行软件安装管理与状态监控 注意宝塔面板安装的环境都位于 www/server 下
Spring Boot 项目的部署
打包为 jar 包 可以使用 IDEA 也可以使用 CLI
- 使用 IDEA 记得切换跳过模式来跳过测试 否则测试不通过没法生成 jar 包
- 使用 IDEA 记得把 pom 文件里的主类 configuration 下方的 skip 删掉 否则就会跳过主类配置 然后 jar 包就会因为缺少主类而无法启动
服务器上配置环境:安装 jdk 直接 apt install 即可
运行 jar 包即可
nohup java -jar backend-0.0.1-SNAPSHOT.jar &
输入tail -500f nohup.out
来查看日志
React + Vite 项目的部署(参考)
在服务器上配置环境:
安装 nodejs 见https://blog.csdn.net/weixin_42582542/article/details/129982650
安装 nginx 可以使用 apt 也可以宝塔面板进行编译安装
打包项目
npm run build
产生的 dist 文件夹放入服务器进入 nginx/conf/寻找配置文件进行修改 只需配置监听端口(80 是 http 默认端口) 虚拟主机名(没有域名就使用 ip) 以及 location(见下方代码) 最后运行
nginx -s reload
重新加载一下配置即可访问了
1 |
|
前后端分离项目的跨域问题
前后端由于端口不一致或协议不一致等 会导致 403 报错 解决方法采用 nginx 配置反向代理 有多种方法
- 配置转发 把带有前缀/api/的请求转发给 8080 端口 前端以形如
mydomain/api/
的形式访问后端 消除了前后端差异(最终采用此种方法 注意 location 的配置很细节 不同的匹配方式会导致不同的结果 比如/api
还是/api/
) - 也可以添加允许跨域请求头
- 当然也可以在后端中配置运行跨域的来源
- 配置转发 把带有前缀/api/的请求转发给 8080 端口 前端以形如
配置图片等静态资源的访问和上传
- 修改 Nginx 配置以暴露图片资源文件夹 如下
1 |
|
如何上传图片资源到 jar 包以外的固定路径:只需要写成绝对路径即可 比如
/root/ebookstore/static/
如何加快图片加载速度?
由于服务器的带宽只有 2Mbit/s 传输一张 24MB 的图片需要惊人的 96s 这显然是很坑爹的 除了提升带宽以外 还可以使用什么方法提高效率呢?
- 进行压缩处理 nginx 提供的 gzip 对图片压缩的效果并不好 因此可使用其他压缩工具
- 使用 CDN 分发
- 配置缓存
- 嫖别人的服务器 也就是图床
- 存放到数据库里
使用赛博活菩萨 CloudFlare 的 R2 服务配置个人图床
注册CloudFlare账号 订阅 R2 服务 创建新的存储桶 然后允许公网访问 至此个人图床就可以使用了
接下来使用PicGo进行上传和获取 url 的自动化 参考https://www.pseudoyu.com/zh/2024/06/30/free_image_hosting_system_using_r2_webp_cloud_and_picgo/ 这个软件也可以用于写博客时上传图片
除了手动上传 还需要实现后端上传的 API 采用 aws-java-sdk 来实现 这个 sdk 提供了 java 访问 S3 服务的接口
参考 http://public-cloud-doc.nos-eastchina1.126.net/S3-JAVA-SDK.html# 和 https://blog.csdn.net/qq_40942490/article/details/110168965
在 pom 导入 sdk 的依赖 然后编写两个文件 一个 config 注入密钥等配置 一个 service 实现上传图片的函数即可
至此 不仅可以手动上传图片 也可以在自己的项目里使用上传图片的接口 以后图片就无需重新迁移了
*不过还是有个小问题 因为上传还是要经过自己的服务器 然后在上传给图床 这就导致很慢 可以考虑前端直接调用图床 应该可以使用 nodejs 的接口
开始使用 Docker 一键部署项目
安装 Docker
参考官方文档:https://docs.docker.com/engine/install/ubuntu/#installation-methods 安装完后配置一下仓库的国内镜像源 否则很卡:https://blog.csdn.net/m0_59748326/article/details/140502750
为 Docker 配置代理
为了方便后续使用自己仓库 去 dockerhub 注册一个账号 由于目前 dockerhub 以及大部分的仓库会被墙 所以干脆在服务器上使用 clash 配置代理 下面介绍流程:
在服务器上安装 clash 见https://glados.space/console/terminal 记得最后用 nohup 指令来后台运行 clash
配置 docker 使用代理 新建/etc/systemd/system/docker.service.d/proxy.conf 文件 输入下面内容:
1
2
3
4[Service]
Environment="HTTP_PROXY=http://127.0.0.1:7890/"
Environment="HTTPS_PROXY=http://127.0.0.1:7890/"
Environment="NO_PROXY=localhost,127.0.0.1,.example.com"然后输入下面指令重启服务并验证是否配置成功
1
2
3
4
5
6# 加载配置
systemctl daemon-reload
# 重启docker
systemctl restart docker
# 查看代理配置是否生效
systemctl show --property=Environment docker最后
docker login
登录账号 即可上传镜像
使用 Docker 部署 Nginx
先尝试拉取官方的 nginx 并挂载自己的配置文件 命令如下
1
2
3
4
5
6
7docker run -d #后台运行
--name my_nginx #容器名称
-p 80:80 #端口映射
-v /root/nginx/nginx.conf:/etc/nginx/nginx.conf:ro #挂载配置文件
-v /root/ebookstore/frontend:/root/ebookstore/frontend #挂载前端静态资源 后续前端也用容器部署就无需挂载了
nginx:latest在配置文件中还是有一些需要动态注入的变量
一个是 server name 会随着服务器更换而变化 不过我们可以注册一个域名 使用域名解析 然后配置文件里就可以写成我们的某一个子域名了 只需要去域名注册的网站里配置一下 DNS 服务 把一个子域名解析到服务器 ip 即可
另一个是转发给前后端的 proxy_pass 因为配置在容器内部 不可以再用 localhost 必须使用容器间通信的内网或者公网 ip 不过我们知道 docker 有一个自带的 dns 解析服务 因此可以利用这个固定容器间的网络名称 做到不修改配置文件 具体步骤如下:
- 使用
docker network create my-network
指令创建一个容器网络 - 启动容器时加上参数
--network my-network
以加入容器网络 - 接下来容器间即可用形如
curl http://container_name
的格式来通信了! - 具体而言 我们把前端和后端的容器命名为 frontend 和 backend 即可用名字和端口转发请求了
- 使用
为了方便以后一键部署 我们写一个 Dockerfile 来创建一个镜像提交到自己的仓库里
1
2
3
4
5
6
7
8
9# 使用官方 Nginx 镜像作为基础镜像 选用alpine而不是latest 是因为前者的镜像更为精简 拉取更快 同样的nginx latest有180M 而alpine只有40M
FROM nginx:alpine
# 复制自定义配置文件到容器 注意路径是相对路径 相对于指定的上下文路径 我的dockerfile在/root下
COPY /nginx/nginx.conf /etc/nginx/nginx.conf
# 复制自定义网站文件到容器 (后续部署前端容器就无需复制)
COPY /ebookstore/frontend /root/ebookstore/frontend
# 暴露80端口(仅做声明)
EXPOSE 80然后使用
1
2
3docker build -t my-nginx . #最后的.是上下文路径为当前目录
docker tag my-nginx nwdnysl/my-nginx:latest #打上标记 我还不是很理解这个指令的作用
docker push nwdnysl/my-nginx:latest #推送这样就把完整的镜像 push 到了自己的仓库 后续可以一键 run 或者使用 docker compose 来编排
虽然现在可以一键拉取自己的镜像 但是有些参数(比如端口映射和网络)还是需要手动在 docker run 时写明 很不方便 接下来使用 Docker Compose 实现一个 yml 文件来进行一次性的编排
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30services:
my-nginx:
image: nwdnysl/my-nginx:latest
container_name: my-nginx
ports:
- 80:80
networks:
- my-network
depends_on: #依赖顺序 nginx因为启动需要DNS解析所以要后启动 不过也可以设置自动重启
- ebook-frontend
- ebook-backend
restart: always
ebook-frontend:
image: ebook-frontend:latest
ports:
- 8080:8080
networks:
- my-network
environment: - APP_ENV=production
ebook-backend:
image: ebook-backend:latest
ports:
- 8080:8080
networks:
- my-network
restart: always
networks: #需要显示声明并建立network
my-network:
driver: bridge然后使用指令
docker-compose up -d
来一键拉起所有的容器
使用 Docker 部署 Spring Boot 项目
有了 nginx 部署的经验 我们直接从 Dockerfile 开始编写:
1
2
3
4
5
6
7
8
9
10
11
12
13
14# 使用官方的 OpenJDK 作为基础镜像
FROM openjdk:17-jdk-alpine
# 设置工作目录 相当于在当前目录进行指令 'cd backend'
WORKDIR /backend
# 将应用程序的 JAR 文件添加到工作目录 我的上下文路径是ebookstore/backend/
COPY backend-0.0.1-SNAPSHOT.jar /backend/ebook-backend.jar
# 运行 JAR 文件
ENTRYPOINT ["java", "-jar", "ebook-backend.jar"]
# 暴露端口
EXPOSE 8080同理使用以下指令 push 到仓库
1
2
3docker build -t ebook-backend .
docker tag ebook-backend nwdnysl/ebook-backend
docker push nwdnysl/ebook-backend:latest
使用 Docker 部署 Vite 项目
不多说了 还是 Dockerfile 开始:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23# 使用 Node.js 官方镜像作为基础镜像
FROM node:18-alpine
# 设置 npm 镜像源
RUN npm config set registry https://registry.npmmirror.com/
# 创建应用目录
WORKDIR /ebook-frontend
# 复制构建产物到容器中
COPY dist ./dist
# 复制其他必要的文件,如 server.js
COPY server.js .
# 安装应用依赖
RUN npm install express
# 暴露端口
EXPOSE 3000
# 启动 Express 服务器
CMD ["node", "server.js"]同理使用以下指令 push 到仓库
1
2
3docker build -t ebook-frotend .
docker tag ebook-frotend nwdnysl/ebook-frotend
docker push nwdnysl/ebook-frotend:latest
使用 Docker 部署 MySQL 数据库服务
MySQL 服务并不需要经常更改配置 因此可以使用命令直接运行 参考:https://blog.csdn.net/lianghl_wb/article/details/141282257
编写 Dockerfile 拉取 5.7 的基础镜像(8.0 实测内存占用大很多) 把配置文件复制进去即可
1
2
3
4
5
6
7FROM mysql:5.7
# 复制自定义配置文件(如果有)
COPY my.cnf /etc/mysql/conf.d/my.cnf
# 暴露MySQL端口
EXPOSE 3306使用以下指令运行镜像
docker run --name ebook-mysql -e MYSQL_ROOT_PASSWORD=your_password -d -p 3306:3306 -v ./data:/var/lib/mysql ebook-mysql
记得创建 data 目录用于挂载我最终在服务器上没有采用容器来部署 MySQL 因为单例数据库的情况下不太需要容器管理 如果后续需要配置主从复制等 就可以使用 Docker 来管理
另外一个情况是本地环境下使用容器 就不用配置 MySQL 服务在主机上了 后续使用 Redis 等中
间件也是同理 可以解放电脑 做到随用随启 给自己的电脑一个干净隔离的环境
向 Vite 项目注入环境变量
一些动态变量(如端口 ip)固定写在项目中是很差的做法 接下来我们尝试把这些变量替换为环境变量 然后在本地环境下用.env 文件注入 在生产环境下可以通过容器的环境变量来注入 下面介绍流程 参考文档:https://vitejs.cn/vite3-cn/guide/env-and-mode.html
创建.env 文件 定义形如
VITE_BASEURL=http://localhost
的环境变量 注意官方文档说明必须带上’VITE_‘前缀才可以读取环境变量在 Vite 项目中 可以这么使用
const apiUrl = import.meta.env.VITE_BASEURL;
由于 Vite 项目和 java 项目不一样 一旦编译环境变量就会作为静态字符串注入到文件 所以不能在 build 后再注入变量了 为了自动部署方便 我们可以在利用 Vite 提供的不同模式:
- 首先在本地仓库写两份环境变量 分别为.env.development 和.env.production
- 然后把 package.json 文件里的 build 脚本改为
vite build --mode production
这表明编译时采用生产环境 - 这样子本地开发运行
npm start
默认是运行 dev 模式 而编译的 dist 文件夹就是生产环境的了 自动部署时无需在服务器上运行 build 而是在本地仓库运行 build 后把 dist 文件夹上传然后部署 省下传输时间
向 Spring Boot 项目注入环境变量
- 在 Spring Boot 项目中把 application.yml 文件的变量都变为
${}
格式的环境变量 然后在各个类中可以用@Value
注解进行注入 - 本地环境下创建一个.env 文件 以键值对形式声明环境变量 然后在 IDEA 中编辑运行/调试配置 选中环境变量文件 或者也可以在设置中用键值对形式手动声明
- 2 中的环境变量是局部变量 也可以设置系统变量 Windows 系统去系统设置里设置即可 系统变量会自动被读取 无需手动配置
- 在生产环境下 为了方便自动化部署 我们可以注入容器的环境变量 只需在 docker-compose.yml 文件中声明环境变量 就可以改变容器中的系统变量 在运行 jar 包时也就自动把环境变量注入了
在 docker-compose.yml 中进行环境变量的注入
可以采用手动编写 也可以采用.env 文件注入 我们采用后者来实现 springboot 后端项目的环境变量注入
在 backend 服务中加入这一行
1
2env_file:
- .env然后 backend 这个容器就会被注入环境变量 这样注入的环境变量就相当于容器内部的系统变量 jar 包运行时会自动读取
注意一个巨坑的点!在 Dockerfile 里如果启动 jar 包的命令是 ENTRYPOINT 则会出现 jar 包读取不到环境变量的问题 原因未知 改为 CMD 后解决
顺便在说说前端项目如何注入环境变量:因为 Vite 项目编译后环境变量没法再注入 所以环境变量文件编写在本地仓库里 编译时就注入完成 java 项目则可以先把 jar 包上传到服务器对应位置进行镜像的 build 然后在 docker-compose 一键拉起时在注入环境变量
除了需要注入容器的变量以外 容器本身的名称等也可以提取出来 以后就不用修改 yml 文件了 方法就是编写.env 然后用
${}
的格式注入即可 视需要配置到这一步后 我们总结一下 只需以下几步就可以交付部署新的版本:
- 编译 包括前端 build 成 dist 文件夹 后端 package 成 jar 包
- 把本地仓库需要上传到服务器的文件进行上传 包括前端的 dist 文件夹 后端的 jar 包 后端的 env 文件 前端部署用的 server.js 各个 dockerfile 以及 yml 脚本
- 依次进入对应路径位置 运行
docker build -t image_name .
编译出新版的镜像 - 进入对应路径位置 运行
docker compose up -d
一键拉起所有镜像 - 如果需要迁移 可以把本地仓库里的最新版镜像 tag 后 push 到自己的仓库 就可以到另一个服务器里开箱即用啦
上述流程要我们手动执行还是太麻烦了 最后一步 我们可以用 Github Actions 的脚本来执行这些流程 做到 push 一次 完整部署所有项目
使用 Docker 部署 Kafka
kafka 如何使用 docker 部署可以见应用系统体系结构课程笔记 在这里补充一下 kafka 部署到服务器上时遇到的问题(沟槽的花了我大半天时间)
可以知道 由于目前 kafka 集成了 kraft 来代替之前需要单独部署的 zookeeper 所以在本地配置 kafka 是很快捷的 只需要拉取镜像并运行即可
然而使用容器部署会产生一个问题 我之前没有注意到:kafka 有一个代理的 broker 这个 broker 的连接地址是在
kafka/config/server.properties
文件中的advertised.listeners
来配置的 默认配置是PLAINTEXT://localhost:9092
如果你的后端也部署在容器里 显然这个 localhost 会导致连接不上(因为容器内的 localhost 指的不是宿主机)那你会说 很简单 我们改一下配置不就行了 确实 我们接下来要做的事都只是为了修改这么一个小小的配置:docker 有两种方式改变容器内的配置文件 一种是在 Dockerfile 里写 COPY 指令 一种是使用
docker cp
指令 详细操作见:https://blog.csdn.net/CatchLight/article/details/139526724
然而 我本人试过很多遍 这个方法不生效 原因大概是 kafka 的镜像在启动时会自动生成一个配置文件 也就导致你复制进去的配置都会被默认配置覆写kafka 还提供了另一条路来修改配置 即启动参数来添加环境变量 所以我们只需要在 docker-compose 里添加这一行即可:
1
2environment:
- KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://ebook-kafka:9092```解决了吗?并没有!如果你启动容器 kafka 会报错说缺少 zookeeper 的连接配置 不对啊 我们之前说过现在使用了 kraft 代替了它 怎么又要我配置呢?
原因是这样的:kafka 如果设置了启动参数 会把所有环境变量写到一个空的配置文件里 而并不会先生成一个默认的配置文件后进行修改 所以在原来配置里的
process.roles=broker,controller
这一项就消失了 这一项是用来告诉 kafka 我要使用 kraft 的 同理的 还有很多配置都是必须的 而现在全没有了 自然会报错走到这一步花了我半天的时间 然后我终于知道该如何配置沟槽的 kafka 了:把默认的配置文件找出来 把所有的键值对都作为环境变量配置到 docker-compose 里!是的 这样配置很蠢 但是我找不到更好的办法了 TT 如果你也遇到同样的问题需要解决 你可以查看我 Ebook 仓库的
docker-compose.yml
文件找到所有应该注入的环境变量 希望这能帮到你
使用 Github Actions 配置 CI/CD 自动化部署
CI(持续集成)/CD(持续部署)的概念概括一下就是不断地更新合并代码以及自动化测试部署 我们使用 Github Code 已经基本实现持续集成 接下来使用 Actions 来实现一下持续部署
先了解 Github Actions 的使用和语法 在仓库根目录下创建
./github/workflows
GitHub 会读取这个路径下所有.yml
文件 并一一配置工作流 每一个工作流包含几个配置:- name:工作流的名称
- on:工作流的触发条件 例如 push、pull、workflow_dispatch(手动触发 但是只能在默认分支下的工作流文件使用)
- jobs:工作流中的任务 一个任务会单独在一个虚拟环境运行 默认情况下 jobs 之间是并行的 除非使用
jobs.<job_id>.needs
指定了 jobs 间的依赖 - runs-on:指定一个 job 运行的环境 例如 Ubuntu、Windows
- steps:一个任务中的运行步骤 为执行任务的基本单元 可以是一个脚本 也可以是已经包装好的 action
了解了这些语法后 我们先整理好部署的流程 对于一个经典的前后端分离项目 不妨为前端和后端分别建立工作流 这样当前端/后端更新代码后需要重新部署时 就不会影响其他容器了
前端 Vite 项目的工作流:
- 在 frontend 文件夹下运行
npm run build
把项目编译为 dist 文件夹 - 使用 SSH 连接服务器 把 dist 文件夹、server.js、Dockerfile 上传到服务器的对应位置
- 在
/root/ebookstore/frontend
下运行docker build -t ebook-frontend .
指令 生成镜像 - 使用
docker compose up -d
来拉起镜像
- 在 frontend 文件夹下运行
后端 Spring Boot 项目的工作流:
- 在 backend 文件夹下运行
mvn package
把项目编译为 jar 包 - 使用 SSH 连接服务器 把 jar 包和 Dockerfile 上传到服务器
- 在
/root/ebookstore/backend
下运行docker build -t ebook-backend .
指令 生成镜像 - 使用
docker compose up -d
来拉起镜像
注意 本来后端项目还需要上传一个.env 文件来进行环境变量的注入 由于环境变量里包含了数据库密钥等敏感信息 是不推荐被 GitHub 跟踪的 也就无法通过工作流的 SSH 进行上传 有两种方法解决这个问题:
- 手动上传.env 文件到服务器 因为通常环境变量不会经常性变更 所以还算方便
- 配置 GitHub Secrets 来进行环境变量注入 只需要在 GitHub 上一个一个配置密钥即可在工作流文件中使用这些环境变量来注入 但是由于不支持以文件形式 一个个配置密钥非常麻烦 而且如果环境变量有变化 还是需要手动修改 个人觉得还不如直接手动上传
- 在 backend 文件夹下运行
Nginx 部署的工作流:
有时候 nginx 的配置文件也会更改 我们可以把配置文件作为触发器来进行自动更新部署
- 上传配置文件和 Dockerfile
- 在
/root/ebookstore/nginx
下运行docker build -t ebook-nginx .
指令 生成镜像 - 使用
docker compose up -d
来拉起镜像
一些细节:
- 每一次部署都需要在虚拟环境下重新安装所有依赖 非常浪费网络资源 GitHub Actions 提供了缓存指令 来缓存一些经常性不变的资源
setup-*
指令会自动配置各个包管理器 包括 npm 和 maven 对于 npm 它会缓存 package.json 等文件cache
指令可以手动缓存指定的文件(比如node_modules
) 详见文档:https://docs.github.com/zh/actions/writing-workflows/choosing-what-your-workflow-does/caching-dependencies-to-speed-up-workflows - 由于 Vite 项目需要静态注入环境变量 如果仓库不跟踪环境变量 就没有办法在 GitHub Actions 中进行 build 好在前端本身就不应该注入敏感信息(因为前端项目可以解包) 所以干脆跟踪前端的.env 文件
- SSH 连接需要在 GitHub Secrets 里配置仓库的密钥 详见文档:https://docs.github.com/zh/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository
- 每一次部署都需要在虚拟环境下重新安装所有依赖 非常浪费网络资源 GitHub Actions 提供了缓存指令 来缓存一些经常性不变的资源
最终前端部署的示例代码如下:
1 |
|
类似的我们可以把后端、nginx 的部署工作流都写好 在这里就不赘述了 有兴趣可以去仓库查看 maven 缓存文档示例:https://docs.github.com/zh/actions/use-cases-and-examples/building-and-testing/building-and-testing-java-with-maven#caching-dependencies
- 9.26 更新:
- 之前写的工作流有点问题 比如前端和后端的代码都修改了 会导致重新部署两遍 为了节省资源 希望把部署的工作流和编译新代码上传文件到服务器的工作流进行分离 不过即使这样分离后 使用工作流的依赖来触发部署 还是会触发多次 我目前找不到很好的解决方法
写一个 docker push 的脚本 方便一键推送
当我们需要更换服务器环境时 需要把本地仓库最新的镜像推送到云端 然后到新的服务器上进行拉取 不妨写一个脚本方便推送 如下
1 |
|
不过 push 过程还是会因为代理不稳定而报错 很烦 我也没有什么解决方法
注册并配置你的域名
- 首先去注册一个域名 我是在阿里云上购买的
- 然后配置域名解析 比如我的书店网站 就是添加一个简单的 IPv4 的 A 记录 而个人博客则需要一个域名到域名的 CNAME 记录 至于个人图床使用了 CloudFlare 的 R2 需要去个人控制台进行配置 这当中先要把 dns 服务器改为 cf 提供的 dns 服务器 让 cf 提供流量分发服务 然后在设置里配置存储桶的自定义域名 以及 DNS 解析(因为 dns 服务器改为了 cf 提供 所以阿里云的 dns 解析是没用的)
- 最后 如果使用了国内的服务器 需要 ICP 备案
至此 一个完整的项目就可以通过 CI/CD 部署到任意一个服务器上了 本篇博客完结!