从零开始用 Docker 部署 Node.js 后端服务
本文记录一套适合个人项目、小团队项目的 Docker 部署方案。目标是解决服务器 Node.js 版本太老、依赖安装环境不一致、上线流程容易遗漏文件等问题。
一、为什么要用 Docker 部署
很多老服务器系统版本比较低,比如 CentOS 7。直接在服务器上安装新版 Node.js、pnpm、项目依赖,经常会遇到下面这些问题:
- 服务器 Node.js 版本不够,项目跑不起来。
- 本地能安装依赖,服务器安装依赖失败。
- 原生依赖和系统环境有关,本地和服务器表现不一致。
- PM2、Node、pnpm、系统依赖都要单独维护。
- 换服务器后,又要重新配置一遍运行环境。
Docker 的思路是:把运行环境和项目一起打包成镜像。
本地构建好镜像后,服务器只需要安装 Docker,然后加载镜像并启动容器即可。这样服务器不用关心 Node.js 是 18、20 还是 22,因为 Node.js 已经被打进镜像里了。
二、整体部署流程
本文采用的流程如下:
text
本地电脑
1. 安装依赖
2. 构建项目 dist
3. 使用 Docker 构建 Linux 服务器可运行的镜像
4. docker save 导出镜像 tar
5. 打包部署目录
服务器
1. 安装 Docker
2. 上传部署包
3. 解压部署包
4. docker load 导入镜像
5. docker compose 启动服务
6. Nginx 反向代理到容器端口
最终上线时,只需要上传一个部署压缩包即可。
三、本地准备
本地需要安装:
- Node.js
- pnpm
- Docker Desktop
如果是 macOS,可以安装 Docker Desktop。安装后打开 Docker Desktop,再执行:
bash
docker -v
docker compose version
能看到版本号就说明本地 Docker 可以用了。
如果是 Windows,也可以安装 Docker Desktop。Windows 下建议使用 WSL2 后端,安装时勾选或开启 WSL2 相关选项。
安装完成后:
- 启动 Docker Desktop。
- 等待 Docker Desktop 显示 Docker Engine 已启动。
- 重新打开 PowerShell、CMD 或 Windows Terminal。
- 执行:
bash
docker -v
docker compose version
如果能看到 Docker 和 Docker Compose 版本号,就说明安装成功。
如果 Windows 提示 Docker Desktop 需要 WSL2,可以先安装 WSL2:
powershell
wsl --install
安装完成后重启电脑,再打开 Docker Desktop。
如果执行 docker -v 提示:
text
command not found: docker
macOS 一般是 Docker Desktop 没有启动,或者命令行工具还没加入 PATH。Windows 一般是 Docker Desktop 没启动、WSL2 没配置好,或者终端没有重新打开。先打开 Docker Desktop,再重新打开终端试一下。
四、先准备一个最小 Koa + Vite 后端示例
为了让整个 Docker 部署流程更清楚,先用一个最小后端项目做例子。这个项目只提供一个接口:
text
GET /test
返回:
json
{
"code": 200,
"message": "test ok"
}
1. 创建项目目录
bash
mkdir koa-docker-demo
cd koa-docker-demo
pnpm init
mkdir src
安装依赖:
bash
pnpm add koa koa-router
pnpm add -D typescript vite vite-plugin-node @types/node @types/koa @types/koa-router cross-env
2. 配置 package.json
把 package.json 调整成下面这样:
json
{
"name": "koa-docker-demo",
"version": "1.0.0",
"type": "module",
"main": "src/app.ts",
"scripts": {
"dev": "cross-env NODE_ENV=dev VITE_NODE_APP=true vite",
"build:dev": "cross-env NODE_ENV=dev vite build --mode dev",
"build:test": "cross-env NODE_ENV=test vite build --mode test",
"build:prod": "cross-env NODE_ENV=prod vite build --mode prod",
"start:prod": "cross-env NODE_ENV=prod node ./dist/app.js"
},
"dependencies": {
"koa": "^3.1.2",
"koa-router": "^14.0.0"
},
"devDependencies": {
"@types/koa": "^3.0.1",
"@types/koa-router": "^7.4.9",
"@types/node": "^25.3.2",
"cross-env": "^10.1.0",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vite-plugin-node": "^7.0.0"
}
}
这里有一个点容易让人疑惑:为什么 Node.js 后端也能用 Vite 打包?
原因是 Vite 不只可以打包前端,也可以配合 vite-plugin-node 打包 Node.js 服务端入口。最终会把 TypeScript 后端代码构建成可运行的 JavaScript 文件。
3. 配置 TypeScript
新增 tsconfig.json:
json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"types": ["node"]
},
"include": ["src/**/*.ts", "vite.config.ts"]
}
4. 配置 vite.config.ts
新增 vite.config.ts:
ts
import { defineConfig } from 'vite';
import { VitePluginNode } from 'vite-plugin-node';
export default defineConfig({
server: {
port: 9998,
open: false,
},
build: {
target: 'es2022',
outDir: 'dist',
lib: {
entry: 'src/app.ts',
formats: ['es'],
fileName: () => 'app.js',
},
rollupOptions: {
external: ['koa', 'koa-router'],
},
},
plugins: [
...VitePluginNode({
adapter: 'koa',
appPath: './src/app.ts',
exportName: 'viteNodeApp',
initAppOnBoot: false,
reloadAppOnFileChange: true,
tsCompiler: 'esbuild',
}),
],
});
这里最重要的是:
ts
lib: {
entry: 'src/app.ts',
fileName: () => 'app.js',
}
表示把 src/app.ts 打包成:
text
dist/app.js
Docker 最终启动的也是这个文件:
bash
node app.js
5. 写一个 test 接口
新增 src/app.ts:
ts
import Koa from 'koa';
import Router from 'koa-router';
const app = new Koa();
const router = new Router();
const port = 9998;
router.get('/test', (ctx) => {
ctx.body = {
code: 200,
message: 'test ok',
};
});
app.use(router.routes());
app.use(router.allowedMethods());
if (!process.env.VITE_NODE_APP) {
app.listen(port, () => {
console.log(`server running at http://localhost:${port}`);
});
}
export const viteNodeApp = app;
这里有一个判断:
ts
if (!process.env.VITE_NODE_APP) {
app.listen(...)
}
开发环境用 Vite 跑时,由 vite-plugin-node 接管启动;构建产物直接用 Node 跑时,再执行 app.listen。
6. 本地开发验证
启动开发服务:
bash
pnpm dev
访问接口:
bash
curl http://localhost:9998/test
正常会返回:
json
{
"code": 200,
"message": "test ok"
}
7. 本地打包验证
执行:
bash
pnpm build:prod
构建完成后会生成:
text
dist/app.js
直接用 Node 启动构建产物:
bash
node ./dist/app.js
再访问:
bash
curl http://localhost:9998/test
如果这里正常,说明这个后端项目已经具备了 Docker 部署的基础:
text
源码 src/app.ts
-> vite build
-> dist/app.js
-> node dist/app.js 可运行
后面的 Docker 部署,本质上就是把 Node.js 22、生产依赖和 dist/app.js 一起放进镜像里。
五、项目 Dockerfile
后端项目根目录增加 Dockerfile:
dockerfile
ARG NODE_IMAGE=node:22-alpine
ARG APP_ENV=prod
FROM ${NODE_IMAGE} AS base
WORKDIR /app
ENV PNPM_HOME=/pnpm
ENV PATH=$PNPM_HOME:$PATH
RUN corepack enable && corepack prepare pnpm@10.15.0 --activate
FROM base AS deps
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
FROM deps AS builder
COPY . .
ARG APP_ENV
RUN pnpm build:${APP_ENV}
FROM base AS prod-deps
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --prod --frozen-lockfile
FROM ${NODE_IMAGE} AS runner
WORKDIR /app
ARG APP_ENV
ENV NODE_ENV=${APP_ENV}
ENV TZ=Asia/Shanghai
COPY --from=prod-deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./
RUN mkdir -p logs sql/db public
EXPOSE 9998
CMD ["node", "--max-old-space-size=256", "app.js"]
这里用了多阶段构建:
deps:安装完整依赖。builder:执行项目构建。prod-deps:只安装生产依赖。runner:最终运行镜像,只保留生产依赖和构建产物。
六、docker-compose.yml
项目根目录增加 docker-compose.yml:
yaml
services:
my-node-api:
image: ${API_IMAGE}
container_name: my-node-api
restart: always
ports:
- "9998:9998"
environment:
NODE_ENV: ${APP_ENV}
TZ: Asia/Shanghai
volumes:
- ./logs:/app/logs
- ./sql/db:/app/sql/db
mem_limit: 512m
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
说明:
image使用环境变量,方便 dev、test、prod 共用同一个 compose 文件。container_name是容器名称,后续看日志、停止服务都会用到。restart: always表示 Docker 启动后自动拉起容器。ports把容器内的9998映射到服务器的9998。volumes把日志目录挂载出来,方便在服务器直接查看日志文件。logging限制 Docker 自身日志大小,避免日志无限增长。
七、.dockerignore
项目根目录增加 .dockerignore:
text
node_modules
dist
logs
.git
.DS_Store
.eslintcache
*.log
coverage
deploy
*.tar
*.tar.gz
tmp
temp
这样可以减少 Docker 构建上下文,避免把无用文件打进镜像。
八、package.json 脚本
示例:
json
{
"scripts": {
"build:dev": "cross-env NODE_ENV=dev vite build --mode dev",
"build:test": "cross-env NODE_ENV=test vite build --mode test",
"build:prod": "cross-env NODE_ENV=prod vite build --mode prod",
"docker:build:dev": "node ./scripts/docker-pack.mjs dev",
"docker:build:test": "node ./scripts/docker-pack.mjs test",
"docker:build:prod": "node ./scripts/docker-pack.mjs prod"
}
}
这里不在源码项目里写 docker:start,因为源码项目负责构建,服务器部署包负责启动。
九、一键打包脚本
创建 scripts/docker-pack.mjs:
js
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const appEnv = process.argv[2];
const envList = ['dev', 'test', 'prod'];
if (!envList.includes(appEnv)) {
console.error(`Usage: node ./scripts/docker-pack.mjs ${envList.join('|')}`);
process.exit(1);
}
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const rootDir = path.resolve(__dirname, '..');
const deployRoot = path.join(rootDir, 'deploy');
const packageDir = path.join(deployRoot, appEnv);
const imageTag = appEnv === 'prod' ? 'my-node-api:latest' : `my-node-api:${appEnv}`;
const packageName = `my-node-api-${appEnv}.tar.gz`;
const platform = process.env.DOCKER_PLATFORM || 'linux/amd64';
const defaultNodeImage = 'node:22-alpine';
const nodeImageCandidates = [
process.env.NODE_IMAGE,
defaultNodeImage,
'docker.1ms.run/library/node:22-alpine',
'docker.m.daocloud.io/library/node:22-alpine',
'dockerproxy.net/library/node:22-alpine',
].filter(Boolean);
const dockerCommand = fs.existsSync('/Applications/Docker.app/Contents/Resources/bin/docker')
? '/Applications/Docker.app/Contents/Resources/bin/docker'
: 'docker';
const run = (command, args, options = {}) => {
execFileSync(command, args, {
cwd: rootDir,
stdio: 'inherit',
...options,
});
};
const dockerBuild = () => {
let lastError;
const candidates = [...new Set(nodeImageCandidates)];
for (const nodeImage of candidates) {
try {
console.log(`\nDocker base image: ${nodeImage}`);
console.log(`Docker image platform: ${platform}\n`);
run(dockerCommand, [
'build',
'--platform',
platform,
'--build-arg',
`NODE_IMAGE=${nodeImage}`,
'--build-arg',
`APP_ENV=${appEnv}`,
'-t',
imageTag,
'.',
]);
return nodeImage;
} catch (error) {
lastError = error;
console.warn(`\nDocker base image failed: ${nodeImage}`);
console.warn('Trying next image source...\n');
}
}
console.error('\nDocker build failed after trying all Node image sources.');
console.error('You can retry with a specific image source, for example:');
console.error(`NODE_IMAGE=docker.1ms.run/library/node:22-alpine pnpm docker:build:${appEnv}`);
throw lastError;
};
fs.rmSync(packageDir, { recursive: true, force: true });
fs.mkdirSync(packageDir, { recursive: true });
run('pnpm', [`build:${appEnv}`]);
const usedNodeImage = dockerBuild();
run(dockerCommand, ['save', imageTag, '-o', path.join(packageDir, 'my-node-api.tar')]);
fs.copyFileSync(path.join(rootDir, 'docker-compose.yml'), path.join(packageDir, 'docker-compose.yml'));
fs.cpSync(path.join(rootDir, 'dist'), path.join(packageDir, 'dist'), {
recursive: true,
force: true,
});
fs.writeFileSync(
path.join(packageDir, 'package.json'),
JSON.stringify(
{
name: `my-node-api-${appEnv}-deploy`,
private: true,
scripts: {
'docker:start':
`docker load -i my-node-api.tar && API_IMAGE=${imageTag} APP_ENV=${appEnv} docker compose up -d`,
'docker:stop':
`API_IMAGE=${imageTag} APP_ENV=${appEnv} docker compose down && docker rm -f my-node-api || true`,
'docker:logs': 'docker logs -f my-node-api',
},
},
null,
2,
),
);
run('tar', ['-czf', path.join(deployRoot, packageName), '-C', packageDir, '.']);
console.log(`\nDocker deploy package created: deploy/${packageName}`);
console.log(`Docker image platform: ${platform}`);
console.log(`Docker base image: ${usedNodeImage}\n`);
这个脚本做了几件事:
- 执行
pnpm build:dev/test/prod。 - 构建 Docker 镜像。
- 默认构建
linux/amd64镜像,避免 Apple Silicon 本地构建出的 arm64 镜像放到 x86 服务器无法运行。 - 如果 Docker Hub 拉取
node:22-alpine超时,自动尝试备用镜像源。 - 使用
docker save导出镜像。 - 生成服务器部署包。
- 在部署包里生成一个极简
package.json,服务器只需要执行npm run docker:start。
十、本地打包
进入后端项目目录:
bash
cd /path/to/my-node-api
pnpm docker:build:dev
生产环境:
bash
pnpm docker:build:prod
构建完成后会生成:
text
deploy/my-node-api-dev.tar.gz
deploy/my-node-api-prod.tar.gz
如果本地是 Apple Silicon,但是服务器是 x86_64,默认会构建 linux/amd64 镜像。
如果服务器也是 arm64,可以这样指定:
bash
DOCKER_PLATFORM=linux/arm64 pnpm docker:build:prod
如果 Docker Hub 网络不好,可以手动指定镜像源:
bash
NODE_IMAGE=docker.1ms.run/library/node:22-alpine pnpm docker:build:prod
十一、不用脚本时,手动部署流程怎么做
一键脚本只是把下面这些命令自动执行了一遍。理解这几步后,Docker 部署就不会那么迷糊。
1. 先构建项目
bash
pnpm build:prod
这一步会生成后端构建产物:
text
dist/
如果只是传统 Node.js 部署,可能会直接把 dist 上传到服务器,然后在服务器上执行:
bash
node ./app.js
但是这种方式仍然依赖服务器自己的 Node.js 版本和依赖环境。
2. 构建 Docker 镜像
bash
docker build --platform linux/amd64 --build-arg APP_ENV=prod -t my-node-api:latest .
这一步会在本地 Docker 里生成一个镜像:
text
my-node-api:latest
可以这样查看:
bash
docker images
这里需要注意:
--platform linux/amd64:表示构建给 x86_64 Linux 服务器用的镜像。--build-arg APP_ENV=prod:把环境传给 Dockerfile,用来执行pnpm build:prod。-t my-node-api:latest:给镜像起名字和标签。- 最后的
.:表示 Dockerfile 和构建上下文在当前目录。
如果本地是 Mac M 系列芯片,而服务器是普通 x86 云服务器,这个 --platform linux/amd64 很重要。不加的话,可能本地构建出来的是 arm64 镜像,上传到服务器后会报:
text
exec format error
3. 导出 Docker 镜像
Docker 镜像默认只存在于本机 Docker 里,不是一个普通文件。要上传到服务器,需要先导出成 tar 文件:
bash
docker save my-node-api:latest -o my-node-api.tar
这一步会得到:
text
my-node-api.tar
可以理解为:把本地 Docker 里的 my-node-api:latest 镜像导出成一个文件。
4. 准备部署目录
手动整理一个部署目录:
bash
mkdir -p deploy/prod
cp my-node-api.tar deploy/prod/
cp docker-compose.yml deploy/prod/
cp -r dist deploy/prod/
然后在 deploy/prod 里放一个极简 package.json,方便服务器执行命令:
json
{
"private": true,
"scripts": {
"docker:start": "docker load -i my-node-api.tar && API_IMAGE=my-node-api:latest APP_ENV=prod docker compose up -d",
"docker:stop": "API_IMAGE=my-node-api:latest APP_ENV=prod docker compose down && docker rm -f my-node-api || true",
"docker:logs": "docker logs -f my-node-api"
}
}
部署目录最终大概长这样:
text
deploy/prod/
my-node-api.tar
docker-compose.yml
package.json
dist/
其中:
my-node-api.tar:Docker 镜像文件,服务器用它导入镜像。docker-compose.yml:容器启动配置。package.json:只是为了提供几个启动脚本。dist/:原始构建产物,方便排查问题或临时用宿主机 Node 启动。
5. 打成一个压缩包
bash
tar -czf my-node-api-prod.tar.gz -C deploy/prod .
得到:
text
my-node-api-prod.tar.gz
这个文件就是最终要上传到服务器的部署包。
6. 上传到服务器
bash
scp my-node-api-prod.tar.gz root@your-server-ip:/data/my-node-api/
7. 服务器解压
bash
cd /data/my-node-api
tar -zxf my-node-api-prod.tar.gz
8. 服务器导入镜像
bash
docker load -i my-node-api.tar
这一步会把 my-node-api.tar 导入到服务器 Docker 里。导入后可以查看:
bash
docker images
能看到:
text
my-node-api latest
9. 启动容器
bash
API_IMAGE=my-node-api:latest APP_ENV=prod docker compose up -d
这里的环境变量会传给 docker-compose.yml:
yaml
image: ${API_IMAGE}
environment:
NODE_ENV: ${APP_ENV}
所以实际效果就是:
text
使用 my-node-api:latest 镜像
用 prod 环境启动服务
10. 查看服务状态
bash
docker ps
查看日志:
bash
docker logs -f my-node-api
11. 一键脚本到底省了什么
前面的手动流程完整写下来就是:
bash
pnpm build:prod
docker build --platform linux/amd64 --build-arg APP_ENV=prod -t my-node-api:latest .
docker save my-node-api:latest -o deploy/prod/my-node-api.tar
cp docker-compose.yml deploy/prod/docker-compose.yml
cp -r dist deploy/prod/dist
生成 deploy/prod/package.json
tar -czf deploy/my-node-api-prod.tar.gz -C deploy/prod .
一键脚本只是把这些步骤串起来,并且补了一些兜底逻辑:
- 自动区分
dev、test、prod。 - 自动指定
linux/amd64。 - 自动尝试多个 Node 镜像源。
- 自动生成服务器部署包。
- 自动生成服务器用的
package.json启动脚本。
所以一键打包命令:
bash
pnpm docker:build:prod
本质上就是自动完成了本章的手动步骤。
十二、服务器安装 Docker
下面以 CentOS 7 为例。
先确认系统:
bash
cat /etc/redhat-release
uname -m
安装依赖:
bash
yum install -y yum-utils device-mapper-persistent-data lvm2
添加 Docker yum 源。如果官方源连接失败,可以使用国内镜像源:
bash
yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
安装 Docker:
bash
yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
启动并设置开机自启:
bash
systemctl enable docker
systemctl start docker
验证:
bash
docker version
docker compose version
如果能看到 Docker 和 Docker Compose 的版本号,就说明安装成功。
十三、上传部署包
假设服务器部署目录是 /data/my-node-api:
bash
ssh root@your-server-ip
mkdir -p /data/my-node-api
本地上传:
bash
scp deploy/my-node-api-prod.tar.gz root@your-server-ip:/data/my-node-api/
也可以用任意自己习惯的上传方式。
十四、服务器启动
进入服务器目录:
bash
cd /data/my-node-api
tar -zxf my-node-api-prod.tar.gz
npm run docker:start
注意:这里可以用 npm run,不一定非要用 pnpm。因为部署包里的 package.json 只负责执行 Docker 命令,不需要安装项目依赖。
查看容器状态:
bash
docker ps
看到类似下面的状态,就说明容器已经启动:
text
CONTAINER ID IMAGE STATUS PORTS
xxxxxx my-node-api:latest Up 10 seconds 0.0.0.0:9998->9998/tcp
查看日志:
bash
npm run docker:logs
停止服务:
bash
npm run docker:stop
十五、Nginx 反向代理
假设后端容器暴露的是 9998 端口,公网希望通过:
text
https://example.com/api/version
访问后端内部接口:
text
http://127.0.0.1:9998/version
那么 Nginx 可以这样配置:
nginx
location /api/ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-Nginx-Proxy true;
proxy_set_header Connection "";
proxy_pass http://127.0.0.1:9998/;
proxy_redirect default;
}
这里最关键的是:
nginx
location /api/ {
proxy_pass http://127.0.0.1:9998/;
proxy_pass 后面带 /,表示转发时去掉 /api/ 前缀。
所以:
text
/api/version -> /version
/api/user/login -> /user/login
修改 Nginx 后检查配置:
bash
nginx -t
systemctl reload nginx
测试:
bash
curl http://127.0.0.1:9998/version
curl https://example.com/api/version
十六、常用 Docker 命令
查看运行中的容器:
bash
docker ps
查看所有容器,包括已经退出的:
bash
docker ps -a
查看某个容器日志:
bash
docker logs -f my-node-api
查看容器状态:
bash
docker inspect my-node-api --format '状态={{.State.Status}} 重启次数={{.RestartCount}} 启动时间={{.State.StartedAt}}'
停止并删除容器:
bash
docker rm -f my-node-api
查看本机镜像:
bash
docker images
删除镜像:
bash
docker rmi my-node-api:latest
十七、常见问题
1. Apple Silicon 本地构建,服务器运行时报 exec format error
错误类似:
text
exec /usr/local/bin/docker-entrypoint.sh: exec format error
原因是本地 Mac 构建出了 linux/arm64 镜像,但服务器是 linux/amd64。
解决方法:构建时指定平台。
bash
docker build --platform linux/amd64 -t my-node-api:latest .
本文的一键打包脚本已经默认使用 linux/amd64。
2. Docker Hub 超时
错误类似:
text
failed to fetch anonymous token
Client.Timeout exceeded while awaiting headers
原因是访问 Docker Hub 网络不稳定。
解决方法:换镜像源。
bash
NODE_IMAGE=docker.1ms.run/library/node:22-alpine pnpm docker:build:prod
也可以先手动拉取:
bash
docker pull --platform linux/amd64 node:22-alpine
3. 容器名称已经被占用
错误类似:
text
Conflict. The container name "/my-node-api" is already in use
说明之前已经启动过同名容器。
解决方法:
bash
npm run docker:stop
或者手动删除:
bash
docker rm -f my-node-api
4. 容器启动了,但接口访问不通
先看容器状态:
bash
docker ps
再看日志:
bash
docker logs -f my-node-api
然后在服务器本机测试:
bash
curl http://127.0.0.1:9998/version
如果本机能访问,公网不能访问,通常是 Nginx 配置、防火墙、安全组的问题。
5. 容器里连接数据库失败
如果数据库在宿主机或者其他服务器,需要注意:
- 容器里的
localhost指的是容器自己,不一定是宿主机。 - 数据库账号需要允许当前来源 IP 连接。
- 云服务器还要检查安全组端口。
如果 MySQL 报:
text
Host 'xxx' is not allowed to connect to this MySQL server
说明 MySQL 用户授权不允许这个来源连接,需要给对应用户授权。
十八、前端是否需要 Docker
普通前端项目一般不需要 Docker。
前端本地构建后得到的是静态文件:
bash
pnpm build:prod
然后把 dist 上传到服务器 Nginx 静态目录即可。
前端产物是 HTML、CSS、JS,不依赖服务器 Node.js 版本。真正需要固定 Node.js 运行环境的是后端服务。
十九、最终发布命令总结
本地:
bash
cd /path/to/my-node-api
pnpm docker:build:prod
scp deploy/my-node-api-prod.tar.gz root@your-server-ip:/data/my-node-api/
服务器:
bash
cd /data/my-node-api
tar -zxf my-node-api-prod.tar.gz
npm run docker:start
npm run docker:logs
停止:
bash
npm run docker:stop
查看状态:
bash
docker ps
到这里,一个 Node.js 后端服务就可以通过 Docker 完成部署了。