在 NestJS 项目进入生产环境后,部署方式会直接影响交付效率、环境一致性和线上故障排查成本。如果仍然在服务器上手动安装依赖、执行构建、切换 Node.js 版本,很容易出现「本地能跑、线上不能跑」的问题。
Docker 的价值在于把应用、Node.js 运行时、生产依赖和启动命令封装进同一个镜像。服务器只需要具备 Docker 和 Docker Compose,就可以按固定流程加载镜像、启动容器、挂载日志目录并执行健康检查。
本文结合 NestJS 后端项目的 Dockerfile、docker-compose.yml 和 scripts/deploy.sh,总结一套可直接落地的 Docker 部署方案:本地构建镜像、压缩上传到服务器、远程加载镜像、使用 Docker Compose 重建服务,并通过 /api/meta 完成发布后的健康验证。
一、整体部署思路
不使用远程镜像仓库,而是采用「本地构建镜像 + scp 上传镜像包」的部署方式。
完整流程如下:
本地开发机
├── 检查 Docker、ssh、scp、gzip 等命令
├── 读取 package.json version 作为默认镜像版本
├── 通过 ssh 检测远程服务器 CPU 架构
├── 使用 docker buildx 构建对应平台镜像
├── docker save 导出镜像
└── gzip 压缩后上传到远程服务器
远程服务器
├── 创建部署目录
├── 接收镜像包、docker-compose.yml、.env、.env.production
├── gzip 解压并 docker load 导入镜像
├── 使用 Docker Compose 强制重建容器
├── 等待 Docker healthcheck 通过
└── 请求 /api/meta 做最终服务检查
这种方式的核心优点是简单、直观、不依赖镜像仓库。对于个人项目、小团队内部系统或单机部署场景,它的维护成本很低。
二、项目目录约定
部署相关文件:
app-backend/
├── Dockerfile
├── docker-compose.yml
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── .env
├── .env.production
└── scripts/
└── deploy.sh
几个关键文件的职责如下:
-
Dockerfile:定义 NestJS 应用如何构建成 Docker 镜像 -
docker-compose.yml:定义服务器上如何启动容器、加载环境变量、挂载日志、配置健康检查 -
.env:基础环境配置,会被上传到远程部署目录 -
.env.production:生产环境配置,会被上传到远程部署目录 -
scripts/deploy.sh:一键部署脚本,负责构建、打包、上传、远程启动和健康检查
三、准备 NestJS 生产启动命令
Docker 镜像最终启动的是编译后的 JavaScript 文件,而不是 nest start --watch 这种开发命令。
package.json 中,生产启动命令是:
{
"scripts": {
"build": "node scripts/meta.mjs && nest build",
"start:prod": "NODE_ENV=production node dist/main"
}
}
这里有两个关键点:
-
pnpm build会先执行scripts/meta.mjs更新public/meta.json,再通过nest build编译到dist/ -
pnpm start:prod会设置NODE_ENV=production,然后执行node dist/main
因此 Dockerfile 只需要在构建阶段生成 dist/,在运行阶段安装生产依赖并执行 pnpm start:prod。
四、编写多阶段 Dockerfile
使用 Node.js 24.12.0-alpine,并通过 Docker 多阶段构建减少最终镜像体积。
# 使用Node.js官方镜像作为基础镜像
# 阶段1: 构建应用
FROM node:24.12.0-alpine AS builder
WORKDIR /app
# 保证时区一致
ENV TZ=Asia/Shanghai
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN corepack enable
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
# 阶段2: 运行应用
FROM node:24.12.0-alpine AS runner
WORKDIR /app
# 保证运行时区一致
ENV TZ=Asia/Shanghai
# 复制package.json和pnpm-lock.yaml
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
# 启用package.json中固定版本的pnpm
RUN corepack enable
# 安装生产依赖
RUN pnpm install --prod --frozen-lockfile
# 从构建阶段复制构建产物
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/public ./public
# 暴露8300端口
EXPOSE 8300
# 启动应用
CMD ["pnpm", "start:prod"]
这份 Dockerfile 分成两个阶段。
第一阶段是 builder:
-
设置工作目录为
/app -
设置容器时区为
Asia/Shanghai -
复制
package.json、pnpm-lock.yaml、pnpm-workspace.yaml -
通过
corepack enable启用package.json中声明的pnpm@11.9.0 -
使用
pnpm install --frozen-lockfile按锁文件安装完整依赖 -
复制项目全部源码
-
执行
pnpm build生成dist/和更新后的public/
第二阶段是 runner:
-
使用同样的 Node.js 运行时,避免构建环境和运行环境版本不一致
-
只安装生产依赖:
pnpm install --prod --frozen-lockfile -
从
builder阶段复制dist/和public/ -
暴露默认端口
8300 -
使用
pnpm start:prod启动 NestJS 应用
多阶段构建的好处是:最终镜像不需要保留 TypeScript 源码编译过程中产生的临时状态,也不需要安装 devDependencies,生产镜像更干净。
五、使用 Docker Compose 管理容器
镜像负责「应用如何构建」,Compose 负责「应用如何运行」。
docker-compose.yml 内容如下:
services:
app:
image: ${IMAGE_NAME:-app-backend}:${IMAGE_TAG:-1.0.0}
container_name: app-backend
network_mode: host
ports:
- "8300:8300"
env_file:
- .env
- .env.production
environment:
NODE_ENV: production
volumes:
- ./logs:/app/logs
healthcheck:
test:
- CMD
- node
- -e
- >-
fetch('http://127.0.0.1:' + (process.env.PORT || 8300) + '/api/meta')
.then((response) => { if (!response.ok) process.exit(1) })
.catch(() => process.exit(1))
interval: 5s
timeout: 3s
retries: 12
start_period: 10s
init: true
restart: unless-stopped
这里有几处部署关键点。
1. 镜像名和版本由环境变量控制
image: ${IMAGE_NAME:-app-backend}:${IMAGE_TAG:-1.0.0}
Compose 启动时会读取 IMAGE_NAME 和 IMAGE_TAG。如果没有传入,则默认使用:
app-backend:1.0.0
部署脚本会在远程执行时导出这两个变量:
export IMAGE_NAME IMAGE_TAG
这样每次部署都可以启动指定版本的镜像,而不是固定写死镜像 tag。
2. 使用 host 网络
network_mode: host
ports:
- "8300:8300"
network_mode: host 表示容器直接使用宿主机网络。对于 Linux 服务器来说,NestJS 在容器内监听 8300,宿主机也可以直接通过 8300 访问。
需要注意:使用 host 网络时,ports 映射实际不会再起传统桥接网络的端口转发作用,但保留它可以表达应用默认端口,方便阅读配置。
3. 加载多份环境变量文件
env_file:
- .env
- .env.production
environment:
NODE_ENV: production
容器会加载远程部署目录下的 .env 和 .env.production。同时,Compose 明确设置:
NODE_ENV=production
这和 NestJS 应用内部的配置加载逻辑保持一致。基础配置可以放在 .env,生产专用配置放在 .env.production,避免把所有配置都混在一个文件里。
4. 挂载日志目录
volumes:
- ./logs:/app/logs
容器内 /app/logs 会映射到远程部署目录下的 logs/。
这样做的好处是:容器重建或镜像升级后,日志文件仍保留在宿主机上,便于排查线上问题。
5. 配置健康检查
healthcheck:
test:
- CMD
- node
- -e
- >-
fetch('http://127.0.0.1:' + (process.env.PORT || 8300) + '/api/meta')
.then((response) => { if (!response.ok) process.exit(1) })
.catch(() => process.exit(1))
interval: 5s
timeout: 3s
retries: 12
start_period: 10s
健康检查会在容器内部请求:
http://127.0.0.1:${PORT || 8300}/api/meta
只要接口返回非 2xx 状态,或者请求失败,健康检查就会失败。
这要求 NestJS 应用必须提供 /api/meta 接口。部署脚本也会在容器启动后再次请求这个接口,作为发布完成前的最终检查。
6. 启用 init 和自动重启
init: true
restart: unless-stopped
init: true 会让 Docker 在容器中注入一个轻量 init 进程,用来正确处理子进程和系统信号。
restart: unless-stopped 表示容器异常退出后自动重启,除非人工显式停止容器。对于后端服务来说,这是一个常见的生产配置。
六、编写一键部署脚本
部署入口是:
app-backend/scripts/deploy.sh
它可以直接执行:
cd app-backend
./scripts/deploy.sh
也可以指定镜像版本:
cd app-backend
./scripts/deploy.sh 1.0.1
如果不传版本号,脚本会读取 package.json 中的 version:
IMAGE_TAG="${1:-$(node -p "require('${PROJECT_DIR}/package.json').version")}"
也就是说,默认会使用:
app-backend:1.0.0
1. 定义部署默认值
脚本开头定义了几个关键变量:
REMOTE_HOST="${REMOTE_HOST:-kr}"
REMOTE_DIR="${REMOTE_DIR:-/opt/apps/app-backend}"
IMAGE_NAME="${IMAGE_NAME:-app-backend}"
IMAGE_TAG="${1:-$(node -p "require('${PROJECT_DIR}/package.json').version")}"
COMPOSE_FILE="${PROJECT_DIR}/docker-compose.yml"
BASE_ENV_FILE="${PROJECT_DIR}/.env"
PROD_ENV_FILE="${PROJECT_DIR}/.env.production"
FULL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}"
默认部署目标是:
REMOTE_HOST=kr
REMOTE_DIR=/opt/apps/app-backend
IMAGE_NAME=app-backend
如果要部署到其他服务器,可以在执行命令前覆盖环境变量:
cd app-backend
REMOTE_HOST=root@example.com REMOTE_DIR=/opt/apps/app-backend ./scripts/deploy.sh 1.0.1
如果要修改镜像名称,也可以覆盖 IMAGE_NAME:
cd app-backend
IMAGE_NAME=registry-free/app-backend ./scripts/deploy.sh 1.0.1
2. 校验镜像版本格式
脚本会先校验镜像 tag 是否符合 Docker tag 规则:
if [[ ! "${IMAGE_TAG}" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then
echo "错误:镜像版本格式无效:${IMAGE_TAG}" >&2
exit 1
fi
合法示例:
1.0.0
1.0.1
release-20260726
不建议使用包含空格、中文或特殊符号的版本号。
3. 检查本地依赖命令
脚本要求本地必须安装以下命令:
for command_name in docker ssh scp gzip; do
if ! command -v "${command_name}" >/dev/null 2>&1; then
echo "错误:未找到命令 ${command_name}" >&2
exit 1
fi
done
这些命令分别负责:
-
docker:构建、导出镜像 -
ssh:远程执行命令 -
scp:上传镜像包和配置文件 -
gzip:压缩镜像包,减少传输体积
脚本还会检查 Docker daemon 是否已经启动:
if ! docker info >/dev/null 2>&1; then
echo "错误:Docker daemon 未运行,请先启动 Docker Desktop。" >&2
exit 1
fi
在 macOS 上,如果 Docker Desktop 没有启动,部署会在这里中断。
4. 自动识别远程服务器架构
部署脚本会通过 uname -m 获取远程服务器 CPU 架构:
REMOTE_ARCH="$(ssh "${REMOTE_HOST}" 'uname -m' | tr -d '\r' | tail -n 1)"
然后映射成 Docker 平台:
case "${REMOTE_ARCH}" in
x86_64 | amd64)
DEFAULT_PLATFORM="linux/amd64"
;;
aarch64 | arm64)
DEFAULT_PLATFORM="linux/arm64"
;;
*)
echo "错误:不支持的远程服务器架构:${REMOTE_ARCH}" >&2
exit 1
;;
esac
这一步很重要。比如本地是 Apple Silicon Mac,但服务器是 x86_64。如果直接构建本机架构镜像,传到服务器后可能无法运行。
脚本最终使用:
PLATFORM="${PLATFORM:-${DEFAULT_PLATFORM}}"
如果有特殊需求,也可以手动指定平台:
cd app-backend
PLATFORM=linux/amd64 ./scripts/deploy.sh 1.0.1
5. 构建指定平台镜像
核心构建命令是:
docker buildx build \
--platform "${PLATFORM}" \
--tag "${FULL_IMAGE}" \
--load \
"${PROJECT_DIR}"
参数说明:
-
--platform "${PLATFORM}":按远程服务器架构构建镜像 -
--tag "${FULL_IMAGE}":设置镜像名和版本号 -
--load:把 buildx 构建结果加载到本地 Docker 镜像列表 -
"${PROJECT_DIR}":使用后端项目根目录作为构建上下文
构建完成后,本地会出现类似镜像:
app-backend:1.0.0
6. 导出并压缩镜像
构建完成后,脚本会把镜像导出成 tar 包,并使用 gzip 压缩:
docker save "${FULL_IMAGE}" | gzip -c >"${ARCHIVE_PATH}"
压缩文件名由镜像名和版本号组成:
ARCHIVE_NAME="${IMAGE_NAME//\//-}-${IMAGE_TAG}.tar.gz"
例如:
app-backend-1.0.0.tar.gz
临时文件会放在 mktemp -d 创建的目录中,并在脚本退出时自动清理:
cleanup() {
rm -rf "${TEMP_DIR}"
}
trap cleanup EXIT
7. 上传文件到远程服务器
脚本会先创建远程部署目录:
ssh "${REMOTE_HOST}" bash -s -- "${REMOTE_DIR}" <<'REMOTE_PREPARE'
set -Eeuo pipefail
mkdir -p "$1"
REMOTE_PREPARE
然后上传镜像包、Compose 文件和环境变量文件:
scp "${ARCHIVE_PATH}" "${COMPOSE_FILE}" "${BASE_ENV_FILE}" "${REMOTE_HOST}:${REMOTE_DIR}/"
scp "${ARCHIVE_PATH}" "${COMPOSE_FILE}" "${PROD_ENV_FILE}" "${REMOTE_HOST}:${REMOTE_DIR}/"
远程目录最终会包含:
/opt/apps/app-backend/
├── docker-compose.yml
├── .env
├── .env.production
├── app-backend-1.0.0.tar.gz
└── logs/
其中 logs/ 会在容器运行时作为宿主机日志目录使用。
七、远程加载镜像并启动服务
上传完成后,部署脚本会通过 ssh 在远程服务器执行发布逻辑。
1. 检查生产配置文件
远程部署前必须存在 .env.production:
if [[ ! -f .env.production ]]; then
echo "错误:远程配置文件 ${REMOTE_DIR}/.env.production 不存在。" >&2
exit 1
fi
因为脚本会上传 .env.production,正常情况下不会缺失。这个检查主要用于防止手动部署或上传失败时继续启动服务。
2. 检查远程 Docker 与 Compose
远程服务器必须安装 Docker 和 gzip:
for command_name in docker gzip; do
if ! command -v "${command_name}" >/dev/null 2>&1; then
echo "错误:远程服务器未安装 ${command_name}。" >&2
exit 1
fi
done
Compose 支持两种命令形式:
if docker compose version >/dev/null 2>&1; then
COMPOSE_COMMAND=(docker compose)
elif command -v docker-compose >/dev/null 2>&1; then
COMPOSE_COMMAND=(docker-compose)
else
echo "错误:远程服务器未安装 Docker Compose。" >&2
exit 1
fi
也就是说,新版 Docker Compose V2 和旧版 docker-compose V1 都能兼容。
3. 导入镜像
远程服务器收到的是 .tar.gz 镜像包,需要先解压再导入 Docker:
gzip -dc "${ARCHIVE_NAME}" | docker load
导入成功后,远程服务器就拥有本次部署的镜像:
app-backend:1.0.0
4. 兼容旧版 Docker Compose
脚本中有一段对 Docker Compose V1 的兼容逻辑:
if [[ "${COMPOSE_VERSION}" =~ (^|[[:space:]])v?1\. ]]; then
echo "检测到旧版 Docker Compose,正在移除旧容器以兼容新版 Docker Engine"
"${COMPOSE_COMMAND[@]}" down --remove-orphans
fi
原因是 Python 版 Docker Compose 1.29.2 和新版 Docker Engine 的部分镜像元数据不兼容,直接 recreate 旧容器时可能因为缺少 ContainerConfig 字段而失败。
当前部署本来就要求强制重建容器,所以对旧版 Compose 先执行 down --remove-orphans,不会改变发布语义,只是让部署过程更稳定。
5. 使用 Compose 强制重建容器
真正启动服务的命令是:
"${COMPOSE_COMMAND[@]}" up -d --no-build --remove-orphans --force-recreate
参数说明:
-
up -d:后台启动服务 -
--no-build:不在服务器上构建镜像,只使用已经docker load的镜像 -
--remove-orphans:移除 Compose 文件中已经不存在的孤儿容器 -
--force-recreate:即使配置看起来没有变化,也强制重建容器
启动后,脚本会打印服务状态:
"${COMPOSE_COMMAND[@]}" ps
然后获取 app 服务的容器 ID:
CONTAINER_ID="$("${COMPOSE_COMMAND[@]}" ps -q app)"
如果找不到容器,脚本会输出最近 100 行日志并终止部署:
"${COMPOSE_COMMAND[@]}" logs --tail=100 app >&2 || true
八、等待健康检查通过
容器启动不代表服务已经可用。NestJS 还需要完成配置加载、数据库连接、模块初始化等工作。
因此脚本会等待 Docker healthcheck 进入 healthy 状态:
HEALTH_TIMEOUT=90
HEALTH_DEADLINE=$((SECONDS + HEALTH_TIMEOUT))
while ((SECONDS < HEALTH_DEADLINE)); do
HEALTH_STATUS="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "${CONTAINER_ID}")"
case "${HEALTH_STATUS}" in
healthy)
echo "服务健康检查已通过"
break
;;
unhealthy)
echo "错误:服务健康检查失败。" >&2
"${COMPOSE_COMMAND[@]}" logs --tail=100 app >&2 || true
exit 1
;;
starting)
sleep 2
;;
*)
echo "错误:容器健康状态异常:${HEALTH_STATUS}" >&2
"${COMPOSE_COMMAND[@]}" logs --tail=100 app >&2 || true
exit 1
;;
esac
done
这里最多等待 90 秒。健康状态可能有几种:
-
starting:服务仍在启动,继续等待 -
healthy:健康检查通过,可以继续下一步 -
unhealthy:健康检查失败,打印日志并终止 -
none或其他值:说明容器没有正常提供健康检查状态,打印日志并终止
如果超过 90 秒仍未变成 healthy,脚本会认为部署失败:
if [[ "${HEALTH_STATUS}" != healthy ]]; then
echo "错误:等待服务健康检查超时(${HEALTH_TIMEOUT} 秒)。" >&2
"${COMPOSE_COMMAND[@]}" logs --tail=100 app >&2 || true
exit 1
fi
九、发布后的接口检查
健康检查通过后,脚本还会在容器内执行一次接口请求:
"${COMPOSE_COMMAND[@]}" exec -T app node -e \
"fetch('http://127.0.0.1:' + (process.env.PORT || 8300) + '/api/meta').then((response) => { if (!response.ok) throw new Error('HTTP ' + response.status); return response.text(); }).then((body) => console.log('服务检查通过:' + body))"
这一步和 Docker healthcheck 请求的是同一个接口:
/api/meta
区别在于,healthcheck 只关心成功或失败;这一步会输出接口响应内容,方便确认当前运行版本、构建信息或服务元数据。
检查完成后,脚本会删除远程服务器上的镜像压缩包:
rm -f "${ARCHIVE_NAME}"
最后输出:
部署完成:app-backend:1.0.0
十、服务器首次部署准备
首次使用这套方案前,需要先准备远程服务器环境。
1. 安装 Docker
服务器需要安装 Docker Engine。安装完成后确认版本:
docker version
docker info
2. 安装 Docker Compose
优先使用 Docker Compose V2:
docker compose version
如果服务器只安装了旧版命令,也可以:
docker-compose version
当前脚本兼容两种形式。
3. 配置 SSH 登录
本地需要能通过 REMOTE_HOST 免密或正常登录服务器。
默认目标是:
ssh kr
如果本机 ~/.ssh/config 中配置了 kr,可以直接使用默认值。否则执行部署时需要显式指定:
cd app-backend
REMOTE_HOST=root@your-server-ip ./scripts/deploy.sh 1.0.0
4. 确认生产环境变量
部署脚本会上传本地后端项目中的 .env 和 .env.production。因此执行发布前,要确认这两个文件已经包含生产运行所需配置。
常见配置包括:
PORT=8300
DB_TYPE=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=neocrown
DB_USERNAME=...
DB_PASSWORD=...
JWT_SECRET=...
如果数据库不在同一台服务器,DB_HOST 应该填写数据库服务器地址。
如果数据库运行在同一台服务器,并且应用容器使用 network_mode: host,那么应用访问 127.0.0.1:3306 时访问的是宿主机网络上的 MySQL。
十一、常用部署命令
使用 package.json 中的版本号部署:
cd app-backend
./scripts/deploy.sh
指定版本号部署:
cd app-backend
./scripts/deploy.sh 1.0.1
部署到指定服务器:
cd app-backend
REMOTE_HOST=root@example.com ./scripts/deploy.sh 1.0.1
部署到指定远程目录:
cd app-backend
REMOTE_DIR=/opt/apps/app-backend ./scripts/deploy.sh 1.0.1
指定构建平台:
cd app-backend
PLATFORM=linux/amd64 ./scripts/deploy.sh 1.0.1
组合使用:
cd app-backend
REMOTE_HOST=root@example.com REMOTE_DIR=/opt/apps/app-backend PLATFORM=linux/amd64 ./scripts/deploy.sh 1.0.1
十二、常用排障命令
查看远程容器状态:
ssh kr
cd /opt/apps/app-backend
docker compose ps
查看应用日志:
ssh kr
cd /opt/apps/app-backend
docker compose logs -f app
查看最近 100 行日志:
ssh kr
cd /opt/apps/app-backend
docker compose logs --tail=100 app
查看健康检查状态:
ssh kr
docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' app-backend
手动请求健康检查接口:
ssh kr
curl http://127.0.0.1:8300/api/meta
进入容器执行命令:
ssh kr
cd /opt/apps/app-backend
docker compose exec app sh
重启服务:
ssh kr
cd /opt/apps/app-backend
docker compose restart app
停止服务:
ssh kr
cd /opt/apps/app-backend
docker compose down
十三、常见问题
1. Docker daemon 未运行
本地执行部署时如果出现:
错误:Docker daemon 未运行,请先启动 Docker Desktop。
说明本地 Docker Desktop 没有启动。启动 Docker Desktop 后重新执行:
cd app-backend
./scripts/deploy.sh
2. 镜像版本格式无效
如果版本号包含空格、中文或特殊符号,会触发:
错误:镜像版本格式无效
推荐使用语义化版本或日期版本:
./scripts/deploy.sh 1.0.1
./scripts/deploy.sh release-20260726
3. 远程服务器架构不支持
脚本只支持:
x86_64 / amd64
aarch64 / arm64
如果远程 uname -m 返回其他值,脚本会中断。可以先登录服务器确认:
ssh kr 'uname -m'
4. 健康检查失败
健康检查失败通常有几类原因:
-
.env.production缺少必填环境变量 -
数据库连接失败
-
PORT配置和实际监听端口不一致 -
/api/meta接口不存在或返回非 2xx 状态 -
应用启动时抛出异常
优先查看日志:
ssh kr
cd /opt/apps/app-backend
docker compose logs --tail=100 app
5. 容器启动了但无法访问
当前 Compose 使用 network_mode: host,在 Linux 服务器上应用会直接监听宿主机端口。
先在服务器本机检查:
curl http://127.0.0.1:8300/api/meta
如果本机可以访问,但外网不能访问,重点检查:
-
云服务器安全组是否开放
8300 -
系统防火墙是否放行
8300 -
Nginx 或网关反向代理是否配置正确
十四、为什么不在服务器上构建镜像
很多部署方案会把源码上传到服务器,然后在服务器上执行:
docker compose build
docker compose up -d
当前没有这么做,而是本地构建后上传镜像包。
这种方式有几个好处:
-
服务器不需要安装 Node.js 和 pnpm
-
服务器不需要拉取 npm 依赖,避免网络波动影响部署
-
构建产物在本地生成,失败更容易排查
-
远程服务器只负责运行镜像,职责更单一
-
可以根据远程 CPU 架构提前构建正确平台的镜像
代价是每次部署需要上传完整镜像包。如果镜像较大、服务器带宽较低,上传会比镜像仓库增量拉取慢。项目规模变大后,可以考虑接入私有镜像仓库或 CI/CD。
十五、可以继续优化的方向
当前方案已经能稳定完成单机部署,但还可以继续增强。
1. 接入镜像仓库
如果部署频率提高,可以改成:
docker buildx build --platform linux/amd64 -t your-registry/app-backend:1.0.1 --push .
远程服务器再执行:
docker compose pull
docker compose up -d
这样可以减少手动传输镜像包的成本,也更适合 CI/CD。
2. 增加自动回滚
当前脚本在健康检查失败时会中断并打印日志,但不会自动回滚到上一个版本。
如果需要更强的生产保障,可以在远程保留上一版本镜像 tag,并在健康检查失败后执行:
IMAGE_TAG=previous-version docker compose up -d --no-build --force-recreate
回滚逻辑要谨慎设计,尤其是涉及数据库迁移时,应用版本回滚不一定意味着数据结构也能回滚。
3. 增加 Nginx 反向代理
生产环境通常不会直接暴露 8300,可以在宿主机上用 Nginx 代理到本地端口:
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://127.0.0.1:8300;
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;
}
}
这样可以统一处理域名、HTTPS、访问日志、限流和网关层配置。
十六、总结
NestJS 使用 Docker 部署的关键不是单纯写一个 Dockerfile,而是把构建、运行、配置、日志、健康检查和发布脚本串成一条可靠链路。
当前的部署方案可以总结为:
-
Dockerfile使用多阶段构建,构建阶段生成dist/,运行阶段只安装生产依赖 -
docker-compose.yml负责加载环境变量、挂载日志目录、设置重启策略和健康检查 -
deploy.sh负责本地构建镜像、按远程架构选择平台、压缩上传、远程加载镜像、强制重建服务 -
/api/meta同时承担 Docker 健康检查和发布后验证的职责
对于单机 NestJS 服务来说,这套方案已经具备生产部署需要的核心能力:环境一致、流程可重复、失败可见、发布后可验证。后续如果接入镜像仓库、CI/CD、自动回滚和 Nginx HTTPS,就可以自然演进成更完整的生产发布体系。