首页 / Bun 入门教程 / 部署 Bun 应用

Bun 入门教程

部署 Bun 应用

本教程共 34 篇 · 第 32 篇 · 更新于 2026-08-06

Bun部署DockerVercelRailwayCloud RunAWS Lambdasystemd

本节目标:

  • 掌握部署前必须处理的三个通用问题:端口与监听地址、锁文件与依赖裁剪、产物形态的选择。
  • 能写出一份生产可用的 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.tsbun 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);
Warning

Vercel 上的 Bun 运行时目前处于 Beta,且有两条硬性限制:Bun.serve 在 Vercel Functions 上不受支持,需要改用 Vercel 支持的框架(Next.js、Express、Hono、Nitro 等);此外自动 source map、字节码缓存、node:http/https 的指标采集尚未支持。若要在 Bun 项目里使用 Routing Middleware,需要把中间件运行时显式设为 nodejsexport 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 时填三个值:

配置项
RuntimeNode
Build Commandbun install
Start Commandbun 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 上线前检查清单

  1. bun.lock 已提交,CI/构建阶段使用 bun install --frozen-lockfile
  2. 端口读环境变量,不要硬编码;监听 0.0.0.0 而非 127.0.0.1
  3. NODE_ENV=production 已设置,很多库(包括框架的错误页)依赖它切换行为。
  4. .env 不进镜像.dockerignore 里排除,敏感配置改用平台的环境变量或密钥管理。
  5. Bun 版本已锁定:镜像标签写具体版本,CI 里用 oven-sh/setup-bun 固定版本,避免「本地 1.3.14、线上 1.2.x」的行为差异。
  6. 非 root 运行:容器里 USER bun,裸机上用专用系统用户。
  7. 健康检查端点:提供一个极简的 /healthz 返回 200,供平台探活。
  8. 优雅退出:监听 SIGTERM,调用 server.stop() 后再退出,避免滚动发布时掐断在途请求。
  9. 日志输出到 stdout/stderr:容器与 PaaS 都从标准流采集日志,不要自己写文件。
  10. 原生依赖复核:若依赖里有 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 的另一面:它到底为什么快,官方公开的基准数据怎么读,以及如何用 mitatahyperfine--cpu-prof 这些工具给自己的代码做性能测量。