一键传输wifi互传
118.12M · 2026-02-04
前面我们通过 Github Action 实现了 html 项目的自动化部署,设置相对简单,日常开发用的场景也不多。更多的是基于框架的前端项目和后端项目打包部署,这次我们就实现一下
前后端项目通过 trae 生成,前端基于 vue3+vite,包含简单登录,菜单功能。后端基于 nestjs,功能对应前端,数据库用的 postgresql,项目地址
这是 deepseek 的回答
也就是说通过 Docker,我们可以把项目整体打包为一个镜像,部署的时候,直接拉取镜像运行即可,这对后端来说及其方便
通过在项目内编写 Dockerfile,就可以生成对应项目的镜像,以下是项目的前后端 Dockerfile 设置
前端的 Dockerfile 比较简单,每一步也有注释说明做了什么
FROM node:20-alpine AS build # 使用 Node 20 Alpine 作为前端构建阶段镜像
WORKDIR /app # 设置工作目录为 /app,后续命令都在此目录执行
RUN corepack enable # 启用 corepack,使 pnpm 等包管理工具可用
COPY package.json pnpm-lock.yaml ./ # 只复制依赖清单和锁文件,便于 Docker 利用缓存
RUN pnpm install --frozen-lockfile # 根据锁文件安装依赖,保证依赖版本一致
COPY . . # 复制当前项目所有源码到容器的 /app 目录
RUN pnpm build # 执行前端构建命令,生成 dist 静态资源
FROM scratch AS dist # 使用空镜像作为最终阶段,仅包含打包好的静态文件
COPY --from=build /app/dist /dist # 从构建阶段复制 dist 目录到最终镜像的 /dist
配置完成了,如何验证正确性呢,我们可以下载 docker desktop,下载安装,在frontend目录的命令行里执行构建命令
docker build -t aaa:ccc .
构建完成,我们就能在 docker desktop 看到构建好的镜像,但是点击启动按钮,镜像是无法启动的
以下是 trae 的解释
我们可以先用以下 Dockerfile 设置,生成镜像,测试结果是否正确
FROM node:20-alpine AS build # 使用 Node 20 Alpine 作为前端构建阶段镜像
WORKDIR /app # 设置工作目录为 /app,后续命令都在此目录执行
RUN corepack enable # 启用 corepack,使 pnpm 等包管理工具可用
COPY package.json pnpm-lock.yaml ./ # 只复制依赖清单和锁文件,便于 Docker 利用缓存
RUN pnpm install --frozen-lockfile # 根据锁文件安装依赖,保证依赖版本一致
COPY . . # 复制当前项目所有源码到容器的 /app 目录
RUN pnpm build # 执行前端构建命令,生成 dist 静态资源
FROM node:20-alpine AS runner # 使用轻量 Node 20 Alpine 作为运行阶段镜像
WORKDIR /app # 运行阶段同样使用 /app 作为工作目录
COPY --from=build /app/dist ./dist # 从构建阶段复制打包好的 dist 到运行镜像中
RUN npm install -g http-server # 全局安装 http-server 用于提供静态文件服务
EXPOSE 8080 # 声明容器对外暴露的端口为 8080
CMD ["http-server", "-p", "8080", "dist"] # 使用 http-server 在 8080 端口托管 dist 目录
镜像创建成功,运行镜像就可以创建基于该镜像的容器,点击红框启动服务
成功打开前端项目
以下是backendDockerfile 设置
FROM node:20-alpine AS deps # 使用 Node 20 Alpine 作为依赖安装阶段镜像
WORKDIR /app # 设置工作目录为 /app,后续命令都在此目录执行
RUN corepack enable # 启用 corepack,使 pnpm 等包管理工具可用
COPY package.json pnpm-lock.yaml ./ # 只复制依赖清单和锁文件,便于 Docker 利用缓存
RUN pnpm install --frozen-lockfile # 根据锁文件安装依赖,确保依赖版本一致
FROM node:20-alpine AS build # 使用 Node 20 Alpine 作为构建阶段镜像
WORKDIR /app # 构建阶段同样使用 /app 作为工作目录
RUN corepack enable # 再次启用 corepack,确保 pnpm 可用
COPY --from=deps /app/node_modules ./node_modules # 从 deps 阶段复制安装好的依赖,避免重复安装
COPY . . # 复制项目全部源码到容器中
RUN pnpm prisma:generate # 生成 Prisma Client 代码
RUN pnpm build # 编译 TypeScript,输出到 dist 目录
FROM node:20-alpine AS runner # 使用轻量 Node 20 Alpine 作为运行阶段镜像
WORKDIR /app # 运行阶段同样在 /app 目录下执行
ENV NODE_ENV=production # 设置为生产环境,供应用按生产模式运行
COPY --from=build /app/node_modules ./node_modules # 复制构建阶段的依赖,用于运行时加载
COPY --from=build /app/dist ./dist # 复制编译后的 dist 目录
COPY --from=build /app/prisma ./prisma # 复制 Prisma 相关文件(schema、迁移等)
COPY --from=build /app/prisma.config.ts ./prisma.config.ts
COPY --from=build /app/tsconfig.json ./tsconfig.json
EXPOSE 3001 # 声明容器对外暴露的端口为 3001
CMD ["node", "dist/src/main.js"] # 使用 Node 启动 NestJS 编译后的入口文件
上面的配置你可能会有疑惑,里面三块设置写在一起应该也没问题,为什么靠分开写,这里要提到Dockerfile的分阶段构建
上面配置构建可分为,依赖-编译-运行三个环节,将依赖安装单独构建,好处是后面如果只更改业务代码时,这一段不会重复执行,Docker 直接复用缓存,另外一个好处就是每个阶段职责分明,便于管理
参照前端容器运行的步骤,成功启动后端容器,在容器日志栏可以看到服务启动成功
但现在服务实际还不能用,因为 postgresql 服务还没启动,我们还需要部署一个 postgresql 容器,如何让两个容器产生关联,就要用到 Docker Compose
借用 DeepSeek 的解释
是的,多个容器运行与协作,就要用到 Docker Compose 的容器编排,通过 YAML 文件处理不同容器的依赖关系,backend 和 db(postgresql)设置如下
version: '3.9' # 使用 docker-compose v3.9 语法版本
services: # 定义要运行的多个服务(容器)
db: # 数据库服务:PostgreSQL
image: postgres:16-alpine # 使用官方 Postgres 16 的 Alpine 轻量镜像
container_name: demo-postgres # 容器名称,方便调试和引用
environment: # 数据库初始化环境变量
POSTGRES_USER: postgres # 数据库用户名
POSTGRES_PASSWORD: postgres # 数据库密码
POSTGRES_DB: docker_demo # 默认创建的数据库名称
ports:
- "5432:5432" # 将宿主机 5432 端口映射到容器 5432
volumes:
- db-data:/var/lib/postgresql/data # 持久化数据库数据到名为 db-data 的卷
healthcheck: # 健康检查,确保数据库就绪后再启动依赖服务
test: ["CMD-SHELL", "pg_isready -U postgres"] # 使用 pg_isready 检查数据库状态
interval: 10s # 每 10 秒检查一次
timeout: 5s # 每次检查的超时时间为 5 秒
retries: 5 # 连续 5 次失败视为不健康
backend: # 后端服务:NestJS 应用
build: # 构建镜像配置
context: . # 构建上下文为当前 backend 目录
dockerfile: Dockerfile # 使用当前目录下的 Dockerfile
container_name: demo-backend # 后端容器名称
environment: # 传入后端应用所需环境变量
DATABASE_URL: postgresql://postgres:postgres@db:5432/docker_demo?schema=public # 连接 db 服务的数据库 URL
PORT: 3001 # 应用端口,保持与 main.ts 中一致
JWT_SECRET: your-secret-key # JWT 签名秘钥,用于生成和验证 Token
NODE_ENV: production # 运行环境设为 production
depends_on: # 声明依赖关系
db: # 依赖上面的 db 服务
condition: service_healthy # 仅在 db 健康检查通过后才启动 backend
ports:
- "3001:3001" # 将宿主机 3001 端口映射到后端容器 3001
volumes: # 定义可复用的命名卷
db-data: # Postgres 数据持久化卷
配置不算复杂,看注释基本都可以明白。在backend目录下执行,需要把目录下Dockerfile注释删除,避免运行报错
docker compose up -d --build
构建成功后,可以在 docker destop 容器里看到相应的服务
点击demo-backend进入日志面板,可以看到服务已经启动,点击进入接口页面,测试登录接口,请求返回异常,日志面板查看错误大概率跟数据库有关,因为我们只创建了数据库,还没初始化数据库,执行在 package.json 配置的脚本
// 生成迁移文件,执行迁移
pnpm prisma:migrate
// 初始化数据
pnpm prisma:seed
此时再去 swagger 测试接口,可以正常请求
前面我们已经启动了前端容器,但还不能访问这个后端服务,因为在前端生产环境配置的VITE_API_BASE_URL是前缀/api,如果要请求后端服务,还需要 nginx 做转发,在docker-compose.yaml设置 nginx 可以实现,这里我们先忽略,因为服务器之前已安装过 nginx,因此在项目部署后,使用服务器的 nginx 配置
最后我们需要在项目根目录下创建一个docker-compose.yaml文件,把前端、后端、数据库都编排进来
services:
db:
image: postgres:16-alpine
container_name: demo-postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: docker_demo
ports:
- "5432:5432"
volumes:
- db-data:/var/lib/postgresql/data
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: demo-backend
environment:
DATABASE_URL: postgresql://postgres:postgres@db:5432/docker_demo?schema=public
PORT: 3001
JWT_SECRET: your-super-secret-jwt-key-change-in-production
NODE_ENV: production
depends_on:
- db
ports:
- "3001:3001"
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: demo-frontend
depends_on:
- backend
ports:
- "8080:8080"
volumes:
db-data:
原理就是项目提交到 github 仓库后,触发创建好的工作流文件,具体步骤可以参考往期 小白服务器踩坑(2),在项目根目录.github/workflows下创建 deploy.yml 文件,内容如下
name: CI & Deploy
on:
push:
branches: [master]
workflow_dispatch:
jobs:
build-and-test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15-alpine
env:
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_password
POSTGRES_DB: test_db
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.26.2
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"
cache-dependency-path: |
backend/pnpm-lock.yaml
frontend/pnpm-lock.yaml
- name: Install backend dependencies
working-directory: backend
run: pnpm install --frozen-lockfile
- name: Generate Prisma client
working-directory: backend
run: pnpm prisma:generate
- name: Build backend
working-directory: backend
run: pnpm build
- name: Run backend tests
working-directory: backend
env:
DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db?schema=public
run: |
pnpm prisma db push
pnpm test --runInBand
- name: Install frontend dependencies
working-directory: frontend
run: pnpm install --frozen-lockfile
- name: Build frontend
working-directory: frontend
run: pnpm build
deploy:
needs: build-and-test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master'
steps:
- name: Deploy over SSH
uses: appleboy/ssh-action@v1
env:
TARGET_DIR: ${{ secrets.SSH_TARGET_DIR }}
REPO_URL: git@github.com:${{ github.repository }}.git
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
envs: TARGET_DIR,REPO_URL
script: |
if [ ! -d "$TARGET_DIR/.git" ]; then
mkdir -p "$TARGET_DIR"
git clone "$REPO_URL" "$TARGET_DIR"
fi
cd "$TARGET_DIR"
git pull
# 1. 确保镜像最新
docker compose build
# 2. 启动数据库 (如果尚未启动)
docker compose up -d db
# 3. 等待数据库准备就绪 (简单的延时,生产环境建议用 healthcheck)
echo "Waiting for database..."
sleep 10
# 4. 执行数据库迁移 (Prisma 会自动跳过已应用的迁移)
docker compose run --rm backend npx prisma migrate deploy
# 5. 执行数据填充 (Seed 脚本通常是幂等的)
docker compose run --rm backend npx prisma db seed
# 6. 启动/更新所有服务
docker compose up -d
自动化过程分为build-and-test和deploy,build-and-test创建了数据库,然后构建前后端项目,运行前后端项目的测试用例。deploy拉取仓库到服务器,先启动数据库容器,然后迁移数据库、更新数据,最后启动前后端服务,部署完成后,到服务器查看容器状态,可以看到前后端、数据库已启动成功
我们可以在服务器运行docker exec判断服务启动是否正常
docker exec demo-frontend wget -qO- 查看前端运行结果
docker exec demo-backend wget -qO- 查看后端接口页面
docker exec demo-postgres psql -U postgres -d docker_demo -c "SELECT 'DB OK';"检测数据库连接
现在只是服务内部可以访问,外部还无法访问,还需要配置 nginx
首先配置前端项目的外部访问,这里我们新添加一个子域名docker-demo.ankkaya.top,nginx 配置针对这个域名的转发
server {
listen 80;
server_name docker-demo.ankkaya.top;
return 301 https://$server_name$request_uri;
}
因为前端路由配置的是history模式,还需要处理异常情况
server {
listen 443 ssl;
server_name docker-demo.ankkaya.top;
ssl_certificate /etc/nginx/ssl/ankkaya.top_nginx/ankkaya.top_bundle.crt;
ssl_certificate_key /etc/nginx/ssl/ankkaya.top_nginx/ankkaya.top.key;
# ======================
# 前端(8080)
# ======================
location / {
proxy_pass ;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 支持 WebSocket(如果你前端用到了)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# 页面刷新 404
proxy_intercept_errors on;
error_page 404 = /index.html;
}
}
前端生产环境请求配置前缀/api,也需要在 nginx 做一下转发,这部分放在上面 server 内部
# ======================
# 后端(3001)
# 前缀:/api
# ======================
location /api/ {
proxy_pass ;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
验证 nginx 配置,重启配置
输入地址 docker-demo.ankkaya.top/,用户名admin,密码123456,成功登录,自动化部署完成
对于只有前端开发经验的我,如果是放在 ai coding 以前,想独自完成前后端项目+自动化部署,几天内根本不可能,ai coding 其中一个好处就是,即使我们对某一方面的知识不知道或者不熟悉,也有可能依靠 ai 一步一步达成我们的目标,在不断纠错过程中,也能逐渐学习新知识
这次我全程使用 trae 国际版,主要用的GPT-5.1,不得不说真是开发,学习的利器。因为没有实际后端开发和部署经验,我也知道项目某些配置和自动化流程并不是最优的,当然拿来作为学习是不错的选择
数据库端口不要轻易暴露,注意用户名和密码强度,我昨天创建的数据库,今天已经被爆破,还附上了勒索信息,还好这都是测试数据