此文非独述 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.yml、docker-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 之项目,最习以为常之启动法者:
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-dev、milvus-etcd、milvus-minio、milvus-standalone。 - 生產 Compose 則啟動矣
mysql-prod与nest-app。
3. 端口映射:容器之端口,曝于主机。
容器有自之网络名域。MySQL 在容器中监听 3306,非谓 Mac 或 Linux 之主机可直通之。欲使主机通容器之端口,须行端口映射:
ports:
- '3306:3306'
左为主机端口,右为容器端口。'3306:3306'之意,乃访主机之 3306,转至容器内之 3306。
NestJS 之服务亦然:
ports:
- '3000:3000'
访主机之 http://localhost:3000,实入 nest-app 容器之内。3000端口。
四、数据卷:使容器之数据得久存
容器可删可建。若数据库之数据尽存于容器之内,则容器既删,数据亦隳。数据库此等有状态之服务,必将数据之目录挂载于宿主机。
今项目之MySQL开发环境配置者:
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 访之:
host: isProduction ? 'mysql-prod' : 'localhost',
此行代码甚为枢要。盖因 NestJS 运行于容器之中时,localhost 指者 nest-app 容器自身,非 MySQL 容器,亦非宿主机。若续写localhost,业务容器自觅 MySQL,故自然连接不谐。
此异,乃众初以 Docker 部署后端服务时,最易罹之患也。
三、今项目之整体结构
先观项目结构中与部署相关之文:
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 LR
Browser["浏览器"]
BooksPage["/books 静态页面"]
Api["/book REST 接口"]
Controller["BookController"]
Service["BookService"]
TypeORM["TypeORM EntityManager"]
MySQL[("MySQL book 数据库")]
Browser --> BooksPage
BooksPage --> Api
Api --> Controller
Controller --> Service
Service --> TypeORM
TypeORM --> MySQL
开发与生产之部署方式,有所不同:
flowchart TB
subgraph Dev["本地开发模式"]
DevBrowser["浏览器"]
DevNest["NestJS 在宿主机运行 pnpm run start:dev"]
DevMysql[("mysql-dev 容器")]
DevMilvus["Milvus 相关容器"]
DevEtcd["etcd"]
DevMinio["MinIO"]
DevBrowser --> DevNest
DevNest --> DevMysql
DevMilvus --> DevEtcd
DevMilvus --> DevMinio
end
subgraph Prod["生产 Compose 模式"]
ProdBrowser["浏览器"]
HostPort["宿主机 3000 端口"]
NestContainer["nest-app 容器"]
MysqlProd[("mysql-prod 容器")]
ProdBrowser --> HostPort
HostPort --> NestContainer
NestContainer --> MysqlProd
end
开发之要,在速效:数据库与间件,皆以器运行;业务之码,于主机以观模式运之,如是则改码立效。
生产之要,在一致:业务之码亦构为像,与 MySQL 共由 Compose 管之,服务重启、网络、端口、所恃关系,皆书于配置之文。
四、今之 NestJS 服务所为之事
部署之教,不可独言 Docker。必知器中所运者何服务,否则事出,不知所检其层。
项目之入口,乃 src/main.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 乃理解開發與生產之異同之關鍵:
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:
@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:
@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 静态托举。核心调用之法如下:
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若此:
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,即示国内网络境下之镜像源例:
# Default Docker Hub images:
# NODE_IMAGE=node:24-alpine
# MYSQL_IMAGE=mysql:8.4
# 国内网络环境下的镜像源示例:
NODE_IMAGE=docker.m.daocloud.io/library/node:24-alpine
# 当前项目的 MySQL 使用华为云 SWR 镜像。
# 这个镜像当前按 linux/amd64 拉取,在 Apple Silicon 上会通过模拟方式运行。
MYSQL_IMAGE=swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/mysql:8.4
MYSQL_PLATFORM=linux/amd64
MYSQL_HOST_PORT=3307
。此非业务之理,乃工程部署之容错设也。多机直引Docker Hub辄超时,许以环境变量易镜像源,可降部署之阶。今配置中,MySQL已迁华为云SWR镜像,复将宿主机端口默认易为3307,以避本机或开发境已占之3306。
FROM ${NODE_IMAGE} 者,以 Node 镜像为基,构业务之镜像也。此中 Node 之版本,须与项目所恃相协。今项目用 NestJS 之十一、TypeScript 之五点七,且 package 内已备 @types/node 之二十四,故 node:24-alpine 乃相契。
WORKDIR /app 定容器内作业之域。后之 COPY、RUN、CMD 皆以 /app 为常所。如此则容器内之文脉井然,亦免令行于根域。
COPY package.json pnpm-lock.yaml ./ 但先抄依赖之单,不直抄源码之全。此 Docker 缓存优化之常法也。若依赖之文不改,则后之安装依赖层可复用缓存;汝但改业务之码,非必每度重装依赖。
pnpm install --frozen-lockfile 谓严循 pnpm-lock.yaml 而安其依。倘 lock 文与 package.json 不合,则安装弗成。此于建像为善,盖可避“吾地能安、伺器异之”之患。
COPY . . 复项目之码于像。此须配 .dockerignore ,否则 node_modules 、.git 、volumes 此等弗当入像之序,皆将混入,致建像迟缓且庞。
RUN pnpm run build 行 Nest 之编。今项目之 build 命为:
{
"scripts": {
"build": "nest build"
}
}
。编毕,则生 dist 之序。盖 nest-cli.json 配 assets 故。public靜態文件亦將被拷貝至dist/public:
{
"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 为:
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 之配置如下:
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之开发,由三务成:etcd、minio。standalone。其关系可喻之:
flowchart LR
Client["本地 AI 应用或脚本"]
Milvus["Milvus standalone"]
Etcd["etcd 元数据"]
Minio["MinIO 对象存储"]
Client --> Milvus
Milvus --> Etcd
Milvus --> Minio
Milvus 自身司向量化检索。etcd 用以存元数据、协信息,MinIO 用以存对象数据。尔今之 NestJS 书籍 CRUD 尚未直唤 Milvus,然开发之境已将 Milvus 编入,于后拓展 RAG 或语义检索,实为有益。
standalone 诸服务中,有两段配置甚为枢要:
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,但书服务名。
文末定义了默认网络:
networks:
default:
name: common-network
此将创制或复用一网络,名曰common-network。统一网络名之利,在后续其他Compose项目若接入此网络,则能与此处中间件互通。然亦须留意,网络名既定,不同项目间或生命名与服务访问之耦合,团队中须明定章程。
八、本地开发从零启程之教
下为初学者可依循之本地开发流程。
1. 准备基础环境
汝需先装:
- Docker Desktop或Docker Engine。
- Node.js,建议用与项目相合之较新版本。
- pnpm。
检视Docker是否可用:
docker version
docker compose version
验 Node 与 pnpm:
node -v
pnpm -v
若 docker compose version 可示版本,则知君用新式 Compose 插件,令文为 docker compose。旧篇之 docker-compose 乃旧令,非所必用。
2. 入项目之次第
cd "/Users/zz/AI learning/codeing/tool-test/nest-dockerfile-test"
若路径有隙,则令文须加引。初学者常于此失:AI learning 中有隙,若不加引,壳将之析为二参。
3. 装项目之依
pnpm install
此步所装,乃宿主之依。此依非 Dockerfile 中之 pnpm install --frozen-lockfile 同次所装。
- 本地
pnpm install乃为汝于宿主机运行也pnpm run start:dev。 - Dockerfile 之安装,为构生产镜像而设,俾容器内亦具运行项目所需之依。
四、启开发之基务
今朝package.json已备脚本矣。
{
"scripts": {
"docker:up": "docker compose -f docker-compose.dev.yml up -d",
"docker:down": "docker compose -f docker-compose.dev.yml down"
}
}
启程:
pnpm run docker:up
此令于暗处启 MySQL、etcd、MinIO、Milvus。初行则引镜,费时或久,尤 Milvus 之镜,其大甚也。
察容器之状
docker compose -f docker-compose.dev.yml ps
欲观日志者:
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 之常设,显:
19530Milvus SDK 访问之端口。9091Milvus 健康检查之端口。9000MinIO API。9001MinIO 控制台。
五、启 NestJS 之务
开发 Compose 不得启动 NestJS,故尚需于宿主机行之:
pnpm run start:dev
启动之后,NestJS 将读取AppModule此配置之中。盖因此时未设也。NODE_ENV=production者,数据库之主也,乃localhost:
host: isProduction ? 'mysql-prod' : 'localhost',
。是时localhost:3306,恰为MySQL之器映于主之端口,故连接得通。
。 六、浏览器访页
,启动既成,访
http://localhost:3000
http://localhost:3000/books
/,则返Hello World!,/books启书籍管理之页。其间可增书、辑书、除书。
。 七、以curl验其接口
,增一书:
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"
}'
。查询全书:
curl -X GET "http://localhost:3000/book"
。查单条:
curl -X GET "http://localhost:3000/book/1"
新訂:
curl -X PATCH "http://localhost:3000/book/1" \
-H "Content-Type: application/json" \
-d '{
"stock": 80,
"price": 88.8
}'
刪除:
curl -X DELETE "http://localhost:3000/book/1"
此套 CRUD 驗證全鏈:瀏覽器或 curl 發請求,NestJS Controller 接受請求,Service 呼叫 TypeORM,TypeORM 儲存至 MySQL,MySQL 資料藉 volume 持續於宿主機。
8. 停止本地開發環境
停止容器:
pnpm run docker:down
謹記,docker compose down將刪除容器與默認網絡,然不刪除掛載於宿主機的 volumes/mysql 資料目錄。故下次 pnpm run docker:up 後,資料猶存。
若欲徹底清空資料庫資料,須刪除相應宿主機目錄。此舉破壞性強,實際項目中需慎之又慎。
九、生产之Compose:将业务与数据库共织之序
本地调试之际,NestJS 运行于本机。生产之际,NestJS 当以容器运行。今项目之docker-compose.prod.yml此即为此目而备也。
services:
mysql-prod:
image: ${MYSQL_IMAGE:-mysql:8.4}
platform: ${MYSQL_PLATFORM:-linux/amd64}
container_name: mysql-prod
environment:
MYSQL_ROOT_PASSWORD: admin
MYSQL_DATABASE: book
ports:
- '${MYSQL_HOST_PORT:-3307}: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-prod与nest-app不载 Milvus,盖今之产务链路,唯需 NestJS 与 MySQL耳。倘后之业务果需向量检索,则更宜于产用 Compose 中添入 Milvus,方为得宜。勿以开发之境有某间件,便谓产境必携之。产境每增一组件,即增一资源之耗、监控之费、故障之面。
mysql-prod与开發環境者mysql-dev颇类,然数据之录已易为:
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/mysql-prod:/var/lib/mysql
此可避开发与生产之数据相淆。纵使于同一机演示,亦当别立开发、生产数据之目录。
此处尚有二事与镜像源相关。image: ${MYSQL_IMAGE:-mysql:8.4}表默认用 Docker Hub 之mysql:8.4,然若.env中设MYSQL_IMAGE,则改用.env中之镜像。今项目内MYSQL_IMAGE指华为云 SWR:
MYSQL_IMAGE=swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/mysql:8.4
MYSQL_PLATFORM=linux/amd64
MYSQL_HOST_PORT=3307
platform: ${MYSQL_PLATFORM:-linux/amd64}乃为配此华为云 MySQL 镜像。其明示 Docker 拉取并运行 amd64 架构之镜像。若于 Apple Silicon 机器上运行,Docker Desktop 将以模拟方式运此容器。MYSQL_HOST_PORT=3307 则示宿主机访 MySQL 时用 localhost:3307,然容器内 MySQL 仍聆 3306。
nest-app 用 build,明其非直引成象,乃依 Dockerfile 于今项目目录构之:
build:
context: .
dockerfile: Dockerfile
args:
NODE_IMAGE: ${NODE_IMAGE:-node:24-alpine}
context: . 示建上下乃今项目根。dockerfile: Dockerfile 指建文。args 将 Compose 中 NODE_IMAGE 传 Dockerfile 中 ARG NODE_IMAGE。
environment 设:
environment:
NODE_ENV: production
此将影响。AppModule 之中,数据库之 host 判之:
const isProduction = process.env.NODE_ENV === 'production';
host: isProduction ? 'mysql-prod' : 'localhost',
。故生产容器既启,NestJS 便接 mysql-prod:3306。此址惟于 Compose 之网内方有义。今生产之 Compose,常将 MySQL 映于宿主机 3307,是故宿主机欲访 MySQL,则曰 localhost:3307;然容器欲访他容器,仍当用服名及容器内端口,即 mysql-prod:3306。
depends_on 者,示 nest-app 之依 mysql-prod 也:
depends_on:
- mysql-prod
。于此,须谨记一界:depends_on 仅可保启动之序,不能保 MySQL 已完全备妥。MySQL 容器既启,非即数据库可受连接。今之 demo 较简,重启后通常可通连接;真实生产,当为 MySQL 加 healthcheck,令应用有重试连接之能,或用待机脚本。
十、生产环境从零部署教程
今依初学者可行之式,使生产 Compose 运行。
1. 确勿与开发环境争端口
开发 Compose 映射 MySQL 至宿主机:3306:
ports:
- '3306:3306'
今生产 Compose 已改宿主机端口为默认:3307:
ports:
- '${MYSQL_HOST_PORT:-3307}:3306'
开发环境既如是mysql-dev 可据 3306,生产演繹之境 mysql-prod 可据 3307。若君不欲並運兩套 MySQL,至簡之法,仍先止發展之境:
pnpm run docker:down
。若君之機上 3307 亦被據,可於 .env 中續改:
MYSQL_HOST_PORT=3308
。此謂宿主以 3308 訪問生產 MySQL,然容器內部猶是 3306。慎諦 NestJS 容器訪問 mysql-prod:3306 不受此映,蓋容器間行內部之網,不經宿主映端口也。
2. 準備 .env
之项目,有.env.example:
# Default Docker Hub images:
# NODE_IMAGE=node:24-alpine
# MYSQL_IMAGE=mysql:8.4
# 国内网络环境下的镜像源示例:
NODE_IMAGE=docker.m.daocloud.io/library/node:24-alpine
# 当前项目的 MySQL 使用华为云 SWR 镜像。
MYSQL_IMAGE=swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/mysql:8.4
MYSQL_PLATFORM=linux/amd64
MYSQL_HOST_PORT=3307
若于国内网络境,引Docker Hub易败,可摹其本:
cp .env.example .env
Docker Compose常取今目录之.env。是故docker-compose.prod.yml中,${NODE_IMAGE:-node:24-alpine}、${MYSQL_IMAGE:-mysql:8.4}、${MYSQL_PLATFORM:-linux/amd64}、${MYSQL_HOST_PORT:-3307}皆用.env之设。即华为云SWR镜像之用、MySQL宿主机端口之择,皆可弗改Compose之文,直由.env掌之。
于真实生产境,.env 不可提交于 Git。今 .dockerignore 已忽略 .env 与 .env.*,此乃正道。
3. 构建并启动生产环境
项目 package.json 中备有生产启动之脚本:
{
"scripts": {
"docker:prod:up": "docker compose -f docker-compose.prod.yml up -d --build"
}
}
执行:
pnpm run docker:prod:up
此令将行数事:
- 读取
docker-compose.prod.yml。 - 依
Dockerfile而建nest-app之像。 - 拉起
mysql-prod为器。 - 而举
nest-app之器。 - 背景运之,使诸务备。
若欲直用 Docker 之令,亦可行之:
docker compose -f docker-compose.prod.yml up -d --build
4. 观其务之状
docker compose -f docker-compose.prod.yml ps
当见 mysql-prod 与 nest-app 二器。察其牍:
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_PASSWORD、MYSQL_DATABASE是否与 TypeORM 之配置相合。- 二者是否同处一 Compose 之项目网络。
五、访生产之服务
生产之 Compose 将宿主机 3000 映射至 nest-app 容器之 3000。访问:
http://localhost:3000
http://localhost:3000/books
若在云服务器上,须将 localhost 易为服务器 IP 或域名:
http://服务器IP:3000/books
同时务须确保云服务器之安全组或防火墙放行 3000 端口。真实上线时,通常不直露之。3000,乃以 Nginx 或网关,将 80、443 转发于内 3000。
6. 验证 CRUD
,可继以 curl:
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"
}'
再询:
curl "http://localhost:3000/book"
若得数据,则知生产链路已通:宿主机求入 nest-app 容器,NestJS 以 mysql-prod 服名访 MySQL,MySQL 书于挂载数据卷。
7. 止生产环境
docker compose -f docker-compose.prod.yml down
若惟欲重启:
docker compose -f docker-compose.prod.yml restart
若改代码欲重构:
docker compose -f docker-compose.prod.yml up -d --build
--build至要。无之,则Compose或直复旧像,汝以为码未效。
十一、将项目布于真机
本地生产Compose既通,上服务器之思无异,惟执行之境自汝之电脑易为云主。
一可行之部署序为:
- 备一Linux之机。
- 装Docker及Docker Compose插件。
- 将项目码上传于机。
- 于机上备
.env。 - 行
docker compose -f docker-compose.prod.yml up -d --build。 - 开端口或设逆代理。
- 配置記錄、備份、重啟之略策與監視。
设代码已上传于服务器之侧。/opt/nest-dockerfile-test:
cd /opt/nest-dockerfile-test
cp .env.example .env
docker compose -f docker-compose.prod.yml up -d --build
察其状也。
docker compose -f docker-compose.prod.yml ps
docker compose -f docker-compose.prod.yml logs -f nest-app
若服务器用云商之安全组,须放行端口。学阶段可暂放之。3000,真实生产更宜:
- 唯对外开
80与443。 - 以 Nginx 透传外求至本机
3000。 - MySQL(米耶塞尔)者,
3306不向公网开放。
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之端口默认映于主之 3307,以便于本机之调试。然于真实之公网服务器,数据库之端口不可直示于互联网。汝可删生产compose中mysql之 ports,独存器内之访问:
mysql-prod:
image: ${MYSQL_IMAGE:-mysql:8.4}
platform: ${MYSQL_PLATFORM:-linux/amd64}
environment:
MYSQL_ROOT_PASSWORD: admin
MYSQL_DATABASE: book
既无 ports,则主之外部不可直通mysql,然nest-app 仍可通 Compose 内部之网,访 mysql-prod:3306。此乃更安之生产形态也。
十二、开发与生产之核心异同
多初学者问:既 Docker Compose 可启诸务,何不将 NestJS 亦纳于 Compose 以供本地开发?
答曰:可也,非必也。纳与否,视乎所重何在。
今项目采“开发仅容器化基设,业务代码本地运行”之式:
本地开发:
浏览器 -> 宿主机 NestJS -> localhost:3306 -> mysql-dev 容器
其利在开发体验佳。pnpm run start:dev 可 watch 文件之变,IDE 调试亦更直。其弊在宿主机需装 Node 与 pnpm。
生产环境则采“业务亦容器化”之式:
生产部署:
浏览器 -> 宿主机 3000 -> nest-app 容器 -> mysql-prod:3306 -> MySQL 容器
之长,在部署之境,更为一律。服务器无须手装项目所恃,但具 Docker,即可构作而启之。其短,在调试不若本地直运之便,镜像构作亦需时日。
若团队欲本地开发亦尽容器化,可更添一nest-dev之务,将源码悬于容器,于容器内运pnpm run start:dev。然此将引新患,如文件监听、所恃缓存、器内器外之权等。于学人及中小之务,今法更易解,亦更稳。
十三、今实中,堪注意之工界
1. synchronize: true 适于学,不适于严正之产
今 TypeORM 之配置有:
synchronize: true,
其能自依 Entity 同步表之构。学之阶甚便,惟书 Entity,表即自创。
然实之产,表构之变,须可审、可回、可灰。当用 TypeORM migration 或他数据库迁移之器。产境可易为:
synchronize: false,
migrationsRun: true,
乃以 migration 文件理字增、索变、数修。
2. 密码不宜死于源码与 Compose。
今之 demo 中,MySQL root 密码乃:admin:
MYSQL_ROOT_PASSWORD: admin
TypeORM 中亦书:
password: 'admin',
学之项目可容,然实之项目当依环境变量注:
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
NestJS 中亦当读:
password: process.env.DB_PASSWORD,
密码不囿于代码仓,亦便诸境用异配置。
三、固 MySQL 版本,较之用 latest,尤宜于生产。
今之配置已避直用 mysql:latest,默认固于 MySQL 8.4:
image: ${MYSQL_IMAGE:-mysql:8.4}
同时,.env 中可易华为云 SWR 镜像:
MYSQL_IMAGE=swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/mysql:8.4
此法较 mysql:latest 更稳。latest 随镜像仓之变而迁,今所引或一版,数月后重布所引或异版。固于 8.4 后,至少主版与小版明晰;更可通.env 更换华为云镜像源,可兼顾可复现与国内网络下载之成功率。
产制之际,更宜固定版本,如:
image: mysql:8.4
Node 镜像亦然。版本愈明,部署愈可复现。
4. depends_on 非健康检查,
depends_on 仅保启动次序,未保依赖之服务已就绪。MySQL 启动,常需数秒至数十秒。业务容器若启动过速,或致初连失败。
稳妥之法有二:
- 为 MySQL 配置 healthcheck。
- 在应用层数据库连接处开启重试。
例如 MySQL 可增:
healthcheck:
test: ['CMD', 'mysqladmin', 'ping', '-h', 'localhost', '-padmin']
interval: 10s
timeout: 5s
retries: 5
而后应用之层亦当容短暂败而重连。勿以服务启动之序为服务可用之验。
5. 当下 Dockerfile 尚可继以优化
当下单阶段 Dockerfile 易晓,然镜像将含构建所需之 devDependencies。生产可改为多阶段构建,例如:
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 被占用
现象:
Bind for 0.0.0.0:3306 failed: port is already allocated
缘常因本机已安装 MySQL,或开发 Compose 之故。mysql-dev既已运行,复启Compose之制。mysql-prod。
處理之法:
docker ps
docker compose -f docker-compose.dev.yml down
今之生产Compose已过MYSQL_HOST_PORT=3307避开了開發 MySQL 之途3306若尔之器上3307亦被占,则续改之.env:
MYSQL_HOST_PORT=3308
二、NestJS 之连 MySQL 失败
先辨当前运行之模。
若尔所行者pnpm run start:dev,NestJS 运行于宿主,当连接 localhost:3306。察之:
docker ps
docker compose -f docker-compose.dev.yml logs mysql
。若执行生产 Compose,NestJS 运行于容器,当连接 mysql-prod:3306。察之:
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:
"assets": [
{
"include": "../public/**/*",
"outDir": "dist/public"
}
]
。继而重构之:
pnpm run build
若容器访问不力,则重构其像。
docker compose -f docker-compose.prod.yml up -d --build
因靜態文件須入之dist/public复入镜像。惟更之。public/index.html然未复建,则产像自变不矣。
四、Docker 拉取镜像迟缓或失敗
可使用也.env.example镜像源配置于中
cp .env.example .env
docker compose -f docker-compose.prod.yml up -d --build
当前开发之Compose中,Milvus、etcd、MinIO之镜像地址,乃直书硬编码者也。若此等镜像拉取迟缓,须另行置换为可通之镜像源,或预先于网速优渥之境取之。
五、更易其码,然器中未显其效
生產容器行者,乃鏡像中所編之產,非自動讀宿主之源。改其碼後,需:
docker compose -f docker-compose.prod.yml up -d --build
若犹无效,可强令重筑之。
docker compose -f docker-compose.prod.yml build --no-cache nest-app
docker compose -f docker-compose.prod.yml up -d
六、数据不可删或恒存
此乃 volume 持久之常理也。docker compose down去容器,然不除宿主之目录。
volumes/mysql
volumes/mysql-prod
若欲涤除习得之数据,须先辍容器,继而刈其应配之录。实境之中,勿轻为之,盖此等行径,犹若刈断库之实据也。
十五、命速查
本地開發:
cd "/Users/zz/AI learning/codeing/tool-test/nest-dockerfile-test"
pnpm install
pnpm run docker:up
pnpm run start:dev
察开发之境器。
docker compose -f docker-compose.dev.yml ps
docker compose -f docker-compose.dev.yml logs -f mysql
辍其开物之境
pnpm run docker:down
制器启事
cp .env.example .env
pnpm run docker:prod:up
察生产之境。
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
停造生境
docker compose -f docker-compose.prod.yml down
重塑生之像:
docker compose -f docker-compose.prod.yml up -d --build
试其端:
curl "http://localhost:3000/book"
游其页:
http://localhost:3000/books
十六、何以继此项目而进
今之项目,实为研习 Docker Compose 之佳始,然非成之产之构。后可依序而进。
其一,使配置环境化。库之主、之口、之主名、之密、库名,皆当自环境变量取之,非固于 AppModule。可引 @nestjs/config,立 .env.development、.env.production 之制。
其二,绝产之境synchronize,当以迁移代之。数据库之构,乃系系统之资,不可使 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-prod 与 nest-app。NestJS 乃以 NODE_ENV=production 易其数据库之主,自开发之 localhost 转为生产之 mysql-prod。此乃容器化部署之要义:容器内之 localhost 非宿主机,亦非他容器;容器间当以 Compose 之服务名相通信。
若为初学者,当依本文之序而践之:先明项目之接口与数据库之脉络,次运本地开发之 Compose,终行生产之 Compose。但能明晰“请求自浏览器至 NestJS,复至 TypeORM,终抵 MySQL”之径,亦能阐明“开发模式与生产模式数据库主为何异”,则已越 Docker Compose 部署后端项目之至要门槛矣。












