使用 Github Action + Docker + Git 全自动部署项目到国内服务器,并优化部署速度到 10s

使用 Github Action + Docker + Git 全自动部署项目到国内服务器,并优化部署速度到 10s

三葉Leaves Author

本文中,我会介绍我是怎么自动化部署我的 Nextjs 项目到多个服务器的,并分享一些部署技巧。成功以后,每次 git push 以后,项目会自动把生产构建部署到多个服务器,完全不需要你做任何额外的事情。

我的这个方法有如下几个显著优势:

  1. 完全适配国内网络环境。在各个方案中,这个方案绝对是部署速度最快的。有图有真相,在之后的自动化部署中,每次只花 10 秒左右

自动化部署,而且还是国内服务器,只需要 13 秒
自动化部署,而且还是国内服务器,只需要 13 秒

  1. 轻松的扩展。就算有再多的服务器,也能无痛添加
  2. 干净舒适的环境变量管理。每一台服务器有自己独立的环境变量,且配置清晰、管理方便,而且由于仓库就在服务器上,甚至还能方便的修改运行时变量,而无需重新构建运行容器。
  3. 享受 docker 带来的优秀特性,但是又全程免费。无需买阿里云的容器镜像服务等等
  4. 企业级的合规性,在各个环节的安全评估都经得起推敲。

也有两个局限性:

  1. 用于部署项目的机子,得有 git 仓库的访问权限。(但是一般生产部署的运维人员权限级别都很高,这应该不是问题)
  2. 没有分离构建过程和运行过程,所以用于部署项目的机子也要承担构建的算力开支,对机子的性能有要求。

前置知识

列出了读本文需要知道的一些名词。

  • Github Action

通过在项目根目录配置 .github/workflow/xxx.yml 来启用 action。
这个在 gitee 里叫做“流水线”

  • Github Runner

运行 github action 的环境,可以理解为 github 为你提供的虚拟机。

  • Environment secrets

在 github 仓库设置页面有一个 Environment 配置,里面能配置不同的环境;

每一个环境,都有自己的 Secrets。

在 github 仓库设置的 Secrets and variables 页面,同样能看到这些配置。

部署流程设计思路

我的需求是这样的:

我的项目在 github 和 gitee 上都有 private repo,同时我需要把项目部署到国内和海外两台不同的服务器上。两台服务器上用到的环境变量还不一样。

最初,我想到了这几种方案,每个方案的优缺点我都会做出分析

方案1:利用第三方 docker 镜像托管服务

这是大部分人,或者 AI 推荐的做法。

推送代码到 github 以后,github runner (执行 github action 的虚拟机)会在 github 侧构建 docker 镜像,然后把 docker 镜像推送到 docker hub 中央仓库(这就需要在 github 仓库里配置 docker hub 的账号和密码)

然后,在你的服务器上进行 docker pull,从托管 docker 镜像的地方拉取镜像,然后在本地起容器运行。

需要 Docker Hub 账号密码

开发者推送代码到 GitHub

GitHub Actions Runner 构建 Docker 镜像

推送镜像到 Docker Hub

服务器执行 docker pull 拉取镜像

服务器上运行 Docker 容器(在这个阶段,github action 把 Environment Secrets 注入环境变量,而不是构建镜像阶段)

环境变量的注入时机

我们不要在构建镜像的时候,把环境变量作为构建参数注入。而是应该把环境变量的注入时机调整到 docker run 或者 docker compose up 的时候。
如此一来,你的镜像就可以在多台服务器都能复用。

优点

  1. 构建和运行完全是分开的,你的服务器上只需要跑 docker 容器就行了,对你机子性能要求就不高
  2. 由于 Github Runner 本身就在海外,所以从 Dockerfile build 成镜像的过程不太容易因为网络原因出问题。
  3. 得益于 docker 的 layer 策略,服务器 docker pull 时需要传输的文件大小较小,不需要每次都重新传输一整个镜像。

缺点

在国内服务器上从 docker hub 拉取镜像的过程很不稳定,失败率极高。如果是使用国内阿里云的容器镜像服务之类的,又要面临不小的费用(最低 ¥45/ 月)

方案2:直接传输镜像到服务器

不太推荐这个方案

在 github runner 侧,利用 docker save 命令把镜像打包成 tar.gz 压缩包,然后传输给服务器,再从镜像获得容器

通过 scp 或者 rsync 传给服务器

开发者推送代码到 GitHub

GitHub Actions Runner 构建、打包 Docker 镜像成 tar 压缩包

服务器接收压缩包

服务器从压缩包获得镜像

服务器上运行 Docker 容器(在这个阶段,github action 把 Environment Secrets 注入环境变量,而不是构建镜像阶段)

优点

不需要经过第三方 docker 仓库的中转,模型上看起来好像少一跳。

缺点

彻底丧失了 docker 缓存层的优势,每次都会传完整的镜像,里面包括了 Linux 内核、node 环境等等东西。

就拿 nextjs 项目举例,构建产物才 40MB,但是 docker 镜像有 250 MB。

而且从 github runner 传压缩包到服务器的过程也巨慢无比,不是很稳定。

方案3:弃用 docker,直接传构建产物

这个方案只适合个人开发者部署单个小项目。

思路是在 github runner 里把项目从源码变成构建产物,然后把各个项目的生产构建产物给打包传给服务器,然后直接把服务运行在服务器本机上。

从 github runner 传输构建产物到你的服务器

开发者推送代码到 GitHub

GitHub Runner 从源码构建成生产构建产物

服务器接收构建产物

利用服务器本机环境运行构建产物(在这个阶段,github action 把 Environment Secrets 注入环境变量)

利用例如 pm2 或者原生 systemd 管理各个服务

用这个方法,相比方案2 的优化点就在于只需要传较小的构建产物。然而,这样做会丧失 docker 带来的优良特性。

这会在两个方面带来难度:

  1. 环境管理。你需要在服务器本机上跑起项目所需的环境,比如 node, jdk, maven…
  2. 服务管理。你需要用 pm2 或者 systemd 来管理多个服务,而不能像 docker 那样统一管理。

这样做,就要求运维人员懂各个项目的情况,对运维人员要求很高,运维和开发的分工会很混淆。

方案 4:本文要讲的方案

思路是我们直接在服务器上维护一个 git 仓库,然后每次让 github runner 去操作那台服务器拉最新源码,然后从源码构建 docker 镜像、跑容器。

开发者推送代码到 GitHub

GitHub Runner 操作服务器拉取最新源码,并把 github Environment Secrets 注入会话环境变量

服务器从会话环境变量构建 .env 文件到项目目录

服务器从源码构建 docker 镜像

服务器从镜像运行容器

这个方案的网络传输过程主要就是服务器拉取源码,以及从源码构建镜像的过程。

对于前者,由于我们可以使用国内的 gitee 仓库,而且每次拉取源码只会拉取源码变化的部分,而不是完整的仓库,所以过程是很快的;

对于后者,Dockerfile 构建镜像过程需要下载 Linux 内核,npm install 需要下载 node 包,但是这些过程都能很轻易的通过镜像源加速。

更有甚者,不同于每次都全新的 github runner 环境,我们的服务器上可以充分利用 Docker 缓存层的功能,每次 docker build 的时候,都会利用之前的缓存进行,所以实际情况是只有第一次慢一点(我的项目约 3 分钟),之后全流程只需要不到 10 秒。

部署步骤

1. 在 github 仓库设置页面,创建需要的环境

我这里有两台服务器,所以我就设置了两个环境。

每一个环境都可以设置自己的 Environment secrets,这个在稍后会被用作项目的 .env 环境变量。

在 Environment secrets 界面,我设置了这几个环境变量:

  • SERVER_HOST:服务器地址
  • SERVER_USER:登录用的用户名
  • SERVER_SSH_KEY:登录用的 SSH 私钥,这个私钥是用上面那个用户名的身份创建的
  • SERVER_REPO_PATH:要部署的项目,在你机子上的路径。
  • CLIENT_FRONTEND_ENV:你想使用的项目的 .env 文件的全部内容。之所以这么命名,是因为我的机子上可能还要部署 B 端的前后端项目,所以用 client 和 frontend 区分 B 端和 C 端以及前端和后端。
管理 .env 文件的方式

这里我管理 .env 文件的方式值得提一下。即使我们用前面的其他方案,这个细节也是比较重要的。

我们不把配置项拆开,也不用占位符去 hack 的替换他们,而是直接把 .env 文件一整个塞进 CLIENT_FRONTEND_ENV 里面。

之后,再想办法把这个 CLIENT_FRONTEND_ENV 在服务器上变现成真的 .env 文件即可。

2. 创建 github workflow 配置文件

.github\workflows\deploy-frontend.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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# 部署前端到所有服务器

name: Deploy Frontend to All Production Servers

on:
push:
# 只部署 refactor 分支
branches:
- refactor
# 设置 path 过滤器,只有前端相关文件有变动时才触发前端的部署
paths:
- "frontend/**"
- ".github/workflows/deploy-frontend.yml"

jobs:
deploy:
name: Deploy to ${{ matrix.server_name }}
runs-on: ubuntu-latest
strategy:
# 未来添加新的服务器,只需要在下面添加新的 server_name 和 environment (对应 github 仓库设置中的 Environment)
matrix:
include:
- server_name: JD Server in China
environment: production_jd
- server_name: Ucloud Server in Singapore
environment: production_ucloud
# 设置为 false 时,所有服务器并行部署,互不影响
fail-fast: false
# 将 github 的 Environment Secret 里存的整个 .env 文件注入到 SSH 会话的环境变量中
environment: ${{ matrix.environment }}

steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@master
# 将环境变量传递给脚本
env:
CLIENT_FRONTEND_ENV: ${{ secrets.CLIENT_FRONTEND_ENV }}
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
port: 22
envs: CLIENT_FRONTEND_ENV
script: |
project_path="${{ secrets.SERVER_REPO_PATH }}"

if [ ! -d "$project_path" ]; then
echo "Project directory not found at $project_path"
exit 1
fi

cd $project_path
chmod +x frontend/scripts/deploy.sh
bash frontend/scripts/deploy.sh

在 matrix 块中,我暂时添加了 2 台服务器,对应着我在 github 仓库设置页面添加的两个 Environment。

部署过程中,两个服务器都会触发部署且互不影响、环境也是隔离的,部署任务是并行进行的。

github action 在这个过程中的作用,就是进入服务器的仓库路径,然后触发我们接下来要写的部署脚本,顺便把 github 仓库设置页面填写的 .env 文件注入进 ssh 会话环境,方便接下来 deploy 脚本读取。

3. 代码中创建 deploy bash 脚本

我们在服务器上,需要拉取最新源码、写 .env 文件、构建、启动容器等等。

为了防止在 github workflw yaml 文件中写太多东西(那里的 bash 没有语法高亮),我们直接新建一个部署脚本在项目代码里:

例如:frontend\scripts\deploy.sh

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#!/bin/bash
# Copyright © 2025 LeavesWebber
#
# SPDX-License-Identifier: MPL-2.0
#
# Feel free to contact LeavesWebber@outlook.com

set -e # 遇到错误立即停止

# 这里的逻辑是假设工作目录已经是 git 仓库的根目录

echo "🚀 Starting deployment for production_jd..."

# 1. 确保在 git 仓库根目录 (根据脚本位置反推,如果是在根目录执行则不需要)
# 这里的逻辑假设 GitHub Action 会先 cd 到项目根目录

# 2. 获取最新代码
echo "📦 Fetching latest code from refactor branch..."
git fetch gitee refactor
git reset --hard gitee/refactor

# 3. 进入 frontend 目录
cd frontend

# 4. 生成 .env 文件
# 注意:CLIENT_FRONTEND_ENV 环境变量由 GitHub Action 通过 SSH 注入
echo "📝 Writing .env file..."
if [ -z "$CLIENT_FRONTEND_ENV" ]; then
echo "❌ Error: CLIENT_FRONTEND_ENV is not set!"
exit 1
fi

echo "$CLIENT_FRONTEND_ENV" > .env

# 5. 构建并启动 Docker 容器
echo "🐳 Building and starting Docker containers..."

# 检查是使用 docker-compose 还是 docker compose (v2)
if command -v docker-compose &> /dev/null; then
docker-compose down --remove-orphans || true
docker-compose up -d --build
else
docker compose down --remove-orphans || true
docker compose up -d --build
fi

# 6. 清理未使用的镜像 (可选,防止磁盘占满)
echo "🧹 Cleaning up old images..."
docker image prune -f

echo "✅ Deployment finished successfully!"

4. 创建 Docker 相关文件

下面这个 Dockerfile 是 Nextjs 官方提供的模板,我在其中加入了镜像源的配置。

如果你是其他框架的项目,也可以找找官方有没有提供部署示例模板。

frontend\Dockerfile

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# syntax=docker.io/docker/dockerfile:1
FROM node:20-alpine AS base

# 使用国内镜像源
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories

# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat

WORKDIR /app

# 配置 npm 和 pnpm 使用国内镜像
RUN npm config set registry https://registry.npmmirror.com && \
corepack enable pnpm && \
pnpm config set registry https://registry.npmmirror.com && \
pnpm config set store-dir /root/.local/share/pnpm/store

# Install dependencies based on pnpm
COPY package.json pnpm-lock.yaml .npmrc* ./

RUN pnpm i --frozen-lockfile

# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app

# 配置 pnpm 使用国内镜像
RUN corepack enable pnpm && \
pnpm config set registry https://registry.npmmirror.com

COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Uncomment the following line in case you want to disable telemetry during the build.
ENV NEXT_TELEMETRY_DISABLED=1

RUN pnpm run build

# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app

ENV NODE_ENV=production
# Uncomment the following line in case you want to disable telemetry during runtime.
ENV NEXT_TELEMETRY_DISABLED=1

RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public

# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/config/next-config-js/output
CMD ["node", "server.js"]

frontend\docker-compose.yml

如果不写 docker-compose ,我们的 docker run 命令就得带一堆参数,不是很优雅。

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
services:
frontend:
# 指定镜像名称方便回滚
# 而容器名称交给 docker 自己分配,方便开多个实例,再利用 nginx 的 upstream 配置无缝升级版本
image: mypapers-nextjs
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
ports:
- "3000:3000"
env_file:
- .env
# network 配置是为了让后端或者其他一起加入到同一个网络中,方便互相通信
networks:
- mypapers

networks:
mypapers:
external: true

# 针对生产环境的健康检查 (可选)
# healthcheck:
# test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000"]
# interval: 30s
# timeout: 10s
# retries: 3

frontend\.dockerignore

实测不带这个文件,在 docker desktop 里跑的时候,会因为拷贝文件太多而失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
node_modules
.next
.git
.gitignore
README.md
.env*.local
.vscode
.idea
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.DS_Store
*.pem
coverage
.turbo

5. 在服务器上 clone 项目

SSH 到你的服务器上,然后 cd 进你刚刚在 github Environment Secrets 里配置的 SERVER_REPO_PATH,git clone 你的项目到这个位置。

如果是私有仓库,这边可能就要求你鉴权。你可以通过把服务器上的公钥复制进 git 仓库的设置页面解决这件事。

克隆好以后,别忘了 checkout 到有 script 脚本的那个分支。

之后,你就可以美美发一版上去测试啦。

  • 标题: 使用 Github Action + Docker + Git 全自动部署项目到国内服务器,并优化部署速度到 10s
  • 作者: 三葉Leaves
  • 创建于 : 2025-12-17 00:00:00
  • 更新于 : 2025-12-31 22:02:07
  • 链接: https://blog.oksanye.com/8d5330237612/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
你认为这篇文章怎么样?
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
评论
  • 按正序
  • 按倒序
  • 按热度
来发评论吧~
Powered by Waline v3.2.2