从本地开发到生产部署:用 Docker Compose 跑通 NestJS、MySQL 与 Milvus

本文不是一篇只介绍 Docker 概念的笔记,而是一篇围绕真实项目展开的部署教程。我们会基于当前项目 nest-dockerfile-test 的实际源码,讲清楚如何用 Docker Compose 提升本地开发效率,以及如何把 NestJS 服务、MySQL 数据库一起编排成一套可运行的生产环境。

这篇文章默认读者已经会写一点 JavaScript 或 TypeScript,但不要求你系统学过 Docker、Dockerfile、Docker Compose 或 NestJS 部署。读完之后,你应该能理解下面几件事:

  • 为什么后端项目不能只在本机 pnpm run start:dev 跑起来就算完成。
  • Docker 镜像、容器、数据卷、端口映射、容器网络分别解决什么问题。
  • 本地开发环境为什么通常只用 Compose 启动数据库和中间件,而业务代码仍然在宿主机热更新运行。
  • 生产环境为什么要把业务服务也打成镜像,并让业务容器通过容器名访问数据库容器。
  • 当前项目的 Dockerfile、docker-compose.dev.ymldocker-compose.prod.yml 每一段配置在做什么。
  • 新手如何从零执行命令,把项目在本地开发模式和生产部署模式都跑通。

本文基于当前源码,而不是抽象模板。项目里已经包含:

  • NestJS 11 后端服务。
  • TypeORM + MySQL 的书籍 CRUD 接口。
  • 一个 /books 静态管理页面。
  • 本地开发用的 docker-compose.dev.yml
  • 生产部署用的 docker-compose.prod.yml
  • 用来构建 NestJS 镜像的 Dockerfile
  • 用于镜像上下文裁剪的 .dockerignore

需要提前说明:当前项目的 Dockerfile 是单阶段构建,不是多阶段构建。单阶段写法更容易让新手理解完整流程,但生产环境里可以进一步优化镜像体积。本文会先按当前实现讲清楚,再在后面补充生产优化建议。

一、为什么后端项目一定要认真处理本地环境和部署环境

很多新手第一次写 NestJS、Express 或 Spring Boot 项目时,最熟悉的启动方式是:

bash 复制代码
pnpm install
pnpm run start:dev

只要控制台没有报错,浏览器能访问接口,就会觉得项目已经跑通了。但真实项目的问题通常不在"代码能不能启动",而在"代码依赖的环境能不能稳定复现"。

一个稍微完整一点的后端项目,通常不会只有一个 HTTP 服务。它可能还会依赖:

  • MySQL:保存用户、订单、书籍、权限、配置等核心业务数据。
  • Redis:做缓存、分布式锁、验证码、会话、短期记忆。
  • Elasticsearch:做关键词检索和日志检索。
  • Milvus:做向量检索,常见于 RAG、知识库和语义搜索。
  • MinIO:做对象存储,保存文件、图片、语音、模型产物。
  • 消息队列:做异步任务、削峰填谷、事件解耦。

在 AI 应用开发里,这种依赖会更明显。一个 RAG 系统可能会同时用到 MySQL 存业务元数据,Milvus 存向量,MinIO 存原始文档,Redis 存会话和任务状态。Agent 系统也经常需要数据库、中间件、任务队列和模型服务一起协同。也就是说,后端代码只是整个系统中的"调度层",真正支撑业务运行的是代码、数据库和中间件组成的一整套环境。

如果没有 Docker Compose,本地开发会遇到几个典型问题。

第一,环境安装成本高。每个新同事都要手动安装 MySQL、配置端口、建数据库、设置字符集,再安装 Milvus 依赖的 etcd 和 MinIO。只要某一步版本不一致,后面就可能出现奇怪问题。

第二,环境状态不可控。有人本地 MySQL 是 8.x,有人是 9.x;有人 root 密码是 admin,有人是 123456;有人端口是 3306,有人因为冲突改成了 3307。项目启动脚本看起来一样,实际连接到的环境却完全不同。

第三,部署方式和开发方式割裂。本地开发时服务连的是 localhost,上服务器后服务运行在容器里,localhost 就变成了容器自己。如果开发时没有理解容器网络,生产环境最常见的错误就是业务容器一直报 ECONNREFUSED 127.0.0.1:3306

Docker 和 Docker Compose 的价值就在这里:它们把环境从"靠人手动配置"变成"靠配置文件声明"。一个团队只要维护好 Compose 文件,新人可以用一条命令启动一组依赖服务,服务器也可以用一条命令拉起业务服务和数据库。

二、先把几个 Docker 核心概念讲明白

在正式看配置前,先把 Docker 里最容易混淆的几个概念讲清楚。新手学习 Docker 最大的问题不是命令记不住,而是不知道每个概念在系统链路里承担什么职责。

1. 镜像:应用运行环境的只读模板

镜像可以理解成一个打包好的运行模板。比如 mysql:latest 是 MySQL 的镜像,里面包含 MySQL 服务需要的程序和默认文件结构。node:24-alpine 是 Node.js 的镜像,里面有 Node 运行时和 Alpine Linux 基础环境。

镜像本身不会运行,它只是一个静态产物。你可以把它理解成"安装包 + 基础系统 + 默认配置"的组合。

当前项目的 Dockerfile 就是为了把 NestJS 项目构建成一个业务镜像。这个镜像里会包含:

  • Node.js 运行时。
  • pnpm 包管理器。
  • 项目依赖。
  • 编译后的 NestJS 代码。
  • 容器启动时执行的命令。

2. 容器:镜像运行起来后的实例

容器是镜像运行起来后的进程环境。同一个镜像可以启动多个容器,就像同一个类可以创建多个对象。

比如你可以用 mysql:latest 镜像启动一个叫 mysql-dev 的容器,也可以启动一个叫 mysql-prod 的容器。它们来自同一个镜像,但数据目录、容器名、网络、端口映射都可以不同。

在当前项目中:

  • 本地开发 Compose 会启动 mysql-devmilvus-etcdmilvus-miniomilvus-standalone
  • 生产 Compose 会启动 mysql-prodnest-app

3. 端口映射:把容器内端口暴露给宿主机

容器有自己的网络命名空间。MySQL 在容器里监听 3306,不代表你的 Mac 或 Linux 主机能直接访问它。要让宿主机访问容器内端口,需要做端口映射:

yaml 复制代码
ports:
  - '3306:3306'

左边是宿主机端口,右边是容器端口。'3306:3306' 的意思是:访问宿主机的 3306,转发到容器内部的 3306

NestJS 服务也是一样:

yaml 复制代码
ports:
  - '3000:3000'

访问宿主机的 http://localhost:3000,实际会进入 nest-app 容器内部的 3000 端口。

4. 数据卷:让容器数据持久化

容器可以删除重建。如果数据库的数据只放在容器内部,那么容器删除后数据也会丢失。数据库这种有状态服务必须把数据目录挂载到宿主机。

当前项目的 MySQL 开发环境配置是:

yaml 复制代码
volumes:
  - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/mysql:/var/lib/mysql

右边 /var/lib/mysql 是 MySQL 容器内部保存数据的目录。左边 ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/mysql 是宿主机目录。这样 MySQL 写入的数据会落到项目目录下的 volumes/mysql,容器重建后仍然能复用。

这个 ${DOCKER_VOLUME_DIRECTORY:-.} 是 Compose 的变量语法。意思是:如果环境变量 DOCKER_VOLUME_DIRECTORY 有值,就用它;如果没有值,就用当前目录 .。这让数据目录既有默认值,也允许你在不同机器上自定义。

5. 容器网络:容器之间用服务名互相访问

Docker Compose 会为同一个 Compose 项目创建网络。处在同一个网络里的服务,可以通过服务名互相访问。

生产环境里,NestJS 不是通过 localhost 访问 MySQL,而是通过服务名 mysql-prod 访问:

ts 复制代码
host: isProduction ? 'mysql-prod' : 'localhost',

这行代码非常关键。因为当 NestJS 在容器里运行时,localhost 指的是 nest-app 容器自己,不是 MySQL 容器,也不是宿主机。如果继续写 localhost,业务容器会去自己内部找 MySQL,自然连接失败。

这个差异是很多人第一次 Docker 部署后端服务时最容易踩的坑。

三、当前项目的整体结构

先看项目结构中和部署有关的文件:

text 复制代码
nest-dockerfile-test
├── Dockerfile
├── .dockerignore
├── docker-compose.dev.yml
├── docker-compose.prod.yml
├── .env.example
├── package.json
├── nest-cli.json
├── public
│   └── index.html
└── src
    ├── main.ts
    ├── app.module.ts
    └── book
        ├── book.controller.ts
        ├── book.service.ts
        ├── dto
        │   ├── create-book.dto.ts
        │   └── update-book.dto.ts
        └── entities
            └── book.entity.ts

业务功能很简单:一个书籍管理系统。后端提供 /book CRUD 接口,前端静态页面通过 fetch('/book') 调用后端接口,数据存到 MySQL 的 books 表。

整体链路可以用下面这张图理解:

开发环境和生产环境的部署方式不同:

flowchart TB subgraph Dev[本地开发模式] DevBrowser[浏览器] --> DevNest[NestJS 在宿主机运行 pnpm run start:dev] DevNest --> DevMysql[(mysql-dev 容器)] DevMilvus[Milvus 相关容器] --> DevEtcd[etcd] DevMilvus --> DevMinio[MinIO] end subgraph Prod[生产 Compose 模式] ProdBrowser[浏览器] --> HostPort[宿主机 3000 端口] HostPort --> NestContainer[nest-app 容器] NestContainer --> MysqlProd[(mysql-prod 容器)] end

开发环境的重点是效率:数据库和中间件用容器跑,业务代码在宿主机用 watch 模式跑,这样改代码能立刻生效。

生产环境的重点是一致性:业务代码也构建成镜像,和 MySQL 一起由 Compose 管理,服务重启、网络、端口、依赖关系都写在配置文件里。

四、当前 NestJS 服务做了什么

部署教程不能只讲 Docker。你必须知道容器里跑的到底是什么服务,否则出了问题也不知道该检查哪一层。

项目入口是 src/main.ts

ts 复制代码
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

这段代码做了两件事。

第一,用 NestFactory.create(AppModule) 创建 Nest 应用实例。AppModule 是整个应用的根模块,里面会注册静态资源、数据库连接、业务模块。

第二,监听端口。这里优先读取 process.env.PORT,如果没有设置,就使用 3000。这对部署很重要,因为容器默认暴露的是 3000,生产 Compose 也把宿主机 3000 映射到容器 3000

根模块 src/app.module.ts 是理解开发和生产差异的关键:

ts 复制代码
const isProduction = process.env.NODE_ENV === 'production';

@Module({
  imports: [
    ServeStaticModule.forRoot({
      rootPath: join(__dirname, 'public'),
      serveRoot: '/books',
    }),
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: isProduction ? 'mysql-prod' : 'localhost',
      port: 3306,
      username: 'root',
      password: 'admin',
      database: 'book',
      synchronize: true,
      logging: true,
      autoLoadEntities: true,
      entities: [Book],
    }),
    BookModule,
  ],
})
export class AppModule {}

这里有三个点值得展开。

第一,ServeStaticModule.forRoot 把静态页面挂到 /books。编译后静态文件会被放到 dist/public,运行时 __dirname 指向 dist,所以 join(__dirname, 'public') 会定位到 dist/public。这也是为什么 nest-cli.json 里必须配置 assets 拷贝规则,否则生产镜像里可能只有 JS,没有 public/index.html

第二,TypeOrmModule.forRoot 配置 MySQL 连接。开发时 NODE_ENV 不是 production,所以 host 是 localhost。这是因为开发模式下 NestJS 在宿主机运行,而 MySQL 容器通过端口映射暴露到了宿主机的 3306。生产时 NODE_ENV=production,NestJS 在 nest-app 容器中运行,此时要通过 Compose 服务名 mysql-prod 访问数据库。

第三,synchronize: true 会让 TypeORM 根据 Entity 自动同步表结构。它对 demo 和本地学习很方便,因为你不用手写建表 SQL。但在真实生产项目里不建议长期打开。生产环境应该使用 migration 管理表结构变更,否则一次字段调整可能直接影响线上数据。

书籍表结构定义在 Book Entity:

ts 复制代码
@Entity({ name: 'books' })
export class Book {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ length: 255 })
  title: string;

  @Column({ length: 255 })
  author: string;

  @Column({ type: 'text' })
  description: string;

  @Column({ type: 'decimal', precision: 10, scale: 2 })
  price: number;

  @Column({ type: 'int', default: 0 })
  stock: number;

  @Column({ type: 'datetime' })
  publishedAt: Date;

  @CreateDateColumn({ type: 'datetime' })
  createdAt: Date;

  @UpdateDateColumn({ type: 'datetime' })
  updatedAt: Date;
}

这段 Entity 的作用是把 TypeScript 类映射成 MySQL 表。@Entity({ name: 'books' }) 指定表名,@PrimaryGeneratedColumn() 指定自增主键,@Column() 指定普通字段,@CreateDateColumn@UpdateDateColumn 让 TypeORM 自动维护创建时间和更新时间。

这里 price 使用了 decimal(10, 2),这是和金额相关字段更合适的做法。浮点数容易出现精度问题,数据库里用 decimal 可以避免很多不必要的金额误差。

业务逻辑在 BookService

ts 复制代码
@Injectable()
export class BookService {
  @Inject(EntityManager)
  private readonly entityManager: EntityManager;

  async create(createBookDto: CreateBookDto) {
    const book = this.entityManager.create(Book, {
      ...createBookDto,
      publishedAt: new Date(createBookDto.publishedAt),
    });
    return this.entityManager.save(Book, book);
  }

  async findAll() {
    return this.entityManager.find(Book, {
      order: { id: 'DESC' },
    });
  }

  async findOne(id: number) {
    const book = await this.entityManager.findOneBy(Book, { id });
    if (!book) {
      throw new NotFoundException(`Book #${id} not found`);
    }
    return book;
  }
}

这段代码在部署链路里的位置是"业务容器内部的应用逻辑"。浏览器访问 /book,请求先进入 Nest Controller,再调用 Service,最后通过 TypeORM 访问 MySQL。

publishedAt: new Date(createBookDto.publishedAt) 这个转换也值得注意。前端表单传上来的是字符串,比如 "2008-08-01",数据库字段是 datetime。在保存前显式转成 Date,比把字符串直接丢给 ORM 更清晰。

前端页面在 public/index.html,它不是独立前端工程,而是由 NestJS 静态托管。核心调用方式如下:

js 复制代码
const loadBooks = async () => {
  const response = await fetch('/book');
  const books = await response.json();
  renderRows(books);
};

form.addEventListener('submit', async (event) => {
  event.preventDefault();

  const id = inputs.id.value.trim();
  const payload = mapFormData();
  const method = id ? 'PATCH' : 'POST';
  const url = id ? `/book/${id}` : '/book';

  await fetch(url, {
    method,
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload),
  });

  await loadBooks();
});

这里用相对路径 /book,而不是写死 http://localhost:3000/book。这样做有一个部署优势:开发环境和生产环境只要页面和 API 同源,就不需要处理跨域,也不需要在前端代码里区分不同 API 地址。

五、Dockerfile:如何把 NestJS 项目变成镜像

当前项目的 Dockerfile 如下:

dockerfile 复制代码
ARG NODE_IMAGE=node:24-alpine
FROM ${NODE_IMAGE}

WORKDIR /app

COPY package.json pnpm-lock.yaml ./

RUN npm config set registry https://registry.npmmirror.com/ \
  && npm install -g pnpm@10.14.0 \
  && pnpm install --frozen-lockfile

COPY . .

RUN pnpm run build

EXPOSE 3000

CMD ["node", "dist/main.js"]

逐行拆开看。

ARG NODE_IMAGE=node:24-alpine 定义了构建参数。默认使用 node:24-alpine,但生产 Compose 可以通过 build args 覆盖它。当前项目的 .env.example 就给了国内网络环境下的镜像源示例:

env 复制代码
NODE_IMAGE=docker.m.daocloud.io/library/node:24-alpine
MYSQL_IMAGE=docker.m.daocloud.io/library/mysql:latest

这不是业务逻辑,而是工程部署上的容错设计。很多机器直接拉 Docker Hub 会超时,允许通过环境变量替换镜像源,可以降低部署门槛。

FROM ${NODE_IMAGE} 表示基于 Node 镜像构建业务镜像。这里的 Node 版本要和项目依赖兼容。当前项目使用 NestJS 11、TypeScript 5.7,并且 package 里已经有 @types/node 24.x,所以 node:24-alpine 是匹配的。

WORKDIR /app 设置容器内工作目录。后续 COPYRUNCMD 都以 /app 为默认目录。这样容器里的文件结构清晰,也避免命令在根目录里执行。

COPY package.json pnpm-lock.yaml ./ 只先复制依赖清单,不直接复制全部源码。这是 Docker 缓存优化的常见写法。依赖文件不变时,后面的安装依赖层可以复用缓存;你只是改了业务代码,不需要每次都重新安装依赖。

pnpm install --frozen-lockfile 表示严格按 pnpm-lock.yaml 安装依赖。如果 lock 文件和 package.json 不一致,安装会失败。这在构建镜像时是好事,因为它可以避免"我本地能装、服务器装出来不一样"的问题。

COPY . . 把项目代码复制进镜像。这里要配合 .dockerignore 使用,否则 node_modules.gitvolumes 这些不该进入镜像的目录都会被塞进去,镜像构建会又慢又大。

RUN pnpm run build 执行 Nest 编译。当前项目的 build 命令是:

json 复制代码
{
  "scripts": {
    "build": "nest build"
  }
}

编译后会生成 dist 目录。因为 nest-cli.json 配置了 assets,public 静态文件也会被拷贝到 dist/public

json 复制代码
{
  "compilerOptions": {
    "deleteOutDir": true,
    "assets": [
      {
        "include": "../public/**/*",
        "outDir": "dist/public"
      }
    ]
  }
}

如果没有这段配置,/books 页面在本地开发时可能正常,但生产镜像里访问会失败。原因是 TypeScript 编译默认只处理源码,不会自动把任意静态目录复制到 dist

EXPOSE 3000 只是声明容器内服务使用 3000 端口。它不会自动把端口暴露到宿主机。真正让宿主机访问容器的是 Compose 里的 ports

CMD ["node", "dist/main.js"] 是容器启动命令。镜像构建时执行 RUN,容器启动时执行 CMD。这两者要分清:构建阶段编译代码,运行阶段启动服务。

当前 Dockerfile 是单阶段构建,所以镜像里会保留安装依赖和构建过程所需的一些内容。学习阶段这样更直观,生产环境可以进一步改成多阶段构建:第一阶段安装完整依赖并编译,第二阶段只保留生产依赖和 dist。但如果你还没把单阶段流程跑通,不建议一开始就上复杂优化。

六、.dockerignore:控制什么文件不要进镜像

当前项目的 .dockerignore 是:

text 复制代码
node_modules/
.vscode/
.git/
dist/
coverage/
volumes/
.env
.env.*
.tmp-*
*.log

这份文件非常重要。Docker 构建时会把当前目录作为 build context 发送给 Docker 引擎。如果不忽略这些文件,会带来几个问题。

第一,node_modules 很大,而且宿主机上的依赖不一定适合容器环境。比如 macOS 上安装的二进制依赖,不能直接拿到 Linux 容器里用。正确做法是在镜像内重新安装。

第二,.git 没必要进入镜像。它会增加构建上下文体积,也可能泄露提交信息。

第三,volumes 是数据库和中间件数据目录,绝对不能复制进业务镜像。数据库数据应该通过 volume 挂载管理,不应该混进应用镜像。

第四,.env.env.* 可能包含密码、Token、密钥。当前 demo 里密码很简单,但真实项目里一定不要把环境变量文件打进镜像。

第五,dist 应该由镜像构建过程生成。如果把宿主机旧的 dist 复制进去,很容易出现"源码已经改了但镜像里跑的是旧产物"的错觉。

七、本地开发 Compose:一条命令拉起数据库和中间件

当前项目的本地开发 Compose 文件是 docker-compose.dev.yml。它没有启动 NestJS 应用,而是启动 MySQL 和 Milvus 相关基础服务。

这个设计是合理的。开发阶段你希望改一行 TypeScript 代码就能热更新,所以 NestJS 仍然在宿主机上通过 pnpm run start:dev 运行。而 MySQL、Milvus 这类基础服务不需要频繁改代码,放到容器里统一管理即可。

开发环境 MySQL 配置如下:

yaml 复制代码
services:
  mysql:
    image: mysql:latest
    container_name: mysql-dev
    ports:
      - '3306:3306'
    environment:
      MYSQL_ROOT_PASSWORD: admin
      MYSQL_DATABASE: book
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci
    volumes:
      - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/mysql:/var/lib/mysql
    restart: always

这里 MYSQL_ROOT_PASSWORD: admin 会设置 root 密码,MYSQL_DATABASE: book 会在 MySQL 初始化时创建 book 数据库。NestJS 里 TypeORM 连接的数据库名也是 book,所以这两个地方必须一致。

command 设置了默认字符集为 utf8mb4。这对中文内容很重要。MySQL 早期的 utf8 并不是真正完整的 UTF-8,utf8mb4 才能完整支持中文、emoji 和更多 Unicode 字符。

Milvus 在开发 Compose 里由三个服务组成:etcdminiostandalone。它们的关系可以这样理解:

flowchart LR Milvus[Milvus standalone] --> Etcd[etcd 元数据] Milvus --> Minio[MinIO 对象存储] Client[本地 AI 应用或脚本] --> Milvus

Milvus 本身负责向量检索。etcd 用来保存元数据和协调信息,MinIO 用来保存对象数据。你当前的 NestJS 书籍 CRUD 还没有直接调用 Milvus,但开发环境已经把 Milvus 编排进来了,这对后续扩展 RAG 或语义检索是有价值的。

standalone 服务里有两段配置很关键:

yaml 复制代码
environment:
  MINIO_REGION: us-east-1
  ETCD_ENDPOINTS: etcd:2379
  MINIO_ADDRESS: minio:9000
depends_on:
  - 'etcd'
  - 'minio'

ETCD_ENDPOINTS: etcd:2379 说明 Milvus 通过服务名 etcd 访问 etcd。MINIO_ADDRESS: minio:9000 说明 Milvus 通过服务名 minio 访问 MinIO。这就是 Compose 网络的价值:容器之间不用写宿主机 IP,直接写服务名。

文件最后定义了默认网络:

yaml 复制代码
networks:
  default:
    name: common-network

这会创建或复用一个叫 common-network 的网络。统一网络名的好处是后续其他 Compose 项目如果也接入这个网络,就能和这里的中间件互通。但也要注意,网络名固定后,不同项目之间可能发生命名和服务访问上的耦合,团队里要约定清楚。

八、本地开发从零启动教程

下面是新手可以直接照着执行的本地开发流程。

1. 准备基础环境

你需要先安装:

  • Docker Desktop 或 Docker Engine。
  • Node.js,建议使用和项目匹配的较新版本。
  • pnpm。

检查 Docker 是否可用:

bash 复制代码
docker version
docker compose version

检查 Node 和 pnpm:

bash 复制代码
node -v
pnpm -v

如果 docker compose version 能输出版本,说明你使用的是新版本 Compose 插件,命令写法是 docker compose。老教程里的 docker-compose 是旧命令,能不用就不用。

2. 进入项目目录

bash 复制代码
cd "/Users/zz/AI learning/codeing/tool-test/nest-dockerfile-test"

路径里有空格,所以命令里要加引号。新手经常在这里踩坑:AI learning 中间有空格,如果不加引号,shell 会把它拆成两个参数。

3. 安装项目依赖

bash 复制代码
pnpm install

这一步安装的是宿主机开发环境依赖。它和 Dockerfile 里的 pnpm install --frozen-lockfile 不是同一次安装。

  • 本地 pnpm install 是为了让你能在宿主机运行 pnpm run start:dev
  • Dockerfile 里的安装是为了构建生产镜像,让容器内部也有运行项目所需的依赖。

4. 启动开发环境基础服务

当前 package.json 已经提供了脚本:

json 复制代码
{
  "scripts": {
    "docker:up": "docker compose -f docker-compose.dev.yml up -d",
    "docker:down": "docker compose -f docker-compose.dev.yml down"
  }
}

启动:

bash 复制代码
pnpm run docker:up

这个命令会在后台启动 MySQL、etcd、MinIO、Milvus。第一次执行会拉取镜像,耗时可能比较久,尤其是 Milvus 镜像比较大。

查看容器状态:

bash 复制代码
docker compose -f docker-compose.dev.yml ps

如果想看日志:

bash 复制代码
docker compose -f docker-compose.dev.yml logs -f mysql
docker compose -f docker-compose.dev.yml logs -f standalone

MySQL 启动成功后,宿主机的 3306 端口会连到 mysql-dev 容器。Milvus 默认暴露:

  • 19530:Milvus SDK 访问端口。
  • 9091:Milvus 健康检查端口。
  • 9000:MinIO API。
  • 9001:MinIO 控制台。

5. 启动 NestJS 开发服务

开发 Compose 不会启动 NestJS,所以还需要在宿主机执行:

bash 复制代码
pnpm run start:dev

启动后 NestJS 会读取 AppModule 中的配置。因为此时没有设置 NODE_ENV=production,所以数据库 host 是 localhost

ts 复制代码
host: isProduction ? 'mysql-prod' : 'localhost',

这时 localhost:3306 正好是 MySQL 容器映射到宿主机的端口,所以连接可以成功。

6. 浏览器访问页面

启动成功后访问:

text 复制代码
http://localhost:3000
http://localhost:3000/books

/ 会返回 Hello World!/books 会打开书籍管理页面。页面里可以新增书籍、编辑书籍、删除书籍。

7. 用 curl 验证接口

新增一本书:

bash 复制代码
curl -X POST "http://localhost:3000/book" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Clean Code",
    "author": "Robert C. Martin",
    "description": "A handbook of agile software craftsmanship",
    "price": 99.9,
    "stock": 50,
    "publishedAt": "2008-08-01"
  }'

查询全部:

bash 复制代码
curl -X GET "http://localhost:3000/book"

查询单条:

bash 复制代码
curl -X GET "http://localhost:3000/book/1"

更新:

bash 复制代码
curl -X PATCH "http://localhost:3000/book/1" \
  -H "Content-Type: application/json" \
  -d '{
    "stock": 80,
    "price": 88.8
  }'

删除:

bash 复制代码
curl -X DELETE "http://localhost:3000/book/1"

这套 CRUD 验证了完整链路:浏览器或 curl 发请求,NestJS Controller 接收请求,Service 调用 TypeORM,TypeORM 写入 MySQL,MySQL 数据通过 volume 持久化到宿主机。

8. 停止本地开发环境

停止容器:

bash 复制代码
pnpm run docker:down

注意,docker compose down 会删除容器和默认网络,但不会删除你挂载到宿主机的 volumes/mysql 数据目录。所以下次 pnpm run docker:up 后,数据仍然存在。

如果你想彻底清空数据库数据,需要删除对应的宿主机目录。这个操作有破坏性,真实项目里要非常谨慎。

九、生产 Compose:把业务服务和数据库一起编排

本地开发模式下,NestJS 在宿主机运行。生产模式下,NestJS 应该作为容器运行。当前项目的 docker-compose.prod.yml 就是为这个目标准备的:

yaml 复制代码
services:
  mysql-prod:
    image: ${MYSQL_IMAGE:-mysql:latest}
    container_name: mysql-prod
    environment:
      MYSQL_ROOT_PASSWORD: admin
      MYSQL_DATABASE: book
    ports:
      - '3306:3306'
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci
    volumes:
      - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/mysql-prod:/var/lib/mysql
    restart: always

  nest-app:
    container_name: nest-app
    build:
      context: .
      dockerfile: Dockerfile
      args:
        NODE_IMAGE: ${NODE_IMAGE:-node:24-alpine}
    ports:
      - '3000:3000'
    environment:
      NODE_ENV: production
    depends_on:
      - mysql-prod
    restart: always

这份文件只包含两个服务:mysql-prodnest-app。没有包含 Milvus,因为当前生产业务链路只需要 NestJS + MySQL。如果后续业务真的用到向量检索,再把 Milvus 加进生产 Compose 会更合理。不要因为开发环境里有某个中间件,就默认生产环境也必须带上它。生产环境每多一个组件,就多一份资源消耗、监控成本和故障面。

mysql-prod 和开发环境的 mysql-dev 很像,但数据目录变成了:

yaml 复制代码
volumes:
  - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/mysql-prod:/var/lib/mysql

这可以避免开发数据和生产数据混在一起。即使是在同一台机器上做演示,也应该把开发和生产数据目录分开。

nest-app 使用 build,说明它不是直接拉现成镜像,而是在当前项目目录里根据 Dockerfile 构建:

yaml 复制代码
build:
  context: .
  dockerfile: Dockerfile
  args:
    NODE_IMAGE: ${NODE_IMAGE:-node:24-alpine}

context: . 表示构建上下文是当前项目根目录。dockerfile: Dockerfile 指定构建文件。args 把 Compose 里的 NODE_IMAGE 传给 Dockerfile 里的 ARG NODE_IMAGE

environment 设置了:

yaml 复制代码
environment:
  NODE_ENV: production

这会影响 AppModule 里的数据库 host 判断:

ts 复制代码
const isProduction = process.env.NODE_ENV === 'production';
host: isProduction ? 'mysql-prod' : 'localhost',

所以生产容器启动后,NestJS 会连接 mysql-prod:3306。这个地址只在 Compose 网络内部有意义。宿主机访问 MySQL 仍然是 localhost:3306,但容器访问另一个容器应该使用服务名。

depends_on 表示 nest-app 依赖 mysql-prod

yaml 复制代码
depends_on:
  - mysql-prod

这里要注意一个边界:depends_on 只能保证启动顺序,不能保证 MySQL 已经完全就绪。MySQL 容器进程启动了,不代表数据库已经可以接受连接。当前 demo 比较简单,通常重启后可以正常连接;真实生产建议给 MySQL 加 healthcheck,并让应用有连接重试能力,或者使用等待脚本。

十、生产环境从零部署教程

下面按新手可执行的方式,把生产 Compose 跑起来。

1. 确认不要和开发环境抢端口

开发 Compose 和生产 Compose 都把 MySQL 映射到宿主机 3306

yaml 复制代码
ports:
  - '3306:3306'

所以不能同时启动开发 MySQL 和生产 MySQL,除非你改其中一个的宿主机端口。最简单的做法是先停掉开发环境:

bash 复制代码
pnpm run docker:down

如果你不想停开发环境,可以把生产 MySQL 改成:

yaml 复制代码
ports:
  - '3307:3306'

这表示宿主机用 3307 访问生产 MySQL,但容器内部仍然是 3306。注意 NestJS 容器访问 mysql-prod:3306 不受这个映射影响,因为容器之间走内部网络,不走宿主机映射端口。

2. 准备 .env

项目里有 .env.example

env 复制代码
# Default Docker Hub images:
# NODE_IMAGE=node:24-alpine
# MYSQL_IMAGE=mysql:latest

# Mainland China / Docker Hub timeout workaround:
NODE_IMAGE=docker.m.daocloud.io/library/node:24-alpine
MYSQL_IMAGE=docker.m.daocloud.io/library/mysql:latest

如果你在国内网络环境拉 Docker Hub 容易失败,可以复制一份:

bash 复制代码
cp .env.example .env

Docker Compose 默认会读取当前目录的 .env。这样 docker-compose.prod.yml 里的 ${NODE_IMAGE:-node:24-alpine}${MYSQL_IMAGE:-mysql:latest} 就会使用 .env 中的镜像地址。

真实生产环境里,.env 不应该提交到 Git。当前 .dockerignore 已经忽略了 .env.env.*,这是正确的。

3. 构建并启动生产环境

项目 package.json 里提供了生产启动脚本:

json 复制代码
{
  "scripts": {
    "docker:prod:up": "docker compose -f docker-compose.prod.yml up -d --build"
  }
}

执行:

bash 复制代码
pnpm run docker:prod:up

这个命令会做几件事:

  1. 读取 docker-compose.prod.yml
  2. 根据 Dockerfile 构建 nest-app 镜像。
  3. 拉起 mysql-prod 容器。
  4. 拉起 nest-app 容器。
  5. 后台运行整套服务。

如果你想直接用 Docker 命令,也可以执行:

bash 复制代码
docker compose -f docker-compose.prod.yml up -d --build

4. 查看服务状态

bash 复制代码
docker compose -f docker-compose.prod.yml ps

你应该能看到 mysql-prodnest-app 两个容器。看日志:

bash 复制代码
docker compose -f docker-compose.prod.yml logs -f mysql-prod
docker compose -f docker-compose.prod.yml logs -f nest-app

如果 nest-app 日志里出现 MySQL 连接错误,优先检查:

  • NODE_ENV 是否是 production
  • mysql-prod 容器是否已经启动完成。
  • MYSQL_ROOT_PASSWORDMYSQL_DATABASE 是否和 TypeORM 配置一致。
  • 两个服务是否在同一个 Compose 项目网络里。

5. 访问生产服务

生产 Compose 把宿主机 3000 映射到 nest-app 容器的 3000。访问:

text 复制代码
http://localhost:3000
http://localhost:3000/books

如果是在云服务器上,需要把 localhost 换成服务器 IP 或域名:

text 复制代码
http://服务器IP:3000/books

同时确保云服务器安全组或防火墙放行了 3000 端口。真实上线时通常不会直接暴露 3000,而是用 Nginx 或网关把 80443 转发到内部 3000

6. 验证 CRUD

可以继续使用 curl:

bash 复制代码
curl -X POST "http://localhost:3000/book" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Docker Compose 实战",
    "author": "Local Dev",
    "description": "从本地开发到生产部署的一本示例书",
    "price": 66.6,
    "stock": 20,
    "publishedAt": "2026-05-23"
  }'

再查询:

bash 复制代码
curl "http://localhost:3000/book"

如果能查询到数据,说明生产链路已经跑通:宿主机请求进入 nest-app 容器,NestJS 通过 mysql-prod 服务名访问 MySQL,MySQL 写入挂载的数据卷。

7. 停止生产环境

bash 复制代码
docker compose -f docker-compose.prod.yml down

如果只是想重启:

bash 复制代码
docker compose -f docker-compose.prod.yml restart

如果改了代码并想重新构建:

bash 复制代码
docker compose -f docker-compose.prod.yml up -d --build

--build 很重要。没有它时,Compose 可能直接复用旧镜像,你会以为代码没生效。

十一、把项目部署到一台真实服务器

本地生产 Compose 跑通后,上服务器的思路其实一样,只是执行环境从你的电脑变成了云主机。

一个最小可行的部署流程是:

  1. 准备一台 Linux 服务器。
  2. 安装 Docker 和 Docker Compose 插件。
  3. 把项目代码上传到服务器。
  4. 在服务器上准备 .env
  5. 执行 docker compose -f docker-compose.prod.yml up -d --build
  6. 开放访问端口或配置反向代理。
  7. 配置日志、备份、重启策略和监控。

假设代码已经上传到服务器的 /opt/nest-dockerfile-test

bash 复制代码
cd /opt/nest-dockerfile-test
cp .env.example .env
docker compose -f docker-compose.prod.yml up -d --build

查看状态:

bash 复制代码
docker compose -f docker-compose.prod.yml ps
docker compose -f docker-compose.prod.yml logs -f nest-app

如果服务器使用云厂商安全组,要放行端口。学习阶段可以先放行 3000,真实生产更建议:

  • 只对外开放 80443
  • 用 Nginx 把外部请求转发到本机 3000
  • MySQL 的 3306 不对公网开放。

一个简化的 Nginx 反向代理示例:

nginx 复制代码
server {
  listen 80;
  server_name example.com;

  location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
}

这样用户访问 http://example.com/books,Nginx 会转发到宿主机的 3000,再进入 nest-app 容器。

这里有一个边界要说清楚:当前 Compose 把 MySQL 端口映射到了宿主机 3306,方便本机调试。但在真实公网服务器上,数据库端口不应该直接暴露给互联网。你可以删除生产 Compose 里 MySQL 的 ports,只保留容器内部访问:

yaml 复制代码
mysql-prod:
  image: ${MYSQL_IMAGE:-mysql:latest}
  environment:
    MYSQL_ROOT_PASSWORD: admin
    MYSQL_DATABASE: book

没有 ports 后,宿主机外部不能直接访问 MySQL,但 nest-app 仍然可以通过 Compose 内部网络访问 mysql-prod:3306。这是更安全的生产形态。

十二、开发环境和生产环境的核心差异

很多新手会问:既然 Docker Compose 可以启动所有服务,为什么本地开发不直接把 NestJS 也放进 Compose?

答案是可以,但不是必须。是否放进 Compose,取决于你更看重什么。

当前项目采用的是"开发只容器化基础设施,业务代码本地运行"的方式:

text 复制代码
本地开发:
浏览器 -> 宿主机 NestJS -> localhost:3306 -> mysql-dev 容器

优点是开发体验好。pnpm run start:dev 可以 watch 文件变化,IDE 调试也更直接。缺点是宿主机需要安装 Node 和 pnpm。

生产环境采用的是"业务服务也容器化"的方式:

text 复制代码
生产部署:
浏览器 -> 宿主机 3000 -> nest-app 容器 -> mysql-prod:3306 -> MySQL 容器

优点是部署环境更一致。服务器不需要手动安装项目依赖,只要有 Docker 就能构建和启动。缺点是调试不如本地直接运行方便,镜像构建也需要时间。

如果团队希望本地开发也完全容器化,可以再加一个 nest-dev 服务,把源码挂载进容器,并在容器里运行 pnpm run start:dev。但这会引入文件监听、依赖缓存、容器内外权限等新问题。对学习和中小项目来说,当前方式更容易理解,也更稳。

十三、当前实现里值得注意的工程边界

1. synchronize: true 适合学习,不适合严肃生产

当前 TypeORM 配置里有:

ts 复制代码
synchronize: true,

它会自动根据 Entity 同步表结构。学习阶段这很方便,因为你只要写 Entity,表就能自动创建。

但真实生产里,表结构变更需要可审计、可回滚、可灰度。推荐使用 TypeORM migration 或其他数据库迁移工具。生产环境可以改成:

ts 复制代码
synchronize: false,
migrationsRun: true,

然后用 migration 文件管理字段新增、索引变更和数据修复。

2. 密码不应该写死在源码和 Compose 里

当前 demo 里 MySQL root 密码是 admin

yaml 复制代码
MYSQL_ROOT_PASSWORD: admin

TypeORM 里也写了:

ts 复制代码
password: 'admin',

学习项目可以接受,但真实项目应该通过环境变量注入:

yaml 复制代码
environment:
  MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}

NestJS 里也应该读取:

ts 复制代码
password: process.env.DB_PASSWORD,

这样密码不会写死在代码仓库,也方便不同环境使用不同配置。

3. mysql:latest 不适合长期生产

当前配置使用:

yaml 复制代码
image: ${MYSQL_IMAGE:-mysql:latest}

latest 对学习很方便,但对生产有风险。因为它会随着镜像仓库更新而变化。今天拉到的可能是一个版本,几个月后重新部署拉到的是另一个版本。

生产更建议固定版本,例如:

yaml 复制代码
image: mysql:8.4

Node 镜像也一样。版本越明确,部署越可复现。

4. depends_on 不是健康检查

depends_on 只保证启动顺序,不保证依赖服务已经就绪。MySQL 启动通常需要几秒到几十秒。业务容器如果启动太快,可能第一次连接失败。

更稳的做法有两个:

  • 给 MySQL 配置 healthcheck。
  • 应用层数据库连接开启重试。

例如 MySQL 可以增加:

yaml 复制代码
healthcheck:
  test: ['CMD', 'mysqladmin', 'ping', '-h', 'localhost', '-padmin']
  interval: 10s
  timeout: 5s
  retries: 5

然后应用层也要允许短暂失败后重连。不要把服务启动顺序当成服务可用性。

5. 当前 Dockerfile 还能继续优化

当前单阶段 Dockerfile 易懂,但镜像会包含构建所需的 devDependencies。生产可以改成多阶段构建,例如:

dockerfile 复制代码
ARG NODE_IMAGE=node:24-alpine

FROM ${NODE_IMAGE} AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm config set registry https://registry.npmmirror.com/ \
  && npm install -g pnpm@10.14.0 \
  && pnpm install --frozen-lockfile
COPY . .
RUN pnpm run build

FROM ${NODE_IMAGE} AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY package.json pnpm-lock.yaml ./
RUN npm config set registry https://registry.npmmirror.com/ \
  && npm install -g pnpm@10.14.0 \
  && pnpm install --prod --frozen-lockfile
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/main.js"]

多阶段构建的价值是:构建阶段可以安装完整依赖,运行阶段只保留生产运行需要的东西。镜像更小,攻击面更小,部署传输也更快。

但优化要建立在理解之上。新手先跑通当前单阶段版本,再做多阶段改造,会更容易定位问题。

十四、常见问题排查

1. 端口 3306 被占用

现象:

text 复制代码
Bind for 0.0.0.0:3306 failed: port is already allocated

原因通常是本机已经安装了 MySQL,或者开发 Compose 的 mysql-dev 已经在运行,生产 Compose 又要启动 mysql-prod

处理方式:

bash 复制代码
docker ps
docker compose -f docker-compose.dev.yml down

或者把某个 Compose 的宿主机端口改成 3307:3306

2. NestJS 报连接 MySQL 失败

先判断当前运行模式。

如果你执行的是 pnpm run start:dev,NestJS 在宿主机运行,应该连接 localhost:3306。检查:

bash 复制代码
docker ps
docker compose -f docker-compose.dev.yml logs mysql

如果你执行的是生产 Compose,NestJS 在容器里运行,应该连接 mysql-prod:3306。检查:

bash 复制代码
docker compose -f docker-compose.prod.yml logs -f nest-app
docker compose -f docker-compose.prod.yml logs -f mysql-prod

重点看 NODE_ENV 是否为 production。如果生产容器里没有这个环境变量,代码会走开发分支,尝试连接容器自己的 localhost,然后失败。

3. /books 页面 404 或空白

优先检查 nest-cli.json

json 复制代码
"assets": [
  {
    "include": "../public/**/*",
    "outDir": "dist/public"
  }
]

然后重新构建:

bash 复制代码
pnpm run build

如果是容器里访问失败,重新构建镜像:

bash 复制代码
docker compose -f docker-compose.prod.yml up -d --build

因为静态文件需要进入 dist/public,再进入镜像。只改了 public/index.html 但没有重新 build,生产镜像不会自动变化。

4. Docker 拉镜像很慢或失败

可以使用 .env.example 中的镜像源配置:

bash 复制代码
cp .env.example .env
docker compose -f docker-compose.prod.yml up -d --build

注意当前开发 Compose 里 Milvus、etcd、MinIO 的镜像地址是直接写死的。如果这些镜像拉取慢,需要单独替换为可访问的镜像源,或者提前在网络较好的环境拉取。

5. 改了代码但容器里没生效

生产容器跑的是镜像里的编译产物,不会自动读取宿主机源码。改代码后需要:

bash 复制代码
docker compose -f docker-compose.prod.yml up -d --build

如果仍然没生效,可以强制重新构建:

bash 复制代码
docker compose -f docker-compose.prod.yml build --no-cache nest-app
docker compose -f docker-compose.prod.yml up -d

6. 数据删不掉或一直存在

这是 volume 持久化的正常表现。docker compose down 删除容器,但不会删除宿主机目录:

text 复制代码
volumes/mysql
volumes/mysql-prod

如果你要清空学习数据,需要停止容器后删除对应目录。真实项目不要随便做这个操作,因为这等价于删除数据库数据。

十五、命令速查

本地开发:

bash 复制代码
cd "/Users/zz/AI learning/codeing/tool-test/nest-dockerfile-test"
pnpm install
pnpm run docker:up
pnpm run start:dev

查看开发环境容器:

bash 复制代码
docker compose -f docker-compose.dev.yml ps
docker compose -f docker-compose.dev.yml logs -f mysql

停止开发环境:

bash 复制代码
pnpm run docker:down

生产构建与启动:

bash 复制代码
cp .env.example .env
pnpm run docker:prod:up

查看生产环境:

bash 复制代码
docker compose -f docker-compose.prod.yml ps
docker compose -f docker-compose.prod.yml logs -f nest-app
docker compose -f docker-compose.prod.yml logs -f mysql-prod

停止生产环境:

bash 复制代码
docker compose -f docker-compose.prod.yml down

重新构建生产镜像:

bash 复制代码
docker compose -f docker-compose.prod.yml up -d --build

测试接口:

bash 复制代码
curl "http://localhost:3000/book"

访问页面:

text 复制代码
http://localhost:3000/books

十六、如何从这个项目继续演进

当前项目是一个非常适合学习 Docker Compose 的起点,但还不是完整生产架构。后续可以按下面的顺序演进。

第一步,把配置环境变量化。数据库 host、端口、用户名、密码、数据库名都应该从环境变量读取,而不是写死在 AppModule。可以引入 @nestjs/config,并建立 .env.development.env.production 的配置规范。

第二步,关闭生产环境 synchronize,改用 migration。数据库结构是系统资产,不能让 ORM 在生产环境里自动猜测和修改。

第三步,优化 Dockerfile 为多阶段构建,并固定 Node、MySQL 镜像版本。这样可以减少镜像体积,提高可复现性。

第四步,给 MySQL 和 NestJS 增加 healthcheck。Compose 不等于编排平台,但基本健康检查仍然能提升部署可靠性。

第五步,引入 Nginx 和 HTTPS。真实用户不应该直接访问 3000,对外应该是域名和 HTTPS。

第六步,建立备份策略。MySQL 数据挂载在宿主机目录里,不代表它天然安全。生产需要定期备份、异地备份和恢复演练。

第七步,如果业务开始使用 Milvus,再把 Milvus 纳入生产 Compose,并明确它的数据目录、备份策略、资源限制和监控方式。不要只因为本地开发有 Milvus,就默认线上也必须启动。

十七、总结

Docker Compose 解决的不是"怎么运行一个命令"这么简单的问题,而是把一组服务的运行方式固化下来:镜像用什么版本,容器叫什么名字,端口怎么映射,数据放在哪里,服务之间怎么互相访问,重启策略是什么。

在当前 NestJS 项目里,本地开发环境用 Compose 启动 MySQL 和 Milvus,业务代码在宿主机用 pnpm run start:dev 热更新运行。这种方式兼顾了依赖环境的一致性和开发调试效率。

生产环境则用 docker-compose.prod.yml 同时编排 mysql-prodnest-app。NestJS 通过 NODE_ENV=production 切换数据库 host,从开发模式的 localhost 变成生产模式的 mysql-prod。这是容器化部署里最关键的认知:容器里的 localhost 不是宿主机,也不是另一个容器;容器之间应该通过 Compose 服务名通信。

如果你是新手,建议按本文顺序实践:先理解项目接口和数据库链路,再跑本地开发 Compose,最后跑生产 Compose。只要你能清楚解释"请求从浏览器到 NestJS,再到 TypeORM,再到 MySQL"的路径,也能清楚解释"开发模式和生产模式数据库 host 为什么不同",你就已经跨过了 Docker Compose 部署后端项目最重要的一道门槛。

相关推荐
yangshicong2 小时前
第11章:结构化输出与数据提取 —— 让 AI 直接返回你想要的数据格式
数据库·人工智能·redis·python·langchain·ai编程
码事漫谈2 小时前
SenseNova Skills Studio:为商汤SenseNova U1打造的本地办公技能包
后端
zhangxingchao2 小时前
AI应用开发七:可以替代 RAG 的技术
前端·人工智能·后端
excel3 小时前
🧠 Prisma 表名大写 vs SQL 导出小写问题深度解析(附踩坑与解决方案)
前端·后端
GetcharZp4 小时前
Hermes Agent:一个真正“会成长”的开源 AI Agent,正在改变 AI 自动化玩法
后端
Gopher_HBo4 小时前
Go依赖管理
后端
ltl4 小时前
Layer Normalization:为什么 Transformer 用 LN,不用 BN
后端
ltl5 小时前
title: 【Transformer 与注意力机制】24|
后端
范什么特西5 小时前
Spring 动态代理 静态代理
java·后端·spring