部署 Bun 应用
本教程共 34 篇 · 第 32 篇 · 更新于 2026-08-06
本节目标:
- 掌握部署前必须处理的三个通用问题:端口与监听地址、锁文件与依赖裁剪、产物形态的选择。
- 能写出一份生产可用的 Bun
Dockerfile(含多阶段构建与.dockerignore)。- 了解 Vercel / Railway / Render / Google Cloud Run / AWS Lambda / DigitalOcean 各自的接入方式与已知限制。
- 会用 systemd、PM2 或
bun build --compile把服务跑在自有服务器上。- 拿到一份可直接照做的上线前检查清单。
32.1 部署前的三个通用前提
不管最终落到哪个平台,下面三件事都要先想清楚。跳过它们,几乎所有「本地跑得好好的,一上线就 502」都源于此。
端口与监听地址
Bun.serve() 的默认行为很重要:
- 端口:依次读取
$BUN_PORT、$PORT、$NODE_PORT环境变量,都没有时才用3000。 - 监听地址(hostname):默认
0.0.0.0。
// server.ts
Bun.serve({
port: 8080, // 不写则回落到 $BUN_PORT / $PORT / $NODE_PORT / 3000
hostname: "0.0.0.0", // 默认值,容器与云平台里必须是它
fetch() {
return new Response("ok");
},
});
绝大多数 PaaS(Railway、Render、Cloud Run、Fly.io、App Platform)都会注入 PORT 环境变量并要求你监听它。Bun.serve 默认就读 PORT,所以不写 port 反而是最稳妥的做法。
Warning如果你用的是 Express、Hono 等框架,务必确认它们没有把监听地址写死成
127.0.0.1。容器里绑定127.0.0.1意味着外部流量永远进不来,平台健康检查会一直失败。Express 的写法应是app.listen(port)或app.listen(port, "0.0.0.0")。
锁文件与依赖裁剪
生产构建里始终带上 --frozen-lockfile。它会拒绝任何修改 bun.lock 的安装行为,保证线上装到的依赖和本地一模一样:
bun install --frozen-lockfile
如果运行时不需要开发依赖(测试框架、类型定义、构建插件),再加 --production 裁掉 devDependencies:
bun install --frozen-lockfile --production
两个参数组合起来,既缩小镜像体积,又让构建结果可复现。前提是 bun.lock 必须提交到版本库,否则 --frozen-lockfile 直接报错。
产物形态:三选一
Bun 上线有三种形态,按「构建复杂度递增、运行开销递减」排列:
| 形态 | 做法 | 适合场景 |
|---|---|---|
| 直接跑源码 | bun run index.ts | 中小服务、部署最简单,Bun 每次启动即时转译 TS |
| 打包后跑 | bun build --target=bun --production --outdir=dist ./src/index.ts 再 bun dist/index.js | 依赖多、想减少运行时解析开销与镜像体积 |
| 编译成单文件 | bun build --compile ./src/index.ts --outfile server | 无 Bun 环境的机器、CLI 分发、极简镜像 |
# 形态二:打包成单个 JS 产物
bun build --target=bun --production --outdir=dist ./server/index.ts
NODE_ENV=production bun dist/index.js
# 形态三:编译成不依赖 Bun 的独立可执行文件(详见第 19 章)
bun build ./src/server.ts --compile --minify --outfile ./dist/server
./dist/server
Tip三种形态并非越复杂越好。先用第一种上线,等镜像体积或冷启动成为瓶颈时再切换。Bun 的即时转译速度足够快,源码直跑在绝大多数场景下没有明显代价。
32.2 Docker:最通用的部署底座
Bun 官方在 Docker Hub 维护了 oven/bun 镜像,可用的主要变体:
docker pull oven/bun # 默认(Debian)
docker pull oven/bun:slim # 精简版
docker pull oven/bun:alpine # Alpine 基础镜像
docker pull oven/bun:distroless # 无 shell、无包管理器,攻击面最小
标签里 oven/bun:1 表示跟随 1.x 最新版;生产环境建议锁到确定版本(如 oven/bun:1.3.14),避免基础镜像自动升级带来的意外。
生产级多阶段 Dockerfile
官方指南的模板把「装依赖」拆成开发依赖、生产依赖两份缓存,构建完成后只把生产依赖搬进最终镜像:
# 使用官方 Bun 镜像,所有版本见 https://hub.docker.com/r/oven/bun/tags
FROM oven/bun:1 AS base
WORKDIR /usr/src/app
# 把依赖装到临时目录,便于 Docker 层缓存,加速后续构建
FROM base AS install
RUN mkdir -p /temp/dev
COPY package.json bun.lock /temp/dev/
RUN cd /temp/dev && bun install --frozen-lockfile
# 再装一份仅含生产依赖的(排除 devDependencies)
RUN mkdir -p /temp/prod
COPY package.json bun.lock /temp/prod/
RUN cd /temp/prod && bun install --frozen-lockfile --production
# 用完整依赖跑测试与构建
FROM base AS prerelease
COPY --from=install /temp/dev/node_modules node_modules
COPY . .
ENV NODE_ENV=production
RUN bun test
RUN bun run build
# 最终镜像只带生产依赖与必要文件
FROM base AS release
COPY --from=install /temp/prod/node_modules node_modules
COPY --from=prerelease /usr/src/app/index.ts .
COPY --from=prerelease /usr/src/app/package.json .
USER bun
EXPOSE 3000/tcp
ENTRYPOINT [ "bun", "run", "index.ts" ]
几个值得注意的细节:
COPY package.json bun.lock单独成层,只要依赖没变,后续构建就能命中缓存,不必重装。USER bun:镜像内置了非 root 的bun用户,生产容器不要用 root 跑应用。bun test放在构建阶段,测试不过就构建失败,相当于免费的发布闸门。
如果采用「打包后跑」的形态,最终阶段可以换成更小的 slim 镜像:
FROM oven/bun:1 AS base
WORKDIR /usr/src/app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun build --target=bun --production --outdir=dist ./server/index.ts
FROM oven/bun:1-slim
WORKDIR /usr/src/app
COPY --from=base /usr/src/app/dist ./
COPY --from=base /usr/src/app/public ./public
EXPOSE 3000
CMD ["bun", "index.js"]
.dockerignore 别忘了
它的语法和 .gitignore 一致,作用是把无关文件挡在构建上下文之外——尤其是 node_modules,漏掉它会让构建上下文暴涨并可能把本机架构的原生模块带进镜像:
node_modules
Dockerfile*
docker-compose*
.dockerignore
.git
.gitignore
README.md
LICENSE
.vscode
Makefile
helm-charts
.env
.editorconfig
.idea
coverage*
构建与运行
# 构建镜像,--pull 拉取最新基础镜像
docker build --pull -t bun-hello-world .
# 后台运行,映射容器 3000 端口到宿主机 3000
docker run -d -p 3000:3000 bun-hello-world
# 查看正在运行的容器
docker ps
# 停止
docker stop <container-id>
Note在 Apple Silicon(M 系列)上构建、部署到 x86 云主机时,必须显式指定平台,否则产出的是 ARM64 镜像,云端跑不起来:
docker buildx build --platform=linux/amd64 -t <tag> --push .
一份 Dockerfile 通吃容器平台
把 Docker 放在最前面讲,是因为它是覆盖面最广的一条路。凡是接受容器镜像或 Dockerfile 的平台——Fly.io、Koyeb、Kubernetes、各家自建 PaaS——用的都是上面这套东西,差别只在部署命令和端口约定。
下一节要讲的 Cloud Run、AWS Lambda、DigitalOcean 也都属于这一类。真正需要单独学的,只有 Vercel、Railway、Render 这几个「原生识别 Bun」的平台。
32.3 主流平台接入
Vercel
Vercel 已原生支持 Bun 运行时。在 vercel.json 里声明版本即可:
{
"bunVersion": "1.x"
}
值必须写 "1.x",具体小版本由 Vercel 内部决定。为了减少行为差异,建议本地 Bun 版本与平台保持在同一条版本线上。
Next.js 项目还需要把脚本改为通过 Bun 执行:
{
"scripts": {
"dev": "bun --bun next dev",
"build": "bun --bun next build"
}
}
--bun 的作用是强制 Next.js CLI 跑在 Bun 上(打包环节仍由 Turbopack 或 Webpack 负责,不受影响)。
部署命令:
# 无需全局安装
bunx vercel login
bunx vercel deploy
验证线上确实跑在 Bun 上:
console.log("runtime", process.versions.bun);
WarningVercel 上的 Bun 运行时目前处于 Beta,且有两条硬性限制:
Bun.serve在 Vercel Functions 上不受支持,需要改用 Vercel 支持的框架(Next.js、Express、Hono、Nitro 等);此外自动 source map、字节码缓存、node:http/https的指标采集尚未支持。若要在 Bun 项目里使用 Routing Middleware,需要把中间件运行时显式设为nodejs:export const config = { runtime: "nodejs" };
Railway
Railway 从 GitHub 零配置部署,自动处理 SSL 并可一键挂 PostgreSQL。CLI 路径:
bun install -g @railway/cli
railway login
railway init
# 需要数据库时(必须先建数据库,再建服务)
railway add --database postgres
railway add --service my-bun-app --variables DATABASE_URL=\${{Postgres.DATABASE_URL}}
# 部署并生成公网域名(服务默认只在私有网络内可达)
railway up
railway domain
默认使用 Nixpacks 构建。官方指出 Railpack 对 Bun 的支持更好,且总是跟进最新 Bun 版本,推荐显式切换:
{
"$schema": "https://railway.com/railway.schema.json",
"build": {
"builder": "RAILPACK"
}
}
Render
Render 原生支持 Bun,可部署为 Web Service、后台 Worker 或定时任务。在创建 Web Service 时填三个值:
| 配置项 | 值 |
|---|---|
| Runtime | Node |
| Build Command | bun install |
| Start Command | bun app.ts |
Runtime 选 Node 不是笔误——Render 用这一环境识别 JS 项目,实际执行仍由你填的 bun 命令决定。
Google Cloud Run
Cloud Run 走容器路线,准备好 Dockerfile 后可以直接从源码部署,构建过程由 Cloud Build 完成:
FROM oven/bun:latest
COPY package.json bun.lock ./
RUN bun install --production --frozen-lockfile
COPY . .
CMD ["bun", "index.ts"]
gcloud init
# 启用所需服务并授予 Cloud Build 权限
gcloud services enable run.googleapis.com cloudbuild.googleapis.com
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member=serviceAccount:$PROJECT_NUMBER-compute@developer.gserviceaccount.com \
--role=roles/run.builder
# 从本地源码构建并部署
gcloud run deploy my-bun-app --source . --region=us-west1 --allow-unauthenticated
部署成功后会输出形如 https://my-bun-app-xxx.us-west1.run.app 的服务地址。
AWS Lambda
Lambda 本身不提供 Bun 运行时,官方方案是容器镜像 + AWS Lambda Web Adapter:把普通的 HTTP 服务包进镜像,由适配器负责在 Lambda 事件与 HTTP 请求之间转换。
# 引入官方 Lambda 适配器
FROM public.ecr.aws/awsguru/aws-lambda-adapter:0.9.0 AS aws-lambda-adapter
FROM oven/bun:debian AS bun_latest
# 把适配器放进扩展目录
COPY --from=aws-lambda-adapter /lambda-adapter /opt/extensions/lambda-adapter
# 适配器要求服务监听 8080
ENV PORT=8080
# Lambda 的默认工作目录
WORKDIR "/var/task"
COPY package.json bun.lock ./
RUN bun install --production --frozen-lockfile
COPY . /var/task
CMD ["bun", "index.ts"]
镜像推送到 ECR 后再创建函数:
# 构建(注意平台与 provenance)
docker build --provenance=false --platform linux/amd64 -t bun-lambda-demo:latest .
# 创建 ECR 仓库并记录 URI
export ECR_URI=$(aws ecr create-repository --repository-name bun-lambda-demo \
--region us-east-1 --query 'repository.repositoryUri' --output text)
# 登录、打标、推送
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin $ECR_URI
docker tag bun-lambda-demo:latest ${ECR_URI}:latest
docker push ${ECR_URI}:latest
随后在 AWS 控制台选择「Container image」创建函数,并在 Additional configurations → Networking → Function URL 中开启函数 URL(Auth Type 选 NONE)即可公网访问。
Note
--provenance=false是必需的:Lambda 不接受带 provenance 附加清单的镜像。--platform linux/amd64同理,除非你的函数架构选的是 arm64。
DigitalOcean
DigitalOcean App Platform 同样基于容器:先在 Container Registry 建仓库,构建并推送镜像,再从镜像创建 App。
doctl registry create bun-digitalocean-demo
doctl registry login
docker buildx build --platform=linux/amd64 \
-t registry.digitalocean.com/bun-digitalocean-demo/bun-digitalocean-demo:latest \
--push .
Dockerfile 与 Cloud Run 版本基本一致,区别是显式 EXPOSE 8080(平台会注入 PORT):
FROM oven/bun:debian
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --production --frozen-lockfile
COPY . .
EXPOSE 8080
CMD ["bun", "index.ts"]
32.4 自有服务器:守护进程与单文件
如果你直接管一台 VPS,不走容器,有三条常见路径。
systemd(Linux 首选)
在 /lib/systemd/system/ 下建一个 service 文件:
[Unit]
Description=My App
After=network.target
[Service]
Type=simple
User=YOUR_USER
WorkingDirectory=/home/YOUR_USER/path/to/my-app
# ExecStart 必须写绝对路径
ExecStart=/home/YOUR_USER/.bun/bin/bun run index.ts
Restart=always
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now my-app
sudo systemctl status my-app
journalctl -u my-app -f # 跟踪日志
非 root 用户默认无法监听 80/443。若确实需要,给 Bun 二进制授权:
setcap CAP_NET_BIND_SERVICE=+eip ~/.bun/bin/bun
Tip更推荐的做法是让应用监听高位端口(如 3000),前面挂 Nginx/Caddy 做反向代理与 TLS 终止,而不是给运行时提权。
PM2
已有 PM2 体系的团队可以把 Bun 当解释器接进去:
pm2 start --interpreter ~/.bun/bin/bun index.ts
或者用配置文件:
// pm2.config.js
module.exports = {
name: "app",
script: "index.ts",
interpreter: "bun",
env: {
PATH: `${process.env.HOME}/.bun/bin:${process.env.PATH}`,
},
};
pm2 start pm2.config.js
单文件可执行
bun build --compile 把运行时与代码打进一个二进制,目标机器上不需要装 Bun,也不需要 node_modules:
bun build ./src/server.ts --compile --minify --bytecode --outfile ./dist/server
scp ./dist/server user@host:/opt/myapp/server
配合 Docker 可以做出体积很小的镜像;配合 systemd 则把 ExecStart 直接指向这个二进制即可。交叉编译用 --target(如 --target=bun-linux-x64)在 macOS 上产出 Linux 二进制,细节见第 19 章。
32.5 上线前检查清单
bun.lock已提交,CI/构建阶段使用bun install --frozen-lockfile。- 端口读环境变量,不要硬编码;监听
0.0.0.0而非127.0.0.1。 NODE_ENV=production已设置,很多库(包括框架的错误页)依赖它切换行为。.env不进镜像:.dockerignore里排除,敏感配置改用平台的环境变量或密钥管理。- Bun 版本已锁定:镜像标签写具体版本,CI 里用
oven-sh/setup-bun固定版本,避免「本地 1.3.14、线上 1.2.x」的行为差异。 - 非 root 运行:容器里
USER bun,裸机上用专用系统用户。 - 健康检查端点:提供一个极简的
/healthz返回 200,供平台探活。 - 优雅退出:监听
SIGTERM,调用server.stop()后再退出,避免滚动发布时掐断在途请求。 - 日志输出到 stdout/stderr:容器与 PaaS 都从标准流采集日志,不要自己写文件。
- 原生依赖复核:若依赖里有 node-gyp 编译的原生模块,务必在与生产同架构的环境构建,
.dockerignore排除本机node_modules。
// 优雅退出的最小写法
const server = Bun.serve({
fetch() {
return new Response("ok");
},
});
process.on("SIGTERM", async () => {
await server.stop();
process.exit(0);
});
32.6 小结
Bun 的部署没什么黑魔法:它就是一个单文件二进制。凡是能跑 Node.js 的地方,把启动命令换成 bun 基本就能工作。
真正需要留心的是三类差异:
- 平台是否原生识别 Bun。Vercel、Railway、Render 有一等支持;Cloud Run、Lambda、DigitalOcean、Fly.io 走容器路线,运行时由你自己控制。
- 平台的具体限制。最典型的是 Vercel Functions 不支持
Bun.serve,必须借道框架;Lambda 需要适配器把事件模型转成 HTTP。 - 构建可复现性。
--frozen-lockfile+ 锁定镜像标签 + 锁定 CI 里的 Bun 版本,这三件事能消除绝大多数「环境不一致」类故障。
下一章我们来看 Bun 的另一面:它到底为什么快,官方公开的基准数据怎么读,以及如何用 mitata、hyperfine、--cpu-prof 这些工具给自己的代码做性能测量。