
认识 Docker
Docker 解决什么问题
没有 Docker 时经常遇到的问题
假设一个项目需要:
- Node.js 24;
- npm 安装依赖;
- React 前端;
- NestJS 后端;
- PostgreSQL 数据库;
- Nginx 提供静态文件和反向代理。
新同事拿到代码后,必须分别安装 Node.js、PostgreSQL、Nginx,并保证版本和配置正确。很容易出现:
- "我电脑可以运行,你的电脑不行";
- 本地 Node.js 版本不同,依赖安装结果不同;
- 本地 PostgreSQL 端口、用户名或密码不同;
- 测试环境和生产环境的配置不一致;
- 部署服务器漏装软件或漏执行某一步;
- 同一台服务器上的两个项目依赖版本冲突。
这些问题的共同点是:代码虽然一样,运行代码的环境却不一样。
Docker 如何解决这些问题
Docker 的思路很直接:把应用和它需要的运行环境一起整理好,再放到不同电脑或服务器上运行。
刚开始只需要记住三个词:
- Dockerfile:记录应用需要什么环境、怎样安装依赖、怎样启动;
- 镜像(Image):按照 Dockerfile 构建出来的完整运行模板;
- 容器(Container):镜像真正启动后,正在运行的应用。
整个过程如下:

对应到命令只有两步:
bash
# 第一步:读取 Dockerfile,把项目构建成镜像
docker build -t my-app .
# 第二步:使用镜像创建并启动容器
docker run my-app
可以把它理解成:
text
准备一份可以重复执行的环境说明
↓
得到一份可以重复使用的应用镜像
↓
在开发电脑、测试服务器或生产服务器上启动容器
这样,不同环境使用同一个镜像时,应用所需的 Node.js、依赖和启动方式也会保持一致,从而减少"我的电脑可以运行,换一台电脑就不行"的问题。
在本文的前后端项目中,React、NestJS 和 PostgreSQL 会分别运行在自己的容器中。后面会逐步解释怎样构建这些镜像,以及怎样让这些容器一起工作。
Docker 与虚拟机
虚拟机
虚拟机通常包含一套完整的客户操作系统:
text
物理机
└── 宿主操作系统
└── 虚拟机软件
├── 虚拟机 A:完整操作系统 + 应用
└── 虚拟机 B:完整操作系统 + 应用
优点是隔离较完整;代价是体积大、启动慢、资源占用高。
容器
容器主要隔离应用进程、文件系统、网络和资源,但共享宿主机内核:
text
物理机
└── 宿主操作系统内核
└── Docker Engine
├── 容器 A:应用及其用户空间依赖
└── 容器 B:应用及其用户空间依赖
因此容器通常:
- 比虚拟机更小;
- 启动更快;
- 更适合交付单个应用或服务。
Docker 的主要组件
Docker 工作时主要涉及三个组件:
- Docker Client:接收我们输入的命令;
- Docker daemon:在后台执行具体操作;
- Registry:在远程保存和分发镜像。
它们之间的关系如下:

Docker Client:接收操作
Docker Client 是我们操作 Docker 的入口。平时在终端里执行的 docker 命令,都属于 Client:
bash
# 查看正在运行的容器
docker ps
# 根据 Dockerfile 构建镜像
docker build -t my-app .
# 使用 nginx 镜像创建并启动容器
docker run nginx
Client 接收到命令后,会把请求发送给 Docker daemon。镜像构建和容器启动等工作不是 Client 自己完成的。
Docker Desktop 在这里提供了两种操作入口:
- Docker CLI:在终端中输入
docker ...命令; - Docker Desktop 图形界面:通过按钮查看和操作镜像、容器、数据卷。
这两个入口最终都会让后台的 Docker daemon 执行具体操作。
Docker daemon:在后台执行操作
Docker daemon 是持续运行的后台服务,也可以把它理解成 Docker 真正干活的部分。
它负责:
- 拉取和保存镜像;
- 根据 Dockerfile 构建镜像;
- 创建、启动、停止和删除容器;
- 管理容器网络与数据卷。
在电脑上打开 Docker Desktop 时,Docker Desktop 会同时启动并管理 Docker Engine,Docker daemon 就在后台工作。
因此,在电脑终端中虽然可以找到 docker 命令,但 Docker Desktop 没有启动时,Client 无法连接后台 daemon,命令便可能出现:
text
Cannot connect to the Docker daemon
Registry:远程镜像仓库
Registry 是保存镜像的远程仓库。这里记住一个公式即可:
bash
# 命名空间:用户名 | 团队名
仓库镜像名 = [仓库地址/]命名空间/镜像名:版本
| 上传位置 | 仓库镜像名示例 |
|---|---|
| Docker Hub | copyer/my-app:1.0 |
| 公司 Registry | registry.example.com/copyer/my-app:1.0 |
对应的指令也是同一套:
bash
# 登录 Docker Hub;Docker Hub 是默认仓库,所以不需要写仓库地址
docker login
# 给本地 my-app:1.0 镜像添加 Docker Hub 仓库名称
docker tag my-app:1.0 copyer/my-app:1.0
# 上传到 Docker Hub 的 copyer/my-app 仓库
docker push copyer/my-app:1.0
# 从 Docker Hub 的 copyer/my-app 仓库下载
docker pull copyer/my-app:1.0
仓库地址省略时,Docker 默认使用 Docker Hub;写上公司 Registry 地址时,就使用公司仓库。
镜像的完整发布过程如下:

一条 Docker 命令的执行过程
执行:
bash
docker run nginx
背后会依次发生:
- Docker Client 接收
docker run nginx; - Client 把请求发送给 Docker daemon;
- daemon 检查本地是否已经存在
nginx镜像; - 如果本地没有,daemon 从 Registry 下载镜像;
- daemon 根据镜像创建并启动容器;
- Client 把执行结果显示在终端中。
安装 Docker
安装
| 使用场景 | 推荐安装 |
|---|---|
| macOS / Windows | Docker Desktop |
| Linux 服务器 | Docker Engine + Docker Compose plugin |
对于刚开始学习 Docker 的前端开发者,直接安装 Docker Desktop 最方便。它已经包含 Docker Engine、Docker 命令、Docker Compose 和图形管理界面。
Linux 服务器通常没有桌面环境,因此安装 Docker Engine 和 Docker Compose plugin 即可。
具体安装步骤根据自己的操作系统自行搜索,或者直接查看 Docker 官方安装文档。安装完成并启动 Docker 后,执行下面的命令验证:
bash
# 查看 Docker 是否可以正常连接
docker version
# 查看 Docker Compose 是否已经安装
docker compose version
# 运行官方测试容器;--rm 表示运行结束后自动删除容器
docker run --rm hello-world
如果 hello-world 能正常输出成功信息,说明 Docker 已经可以使用。
Docker 核心概念
Image:镜像
镜像是只读模板,包含应用运行需要的文件系统内容和元数据。
例如 node:24-alpine 大致表示:
node:镜像仓库名;24-alpine:标签,表示 Node.js 24 的 Alpine Linux 变体。
bash
# 下载镜像,但不创建容器
docker pull node:24-alpine
# 查看本地镜像
docker image ls
同一个镜像可以在开发机、CI 和服务器上重复创建一致的容器。Docker 会根据镜像中保存的文件、依赖和启动配置创建容器。
注意: 标签不是绝对不可变标识,生产环境最好使用明确版本,关键环境还可以固定镜像 digest。不要只依赖 latest。
Container:容器
容器是镜像运行后的实例,本质上仍是宿主机上的隔离进程。
bash
# 创建并后台运行一个 Nginx 容器
docker run --name web -d -p 8080:80 nginx:1.28-alpine
参数逐项解释:
--name web:把容器命名为web,方便后续按名称操作;不写时 Docker 随机生成名称。-d:detached,后台运行;不写时终端会附着到容器主进程。-p 8080:80:宿主机8080端口映射到容器80端口。nginx:1.28-alpine:用于创建容器的镜像。
一个镜像可以创建多个互不相同的容器,就像同一个 JavaScript class 可以创建多个实例。
镜像本身只是模板,不会主动运行。使用镜像创建容器后,应用进程才会真正启动。
容器可写层中的数据通常会随容器删除,因此数据库数据不能只保存在容器可写层中,后面会使用 volume 单独保存。
Dockerfile:镜像制作说明书
Dockerfile 是文本文件,描述如何从基础镜像构建自己的镜像。
dockerfile
# 使用 Node.js 24 Alpine 镜像作为基础
FROM node:24-alpine
# 后续命令默认在 /app 中执行;目录不存在时会创建
WORKDIR /app
# 把宿主机构建上下文中的 package.json 复制进镜像
COPY package.json ./
# 在构建镜像时执行依赖安装
RUN npm install
# 3000 是容器内部端口,表示应用运行后会监听这个端口
# EXPOSE 只起说明作用,不会自动把容器端口开放给本机
# 镜像构建完成后,可在终端执行下面的命令完成端口映射:
# docker run -p 8080:3000 my-app
# 含义:本机 8080 端口 → 容器 3000 端口
EXPOSE 3000
# 容器启动时执行的默认命令
CMD ["npm", "run", "start"]
Dockerfile 可以理解成一份"创建镜像的步骤清单"。Docker 会按照文件中的顺序准备 Node.js、复制项目、安装依赖并设置启动命令。把这份文件交给其他开发者或服务器,Docker 仍然会执行相同的步骤,不需要每个人重新手动配置环境。
Layer:镜像层与构建缓存
Dockerfile 中的 FROM、RUN、COPY 等通常形成镜像层。构建时,如果某一步及其输入没有变化,Docker 可以复用缓存。
因此 Node.js 项目通常这样写:
dockerfile
# 先复制依赖清单,使业务代码变化不会影响依赖安装层
COPY package.json package-lock.json ./
# 缓存 npm 的下载目录;必须重新安装依赖时可以减少重复下载
RUN --mount=type=cache,target=/root/.npm npm ci
# 最后复制经常变化的业务代码
COPY . .
这里同时使用了两种缓存策略:
| 缓存策略 | 什么时候发挥作用 |
|---|---|
| Docker 镜像层缓存 | package.json 和 package-lock.json 没变化时,直接复用 RUN npm ci 的结果,完全不重新安装依赖。两次 COPY 就是为了保护这一层缓存。 |
| BuildKit 的 npm 下载缓存 | 依赖清单变化后,npm ci 必须重新执行,但 /root/.npm 中已经下载过的包可以继续使用,只下载新增或变化的包。 |
可以简单记成:镜像层缓存负责"尽量不重新安装",npm 下载缓存负责"必须重新安装时尽量少下载"。如果一开始就 COPY . .,任何业务代码变化都会让后面的 npm ci 镜像层缓存失效。
Build context:构建上下文
bash
docker build -t my-api:1.0 -f backend/Dockerfile ./backend
逐项解释:
docker build:构建镜像;-t my-api:1.0:设置镜像名称和标签;-f backend/Dockerfile:指定 Dockerfile 路径;./backend:构建上下文。
构建上下文明确了哪些本地文件可以参与构建,并把这些文件交给构建器。上下文包含的文件过多时,构建会变慢,还可能把日志、依赖或敏感文件传给构建器。
.dockerignore
.dockerignore 用于从构建上下文排除不需要的文件:
dockerignore
# 本地依赖不能复制进 Linux 镜像,且体积很大
node_modules
# 本地旧构建结果应由镜像自己重新构建
dist
# 版本控制文件对运行应用没有必要
.git
# 本地环境变量可能包含密码或密钥
.env
.env.*
# 日志和测试输出不应进入镜像
*.log
coverage
# 操作系统和编辑器文件
.DS_Store
.vscode
.idea
.dockerignore 可以减小构建上下文、加快构建,并降低泄露本地敏感文件的风险。否则,COPY . . 可能把 node_modules、.git、.env 等无关甚至敏感内容一起复制进镜像。
Port:端口映射
NestJS 虽然已经在容器的 3000 端口运行,但浏览器是在我们自己的电脑上,暂时还访问不到容器里面的应用。
bash
# 把本机 3001 端口连接到容器 3000 端口
docker run --rm -p 3001:3000 my-api:1.0
-p 3001:3000 可以理解成给本机和容器之间接了一条通道:
text
浏览器访问 localhost:3001
↓
Docker 转发请求
↓
容器中的 NestJS 3000 端口
其中左边的 3001 是本机端口,右边的 3000 是容器端口。配置完成后,在浏览器访问 http://localhost:3001,就能访问容器中的 NestJS。
EXPOSE 3000只是说明应用使用容器的 3000 端口;-p 3001:3000才是真正建立端口映射。
Bind mount:让容器直接使用本机文件
容器默认看不到本机的项目文件。Bind mount 可以把本机项目连接到容器中的一个目录:
text
本机项目目录 → 容器中的 /app
在本机进入项目根目录,然后执行:
bash
# 启动临时 Node.js 容器,并查看容器 /app 中的文件
docker run --rm \
--mount type=bind,source="$(pwd)",target=/app \
node:24-alpine \
ls /app
核心关键:
| 指令部分 | 具体作用 |
|---|---|
--mount |
给容器添加挂载。 |
type=bind |
表示把本机目录挂载到容器。 |
source="$(pwd)" |
本机目录。$(pwd) 表示终端当前所在的项目目录。 |
target=/app |
容器目录。本机项目会出现在容器的 /app 中。 |
连接成功后,本机和容器看到的是同一套项目文件。你在编辑器中修改代码,容器马上就能看到,不需要重新构建镜像。
Volume:命名数据卷
数据库容器可以删除后重新创建,但数据库中的数据不能跟着消失。Volume 可以理解成一个由 Docker 管理的独立存储空间:
text
PostgreSQL 容器 → postgres_data 数据卷
删除 继续保留
先创建一个名为 postgres_data 的数据卷:
bash
docker volume create postgres_data
查看已经创建的数据卷:
bash
docker volume ls
启动 PostgreSQL,并把数据库的数据目录连接到这个数据卷:
bash
docker run --name db \
-e POSTGRES_PASSWORD=example \
--mount type=volume,source=postgres_data,target=/var/lib/postgresql/data \
-d postgres:17-alpine
核心指令:
| 指令部分 | 具体作用 |
|---|---|
docker volume create postgres_data |
创建一个名为 postgres_data 的数据卷。 |
docker volume ls |
查看本机现有的数据卷。 |
--mount |
给容器添加挂载。 |
type=volume |
表示挂载 Docker 数据卷。 |
source=postgres_data |
使用名为 postgres_data 的数据卷。 |
target=/var/lib/postgresql/data |
PostgreSQL 在容器中保存数据的位置。写入这里的数据会进入数据卷。 |
PostgreSQL 写入 /var/lib/postgresql/data 的内容,实际会保存在 postgres_data 中。以后即使删除原来的 PostgreSQL 容器,只要新容器继续挂载 postgres_data,就能继续使用原来的数据。
Volume 负责让数据不随容器一起删除,但它不是备份。删除 Volume 本身,数据仍然会丢失。
Environment variable:环境变量
bash
docker run --rm \
-e NODE_ENV=production \
-e PORT=3000 \
my-api:1.0
环境变量让同一个镜像在不同环境中使用不同配置,而不必修改镜像。
开发、测试、生产的端口和数据库地址通常不同。通过环境变量可以为同一个镜像传入不同配置,避免把配置写死在代码中并为每个环境重新构建镜像。
环境变量不是天然安全的秘密存储。不要把真实密码写进 Git;生产环境应使用部署平台的 secret 管理能力。
Network:容器网络
每个容器都可以理解成一台独立的小电脑。NestJS 容器和 PostgreSQL 容器默认不知道对方在哪里,需要先加入同一个 Docker 网络。
创建并查看网络:
bash
# 创建一个名为 `app-network` 的网络
docker network create app-network
# 查看本机现有的 Docker 网络
docker network ls
启动容器时,通过 --network 把它们加入同一个网络:
bash
# 把 db 加入到 app-network 网络中
docker run --name db \
--network app-network \
-e POSTGRES_PASSWORD=example \
-d postgres:17-alpine
# 把 api 加入到 app-network 网络中
docker run --name api \
--network app-network \
-d my-api:1.0
下图把容器内部访问和本机浏览器访问放在一起对比:

图中的 db 和 api 来自启动命令中的 --name。它们只能在 Docker 网络内部使用,因此本机浏览器不能直接访问 api:3000。
使用 Docker Compose 时,执行
docker compose up会自动为项目创建网络。api、db等 Compose 服务名会自动成为网络中的访问名称,因此通常不需要手动执行docker network create。
Docker Compose
前面的示例需要分别执行多条 docker run 命令,才能启动前端、后端和数据库。Docker Compose 可以把这些容器的配置集中写进一个 compose.yaml 文件,然后统一启动和停止。
可以先记住这个关系:
text
compose.yaml → 记录项目需要哪些容器
docker compose up → 按照文件内容启动这些容器
在项目根目录创建 compose.yaml:
yaml
services:
frontend:
build: ./frontend
api:
build: ./backend
db:
image: postgres:18-alpine
文件中的核心内容:
| 配置 | 具体作用 |
|---|---|
services |
项目中的服务列表。一个服务通常会运行一个容器。 |
frontend、api、db |
三个服务的名称,也可以作为容器之间互相访问的名称。 |
build: ./frontend |
使用 frontend 目录中的 Dockerfile 构建前端镜像。 |
build: ./backend |
使用 backend 目录中的 Dockerfile 构建后端镜像。 |
image: postgres:18-alpine |
数据库不需要自己构建,直接使用现成的 PostgreSQL 镜像。 |
Docker 常用操作
针对 docker 指令了解即可
镜像命令
bash
# 拉取镜像;不启动容器
docker pull nginx:1.28-alpine
# 列出本地镜像
docker image ls
# 查看镜像的详细元数据
docker image inspect nginx:1.28-alpine
# 删除本地镜像;镜像仍被容器引用时可能无法删除
docker image rm nginx:1.28-alpine
容器生命周期命令
bash
# 查看正在运行的容器
docker ps
# 查看所有容器,包括已经退出的容器
docker ps -a
# 停止容器,给主进程发送终止信号并等待
docker stop web
# 启动一个已经存在但处于停止状态的容器
docker start web
# 重启容器
docker restart web
# 删除已经停止的容器
docker rm web
# 强制删除仍在运行的容器;可能来不及优雅关闭,应谨慎使用
docker rm -f web
docker run 是"创建新容器并启动",docker start 是"启动已有容器"。反复执行同名的 docker run --name web ... 会因为名字已经存在而失败。
日志和排错命令
bash
# 查看容器现有日志
docker logs api
# -f 表示持续跟随新日志;类似 tail -f
docker logs -f api
# 只看最后 100 行并继续跟随
docker logs --tail 100 -f api
# 查看容器完整配置、网络、挂载和状态
docker inspect api
# 查看容器资源使用情况
docker stats
# 在运行中的容器内执行 sh
# -i 保持标准输入;-t 分配终端
docker exec -it api sh
Alpine 镜像通常只有 sh,不一定安装 bash。因此 docker exec -it api bash 失败时应尝试 sh。
docker exec 是进入已经运行的容器;docker run 会创建一个新容器,两者不能混为一谈。
数据卷和网络命令
bash
# 查看数据卷
docker volume ls
# 查看某个数据卷的详细信息
docker volume inspect postgres_data
# 删除未被容器使用的数据卷;删除前必须确认数据是否需要
docker volume rm postgres_data
# 查看 Docker 网络
docker network ls
# 查看网络中的容器和地址
docker network inspect 项目名_default
清理命令
bash
# 清理未使用的停止容器、网络和悬空镜像
docker system prune
# 查看 Docker 占用磁盘情况
docker system df
不要在不了解影响时使用 docker system prune -a --volumes。它可能删除大量未使用镜像和数据卷,数据卷中可能有数据库数据。
容器化 React + NestJS 项目
React + NestJS + PostgreSQL 项目结构是这样的:

如果项目使用 pnpm 或 yarn,需要把 npm ci、锁文件和启动命令替换成对应包管理器的写法,不能混用锁文件。
NestJS 容器化准备
让 NestJS 监听容器网络
backend/src/main.ts:
typescript
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
async function bootstrap() {
// 创建 NestJS HTTP 应用。
const app = await NestFactory.create(AppModule)
// 所有后端接口统一带 /api 前缀。
// 例如 UsersController 的 /users 最终会成为 /api/users。
// 这样 Nginx 和 Vite 可以统一代理 /api,不会与前端页面路由混淆。
app.setGlobalPrefix('api')
// 如果开发时浏览器直接跨域访问后端,需要配置 CORS。
// 本文优先让浏览器请求同源 /api,再由 Vite/Nginx 代理,因此生产环境通常不依赖宽松 CORS。
// 如果确实需要跨域,应明确列出允许的 origin,避免无条件开放。
if (process.env.NODE_ENV !== 'production') {
app.enableCors({
origin: 'http://localhost:5173',
credentials: true,
})
}
// 环境变量本质是字符串,所以要转换成 number。
// 没有传 PORT 时使用 3000。
const port = Number(process.env.PORT ?? 3000)
// 0.0.0.0 表示监听容器的所有网络接口。
// 如果只监听 127.0.0.1,通常只有容器内部自己能访问,其他容器和宿主机端口映射可能无法连接。
await app.listen(port, '0.0.0.0')
}
bootstrap()
添加健康检查接口
backend/src/health.controller.ts:
typescript
import { Controller, Get } from '@nestjs/common'
@Controller('health')
export class HealthController {
@Get()
check() {
// 返回简单结果,供 Docker 或负载均衡器判断 HTTP 服务是否可响应。
// 更完整的项目可以继续检查数据库、Redis 等关键依赖。
return {
status: 'ok',
timestamp: new Date().toISOString(),
}
}
}
在 backend/src/app.module.ts 注册:
typescript
import { Module } from '@nestjs/common'
import { HealthController } from './health.controller'
@Module({
// Controller 必须注册到模块,NestJS 才会创建对应路由。
controllers: [HealthController],
})
export class AppModule {}
由于设置了全局前缀,健康检查地址是 /api/health。
健康检查用于判断应用是否真的可以接收请求。容器进程存在时,NestJS 可能仍在启动,数据库连接也可能已经失败,因此只查看容器是否运行并不够。
不提供健康检查时:Compose 只能大致知道容器是否运行,无法判断业务服务是否真的可用。
构建 React 生产镜像
Vite 项目执行 npm run build 后,默认生成 dist 静态文件。生产环境不需要 Vite 开发服务器,也不需要把完整 Node.js 开发环境暴露给用户。
为什么使用多阶段构建
前端构建需要 Node.js、npm 和完整依赖,但运行构建结果只需要一个静态文件服务器。
text
builder 阶段:Node.js + npm + node_modules + 源码 → 生成 dist
runner 阶段:Nginx + dist → 最终镜像
如果直接用 Node.js 开发镜像上线:
- 镜像更大;
- 包含不必要的源码和开发依赖;
- 攻击面更大;
- Vite dev server 也不是这里的生产静态服务器方案。
React 生产 Dockerfile
frontend/Dockerfile:
dockerfile
# syntax=docker/dockerfile:1
# ---------- 第一阶段:构建 React ----------
FROM node:24-alpine AS builder
WORKDIR /app
# 构建参数只在构建过程中使用。
# Vite 会把 VITE_ 前缀变量写入浏览器可下载的 JS,因此绝对不能放密钥。
ARG VITE_API_BASE_URL=/api
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
COPY package.json package-lock.json ./
# 使用 BuildKit 缓存 npm 下载目录,加快重复构建。
# 这不是把 node_modules 从宿主机带进镜像,而是缓存下载内容。
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
# 执行 Vite 生产构建,默认输出 /app/dist。
RUN npm run build
# ---------- 第二阶段:运行静态站点 ----------
# 使用官方 Docker 指南采用的非特权 Nginx 镜像,默认使用非 root 用户。
FROM nginxinc/nginx-unprivileged:alpine3.22 AS runner
# 替换 Nginx 配置,加入 SPA history fallback 和 /api 代理。
COPY nginx.conf /etc/nginx/nginx.conf
# 只从 builder 复制最终静态文件,不复制 node_modules 和源码。
# --chown 确保 Nginx 用户拥有正确文件权限。
COPY --chown=nginx:nginx --from=builder \
/app/dist /usr/share/nginx/html
# 明确使用非 root 用户运行。
USER nginx
# 非特权用户不能随意绑定 1024 以下端口,因此使用 8080。
EXPOSE 8080
# Nginx 默认会转入后台;daemon off 让它留在前台作为容器主进程。
# 主进程退出时,容器就应该退出,Docker 才能正确管理状态。
CMD ["nginx", "-g", "daemon off;"]
Nginx 配置
frontend/nginx.conf:
nginx
# Nginx 事件模块,基础配置必须存在。
events {}
http {
# 根据文件扩展名返回正确 Content-Type,例如 JS、CSS、SVG。
include /etc/nginx/mime.types;
server {
# 非 root Nginx 在容器中监听 8080。
listen 8080;
# Vite 构建产物复制到了这个目录。
root /usr/share/nginx/html;
index index.html;
# 后端 API 反向代理。
location /api/ {
# api 是 Compose 服务名;这里没有尾部 /,因此原始 /api/... 路径会保留。
proxy_pass http://api:3000;
# 把用户请求的 Host 和客户端地址传给 NestJS,便于日志与生成链接。
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;
}
# React Router history 模式支持。
location / {
# 先找真实文件或目录;找不到时返回 index.html,让 React Router 接管路由。
try_files $uri $uri/ /index.html;
}
# 带内容哈希的静态资源通常可以长期缓存。
# 如果你的构建工具不生成内容哈希,需要调整缓存策略。
location /assets/ {
try_files $uri =404;
expires 1y;
add_header Cache-Control "public, immutable";
}
}
}
如果没有 try_files ... /index.html,直接刷新 /users/1 这样的前端路由时,Nginx 会尝试寻找真实文件 /users/1,最终返回 404。
构建 NestJS 生产镜像
backend/Dockerfile:
dockerfile
# syntax=docker/dockerfile:1
# ---------- 第一阶段:安装依赖并构建 ----------
FROM node:24-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
# 构建 NestJS 需要 devDependencies,例如 TypeScript 和 Nest CLI。
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
# 把 TypeScript 编译为 JavaScript,通常输出到 dist。
RUN npm run build
# 构建已经完成,删除 devDependencies,只保留生产运行依赖。
# 如果应用运行时错误地依赖了 devDependency,这一步会暴露项目依赖分类问题。
RUN npm prune --omit=dev
# ---------- 第二阶段:运行 NestJS ----------
FROM node:24-alpine AS runner
# 告诉 NestJS 及依赖当前是生产环境。
ENV NODE_ENV=production
WORKDIR /app
# 复制生产依赖和编译结果,不复制 TypeScript 源码与开发工具。
COPY --chown=node:node --from=builder /app/package.json ./package.json
COPY --chown=node:node --from=builder /app/node_modules ./node_modules
COPY --chown=node:node --from=builder /app/dist ./dist
# Node 官方镜像已经提供低权限 node 用户。
USER node
EXPOSE 3000
# 直接运行编译后的 JavaScript,不在生产容器里调用 Nest CLI。
CMD ["node", "dist/main.js"]
使用 Docker Compose
Dockerfile 描述"一个镜像如何构建",compose.yaml 描述"多个服务如何一起运行"。
Compose 配置模型
Compose 使用 YAML 书写,但能使用哪些属性由 Compose Specification 规定,并不是随意命名的。作为前端开发者,可以先通过下面这个接近 JSON 结构的 JavaScript 对象认识常用属性:
javascript
// 这是帮助理解配置结构的伪代码,不需要在项目中创建这个 JS 文件。
const composeConfig = {
// services 是固定属性,里面放项目需要运行的服务。
services: {
// 服务名由项目自定义,例如 frontend、api、db。
"<服务名>": {
// 直接使用已有镜像。
image: "镜像名称:版本",
// 使用项目中的 Dockerfile 构建镜像。
build: {
context: "构建目录",
dockerfile: "Dockerfile 文件名",
args: {
"<构建参数名>": "构建参数值"
}
},
// 把宿主机端口映射到容器端口。
ports: ["宿主机端口:容器端口"],
// 把环境变量传给运行中的容器。
environment: {
"<环境变量名>": "环境变量值"
},
// 检查容器中的服务是否真正可用。
healthcheck: {
test: ["CMD", "检查命令"],
interval: "10s", // 每隔多久检查一次
timeout: "5s", // 单次检查最多等待多久
retries: 5, // 连续失败多少次后标记为 unhealthy
start_period: "20s" // 容器启动缓冲时间
},
// 声明当前服务依赖哪个服务。
depends_on: {
"<依赖服务名>": {
// 等依赖服务的 healthcheck 通过后再启动。
condition: "service_healthy"
}
},
// 挂载宿主机目录或命名数据卷。
volumes: ["数据来源:容器目录"],
// 容器退出后的重启策略。
restart: "unless-stopped"
}
},
// 声明 services 中使用的命名数据卷。
volumes: {
"<数据卷名称>": {}
}
}
完整属性可以查询 Docker 官方的 Compose 文件配置索引 和 Services 属性列表。
Compose 服务连接
执行 docker compose up 时,Compose 会为项目创建一个默认网络,并把 services 中的服务加入这个网络。服务名同时成为网络内部的访问名称:

因此,容器之间使用的是"服务名:容器端口",不需要查询可能变化的容器 IP。ports 只在需要把某个容器端口发布到宿主机时使用,容器之间通信不依赖 ports。
完整的 compose.yaml
yaml
services:
# 前端服务:构建 React,并使用 Nginx 提供静态文件和 API 代理。
frontend:
build:
# 构建目录;该目录会成为 Docker 构建上下文。
context: ./frontend
dockerfile: Dockerfile
args:
# 传给前端 Dockerfile 的 ARG,构建后的前端统一请求 /api。
VITE_API_BASE_URL: /api
ports:
# 宿主机 8080 端口 → frontend 容器 8080 端口。
- '8080:8080'
depends_on:
# 等 api 的 healthcheck 通过后再启动 frontend。
api:
condition: service_healthy
restart: unless-stopped
# 后端服务:运行 NestJS,并通过 db:5432 连接 PostgreSQL。
api:
build:
context: ./backend
dockerfile: Dockerfile
environment:
NODE_ENV: production
PORT: 3000
# db 是下面的服务名,5432 是 PostgreSQL 的容器端口。
# ${...} 从 compose.yaml 同目录的 .env 文件读取。
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
depends_on:
# 等数据库可以正常连接后再启动 api。
db:
condition: service_healthy
healthcheck:
# 在 api 容器内部请求 NestJS 健康检查接口。
test: ['CMD', 'node', '-e', "fetch('http://localhost:3000/api/health').then(r => { if (!r.ok) process.exit(1) }).catch(() => process.exit(1))"]
interval: 10s # 每隔 10 秒检查一次
timeout: 5s # 单次检查最多等待 5 秒
retries: 5 # 连续失败 5 次后标记为 unhealthy
start_period: 20s # 启动后的前 20 秒属于初始化时间
restart: unless-stopped
# 数据库服务:直接使用 PostgreSQL 官方镜像。
db:
image: postgres:18-alpine
environment:
# PostgreSQL 首次启动时使用这些值创建数据库和用户。
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
# postgres_data 数据卷 → PostgreSQL 的数据目录。
- postgres_data:/var/lib/postgresql
healthcheck:
# $$ 把 $ 保留到容器运行时,再读取容器中的环境变量。
test: ['CMD-SHELL', 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}']
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
# 创建 db 服务使用的命名数据卷。
volumes:
# {} 表示没有额外配置,使用 Docker 默认方式创建和管理该数据卷。
postgres_data: {}
depends_on 只负责启动等待,不会替应用建立业务连接。真正让 Nginx 找到 NestJS 的,是 frontend/nginx.conf 中的代理地址:
nginx
location /api/ {
# api 是 Compose 服务名,3000 是 NestJS 的容器端口。
proxy_pass http://api:3000;
}
运行与检查
bash
# 校验配置,但不启动容器;没有输出表示配置通过。
docker compose config --quiet
# 构建镜像,并在后台启动全部服务。
docker compose up -d --build
服务启动后,通过 http://服务器地址:8080 访问前端。
Compose 常用命令
bash
# 查看全部服务的运行状态和健康状态
docker compose ps
# 持续查看全部服务的日志;按 Ctrl + C 退出查看
docker compose logs -f
# 只持续查看 api 服务的日志
docker compose logs -f api
# 重启 api 服务;不会重新构建镜像
docker compose restart api
# 进入正在运行的 api 容器
docker compose exec api sh
# 停止并删除项目容器和网络;命名数据卷仍然保留
docker compose down
总结
bash
# 验证安装
docker version
docker compose version
docker run --rm hello-world
# 镜像
docker pull nginx:1.28-alpine
docker image ls
docker build -t my-app:1.0 .
# 容器
docker ps
docker ps -a
docker logs -f 容器名
docker exec -it 容器名 sh
docker stop 容器名
docker rm 容器名
# Compose
docker compose config
docker compose up -d --build
docker compose ps
docker compose logs --tail 100 -f
docker compose down
# 磁盘和资源
docker stats
docker system df
docker volume ls
docker network ls
最后记住一句话:Docker 镜像负责"交付什么",容器负责"怎么运行这个交付物",Compose 负责"多个容器怎样一起工作"。