引言

前面我们通过 Github Action 实现了 html 项目的自动化部署,设置相对简单,日常开发用的场景也不多。更多的是基于框架的前端项目和后端项目打包部署,这次我们就实现一下

前后端项目通过 trae 生成,前端基于 vue3+vite,包含简单登录,菜单功能。后端基于 nestjs,功能对应前端,数据库用的 postgresql,项目地址

为什么是 Docker

这是 deepseek 的回答

也就是说通过 Docker,我们可以把项目整体打包为一个镜像,部署的时候,直接拉取镜像运行即可,这对后端来说及其方便

编写 Dockerfile

frontend

通过在项目内编写 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 目录

镜像创建成功,运行镜像就可以创建基于该镜像的容器,点击红框启动服务

成功打开前端项目

backend

以下是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

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 Actions

原理就是项目提交到 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-testdeploybuild-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

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,不得不说真是开发,学习的利器。因为没有实际后端开发和部署经验,我也知道项目某些配置和自动化流程并不是最优的,当然拿来作为学习是不错的选择

插曲

数据库端口不要轻易暴露,注意用户名和密码强度,我昨天创建的数据库,今天已经被爆破,还附上了勒索信息,还好这都是测试数据

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com