本文不是一篇只介紹 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 端口。
4. 數據卷:讓容器數據持久化
容器可以刪除重建。如果數據庫的數據只放在容器內部,那麼容器刪除後數據也會丟失。數據庫這種有狀態服務必須把數據目錄掛載到宿主機。
當前項目的 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
開發環境的重點是效率:數據庫和中間件用容器跑,業務代碼在宿主機用 watch 模式跑,這樣改代碼能立刻生效。
生產環境的重點是一致性:業務代碼也構建成鏡像,和 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 11、TypeScript 5.7,並且 package 裡已經有 @types/node 24.x,所以 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 中間有空格,如果不加引號,shell 會把它拆成兩個參數。
3. 安裝項目依賴
pnpm install
這一步安裝的是宿主機開發環境依賴。它和 Dockerfile 裡的 pnpm install --frozen-lockfile 不是同一次安裝。
- 本地
pnpm install是為了讓你能在宿主機運行pnpm run start:dev。 - Dockerfile 裡的安裝是為了構建生產鏡像,讓容器內部也有運行項目所需的依賴。
4. 啟動開發環境基礎服務
當前 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 默認暴露:
19530:Milvus SDK 訪問端口。9091:Milvus 健康檢查端口。9000:MinIO API。9001:MinIO 控制台。
5. 啟動 NestJS 開發服務
開發 Compose 不會啟動 NestJS,所以還需要在宿主機執行:
pnpm run start:dev
啟動後 NestJS 會讀取 AppModule 中的配置。因為此時沒有設置 NODE_ENV=production,所以數據庫 host 是 localhost:
host: isProduction ? 'mysql-prod' : 'localhost',
這時 localhost:3306 正好是 MySQL 容器映射到宿主機的端口,所以連接可以成功。
6. 瀏覽器訪問頁面
啟動成功後訪問:
http://localhost:3000
http://localhost:3000/books
/ 會返回 Hello World!,/books 會打開書籍管理頁面。頁面裡可以新增書籍、編輯書籍、刪除書籍。
7. 用 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。如果後續業務真的用到向量檢索,再把 Milvus 加進生產 Compose 會更合理。不要因為開發環境裡有某個中間件,就默認生產環境也必須帶上它。生產環境每多一個組件,就多一份資源消耗、監控成本和故障面。
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 項目網絡裡。
5. 訪問生產服務
生產 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?
答案是可以,但不是必須。是否放進 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,
這樣密碼不會寫死在代碼倉庫,也方便不同環境使用不同配置。
3. 固定 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
2. 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 但沒有重新 build,生產鏡像不會自動變化。
4. Docker 拉鏡像很慢或失敗
可以使用 .env.example 中的鏡像源配置:
cp .env.example .env
docker compose -f docker-compose.prod.yml up -d --build
注意當前開發 Compose 裡 Milvus、etcd、MinIO 的鏡像地址是直接寫死的。如果這些鏡像拉取慢,需要單獨替換為可訪問的鏡像源,或者提前在網絡較好的環境拉取。
5. 改了代碼但容器裡沒生效
生產容器跑的是鏡像裡的編譯產物,不會自動讀取宿主機源碼。改代碼後需要:
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
6. 數據刪不掉或一直存在
這是 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 的起點,但還不是完整生產架構。後續可以按下面的順序演進。
第一步,把配置環境變量化。數據庫 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-prod 和 nest-app。NestJS 通過 NODE_ENV=production 切換數據庫 host,從開發模式的 localhost 變成生產模式的 mysql-prod。這是容器化部署裡最關鍵的認知:容器裡的 localhost 不是宿主機,也不是另一個容器;容器之間應該通過 Compose 服務名通信。
如果你是新手,建議按本文順序實踐:先理解項目接口和數據庫鏈路,再跑本地開發 Compose,最後跑生產 Compose。只要你能清楚解釋“請求從瀏覽器到 NestJS,再到 TypeORM,再到 MySQL”的路徑,也能清楚解釋“開發模式和生產模式數據庫 host 為什麼不同”,你就已經跨過了 Docker Compose 部署後端項目最重要的一道門檻。












