部署
本指南涵盖将 AdonisJS 应用部署到生产环境。你将学习如何:
- 理解独立构建(standalone build),以及为何生产环境不需要你的源文件
- 在构建和运行时正确配置
NODE_ENV - 使用
node ace build命令创建生产构建 - 配置要复制到构建输出的静态文件
- 在生产环境中运行你的应用
- 使用持久化存储处理用户上传的文件
- 为生产环境配置日志
- 在生产环境中安全地运行数据库迁移
- 使用多阶段 Dockerfile 创建 Docker 镜像
概述
AdonisJS 应用使用 TypeScript 编写,在运行于生产环境之前必须编译为 JavaScript。构建过程会创建一个独立构建,这意味着编译后的输出包含了运行你的应用所需的全部内容,而无需原始的 TypeScript 源文件。
由于 AdonisJS 应用运行在 Node.js 运行时之上,你的部署平台必须支持 Node.js 24 或更高版本。构建过程会编译你的 TypeScript 代码、打包前端资源(如果使用 Vite),并将必要的文件复制到一个 build 目录,你可以直接将其部署到生产服务器。
理解独立构建
独立构建是你的 AdonisJS 应用的编译输出。创建构建后,你只需要将 build 目录部署到生产服务器即可。原始的源文件、开发依赖和 TypeScript 配置在生产环境中都不需要。
这种方法有几个好处。部署体积显著减小,因为你不会打包 TypeScript 源文件或开发工具。生产环境只需要 JavaScript 运行时,你可以把 build 文件夹当作一个独立的、自包含的应用来对待。
构建与运行时的 NODE_ENV
NODE_ENV 环境变量在构建过程中与运行时的行为不同,理解这一区别对于成功部署非常重要。
在构建过程中,你需要安装开发依赖,因为构建工具(TypeScript 编译器、Vite 以及其他构建工具)通常作为 devDependencies 列在你的 package.json 中。如果你在 CI 环境或尚未安装依赖的沙箱中进行构建,请在运行 npm install 之前将 NODE_ENV 设置为 development,以确保所有依赖都可用。
# In CI/CD or fresh environments
NODE_ENV=development npm install
npm run build
在生产环境运行时,NODE_ENV 应设置为 production。这会启用生产优化,并禁用仅用于开发的功能(如详细的错误页面)。
创建生产构建
从你的项目根目录运行构建命令。
npm run build
这会在底层执行 node ace build。构建过程按以下顺序执行这些步骤:
- 如果已存在
./build文件夹,则将其删除 - 重写
ace.js文件以移除 TypeScript loader 的导入 - 使用 Vite 编译前端资源(如果已配置)
- 使用
tsc将 TypeScript 源代码编译为 JavaScript - 将
metaFiles数组中注册的、非 TypeScript 的文件复制到./build - 将
package.json和你的包管理器 lock 文件复制到./build
如果你的代码中存在 TypeScript 错误,构建将会失败。你必须先修复这些错误,才能创建生产构建。如果你需要临时绕过 TypeScript 错误,可以使用 --ignore-ts-errors 标志。
npm run build -- --ignore-ts-errors
--package-manager 标志控制将哪个 lock 文件复制到构建输出中。如果未指定,构建命令会根据你调用命令的方式(例如 npm run build 与 pnpm build)来检测你的包管理器。
npm run build -- --package-manager=pnpm
构建输出内容
构建成功后,build 目录包含你已编译的应用。你会在其中找到以下内容:
- 反映你源目录结构的已编译 JavaScript 文件
- 用于安装生产依赖的
package.json和 lock 文件 - 在
metaFiles中配置的静态文件及其他资源 build/public中的前端资源(针对 Vite 驱动的应用)
环境文件(.env、.env.example)会被有意排除在构建输出之外。环境变量无法在不同环境之间移植,你必须通过托管平台的环境变量管理功能为每个部署目标单独配置它们。
静态文件
需要包含在生产构建中的静态文件,通过使用 adonisrc.ts 文件中的 metaFiles 数组进行配置。这些是你的应用在运行时所需的非 TypeScript 文件,例如 Edge 模板或公共资源。
{
metaFiles: [
{
pattern: 'resources/views/**/*.edge',
reloadServer: false,
},
{
pattern: 'public/**',
reloadServer: false,
},
],
}
pattern 属性接受 glob 模式来匹配文件。reloadServer 属性控制文件更改是否会在开发期间触发服务器重启,它对生产构建没有影响。
对于 Hypermedia 和 Inertia 应用,Vite 会编译前端资源并将其放置在 public 目录中。随后这些资源会在构建过程中被复制到 build/public。
调整 keepAliveTimeout 以适应反向代理与 node 负载均衡器
真实世界中的应用通常不会被直接访问,而是位于反向代理与 node 负载均衡器(例如 Nginx)之后。
默认情况下,Node.js 会在 5 秒后关闭空闲连接,但 Nginx 可能尝试将它们保持打开 60 秒以上。当 Nginx 试图复用一个它认为仍处于打开状态的旧连接,但 Node.js 已经悄悄将其关闭时,Nginx 会抛出 502 Bad Gateway 错误。
为避免此问题,你需要将 AdonisJS 服务器的 keepAliveTimeout 调整为大于 Nginx 的 proxy_read_timeout(默认 50 秒)。
{
keepAliveTimeout: 55000,
}
在生产环境中提供静态文件
虽然 AdonisJS 内置了一个 静态文件服务器 ,但在生产环境中,你应该将静态文件服务交给专用工具来处理。你的 Node.js 进程处理的每一个静态文件请求,都是一笔本可用于动态工作的请求。反向代理或 CDN 就是为此而生,它们能以更少的资源消耗更快地交付文件。
根据你的基础设施,你有两种主要选择。
反向代理(Nginx、Caddy、Traefik、Apache)
配置你的反向代理直接为 build/public 目录提供静态文件请求,并将其他所有请求转发到你的 AdonisJS 服务器。这样,静态文件永远不会到达你的 Node.js 进程。
使用 Nginx 时,你可以添加一个 location 块,优先尝试从 public 目录提供文件,并回退到 AdonisJS 服务器来处理动态路由。
server {
listen 80;
server_name example.com;
root /path/to/your/app/build/public;
location / {
try_files $uri @adonis;
}
location @adonis {
proxy_pass http://localhost:3333;
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;
}
}
CDN
为了获得最佳性能,请将你编译后的资源上传到 CDN。这需要更新 Vite 配置中的 assetsUrl 选项,使生成的资源 URL 指向你的 CDN 而非应用服务器。
{
assetsUrl: 'https://cdn.example.com/assets',
}
每次构建后,将 build/public 的内容部署到你的 CDN。你的 CI/CD 流水线可以自动化这一步。
你可以将两种方法结合使用。使用反向代理来提供 favicon.ico 和 robots.txt 等来自 public 目录的文件,并使用 CDN 来提供从全球分发中获益的 Vite 编译资源(JavaScript、CSS、图片)。
运行生产构建
要在生产环境中运行你的应用,请将 build 目录视为应用的根目录。进入 build 目录,安装生产依赖,并启动服务器。
cd build
npm ci --omit=dev
node bin/server.js
使用 npm ci --omit=dev 而非 npm install,可以确保使用 lock 文件进行干净、可复现的安装,并跳过开发依赖。你还必须在启动服务器之前提供所有必需的环境变量。你的托管平台应该具备配置环境变量的机制。你可以参考你的 env.ts 文件或 .env.example 来查看应用需要哪些变量。
用户上传的文件
用户上传的文件需要能够跨部署存活的持久化存储。当你部署新版本的应用时,之前的 build 目录通常会被替换,这意味着存储在该目录内的任何文件都会丢失。
对于生产部署,请使用云存储提供商,例如 Amazon S3、Cloudflare R2 或 Google Cloud Storage。这些服务提供了与应用部署相独立的、持久的可靠存储。
如果你必须将文件存储在服务器本地,请配置一个位于应用 build 文件夹之外的目录,并确保该目录在部署之间得以保留。具体方法取决于你的托管平台和部署策略。
日志
AdonisJS 使用 Pino 进行日志记录,它以 NDJSON 格式将结构化日志输出到 stdout。这种格式非常适合生产环境,因为大多数日志服务和日志聚合器都能直接解析 NDJSON。
配置你的托管平台以捕获应用进程的 stdout。大多数平台都提供与 Datadog、Logtail 或 Papertrail 等日志服务的内置集成。或者,你也可以将应用的输出管道传输到日志采集代理。
Pino 的结构化 JSON 输出包含时间戳、日志级别以及你附加到日志消息的任何额外上下文,让你在日志服务中搜索和筛选日志变得轻而易举。
数据库迁移
在生产环境中使用 migration:run 命令并带上 --force 标志来运行迁移。
node ace migration:run --force
需要 --force 标志是因为在生产环境中运行迁移默认是禁用的。这是一项安全措施,用于防止意外的 schema 变更。
AdonisJS 迁移是幂等的,并使用排他锁。如果多个进程同时尝试运行迁移(例如,在滚动部署期间),只有一个进程会执行迁移,而其他进程会等待。这可以防止竞态条件和重复的迁移尝试。
如果你的部署平台支持生命周期钩子,或者有专门用于运行数据库迁移的机制,请使用该机制,而不是在应用启动时运行迁移。这种分离确保了在任何应用实例开始提供服务之前,迁移都已成功完成。
在生产环境中回滚迁移默认是禁用的,也不建议这样做。与其回滚,不如创建一个新迁移来撤销你需要撤消的变更。这种方法保留了所有 schema 变更的清晰历史记录,并避免了生产环境回滚相关的风险。
Dockerfile
以下 Dockerfile 使用多阶段构建创建一个优化后的生产镜像。第一阶段安装所有依赖并创建构建,而最终阶段只包含生产运行时。
FROM node:lts-bookworm-slim AS base
# ----------------------------
# Stage 1: Install all dependencies
# ----------------------------
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
# ----------------------------
# Stage 2: Build the application
# ----------------------------
FROM deps AS build
WORKDIR /app
COPY . .
RUN node ace build
# ----------------------------
# Stage 3: Production runtime
# ----------------------------
FROM base AS production
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/build ./
RUN npm ci --omit=dev
EXPOSE 3333
CMD ["node", "bin/server.js"]
这个 Dockerfile 遵循多阶段模式,每个阶段都有特定用途。deps 阶段安装构建所需的所有依赖。build 阶段编译 TypeScript 应用。production 阶段创建一个仅包含生产依赖的最小镜像。
要构建并运行 Docker 镜像:
docker build -t my-adonis-app .
docker run -p 3333:3333 --env-file .env my-adonis-app
使用 --env-file 标志或单独的 -e 标志传入环境变量。容器默认暴露 3333 端口,你可以将其映射到任意主机端口。
使用 Docker 运行迁移
对于容器化部署,你有两种运行迁移的选择。你可以在启动应用容器之前,将其作为单独的命令运行:
docker run --rm --env-file .env my-adonis-app node ace migration:run --force
或者,你可以创建一个入口点(entrypoint)脚本,在启动服务器之前运行迁移。在你的项目根目录创建此文件:
import { execSync } from 'node:child_process'
/**
* Run migrations before starting the server
* when the MIGRATE environment variable is set.
*/
if (process.env.MIGRATE === 'true') {
console.log('Running migrations...')
execSync('node ace migration:run --force', { stdio: 'inherit' })
}
/**
* Start the server
*/
console.log('Starting server...')
await import('./bin/server.js')
更新你的 Dockerfile 以使用此入口点:
FROM node:lts-bookworm-slim AS base
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM deps AS build
WORKDIR /app
COPY . .
RUN node ace build
FROM base AS production
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/build ./
RUN npm ci --omit=dev
# Copy the entrypoint script
COPY docker-entrypoint.js ./
EXPOSE 3333
CMD ["node", "bin/server.js"]
CMD ["node", "docker-entrypoint.js"]
通过这种设置,将 MIGRATE=true 设置为环境变量后,会在启动服务器之前运行迁移。对于多个容器同时启动的水平扩展场景,迁移锁确保只有一个容器运行迁移,而其他容器等待。