Bun Dockerfile for Production (Alpine, Non-Root bun User)
This Bun Dockerfile installs production dependencies with a frozen lockfile on oven/bun:1-alpine, then runs your TypeScript entry file directly with no compile step. The server runs as the image's non-root bun user on port 3000. It fits APIs written for Bun.serve or frameworks such as Hono and Elysia running on Bun.
- CI status
- Build passing
- Image size (uncompressed)
- 127.4 MB
- Build time
- 3 s
- Runtime image
oven/bun:1-alpine- Port
- 3000
- Last built
- 2026-10-15
# syntax=docker/dockerfile:1
FROM oven/bun:1-alpine AS deps
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile --production
FROM oven/bun:1-alpine
ENV NODE_ENV=production
WORKDIR /app
COPY --from=deps --chown=bun:bun /app/node_modules ./node_modules
COPY --chown=bun:bun . .
USER bun
EXPOSE 3000
CMD ["bun", "run", "index.ts"].git
node_modules/
.env*
Dockerfile
.dockerignoreBefore you build
- Commit bun.lock, the text lockfile used since Bun 1.2.
- Use index.ts as the entry point serving on port 3000, or change the CMD line.
- Keep runtime packages in dependencies, because development packages are skipped.
How this Dockerfile works
FROM oven/bun:1-alpine AS depsThe official Bun image on Alpine. The 1 tag follows the latest Bun 1.x release, which the weekly CI rebuild keeps checking.
COPY package.json bun.lock ./ RUN bun install --frozen-lockfile --productionInstalls exactly what bun.lock pins and skips devDependencies. --frozen-lockfile fails the build if the lockfile doesn't match package.json.
ENV NODE_ENV=productionMany libraries check NODE_ENV to disable development features, and Bun sets the same variable convention.
COPY --from=deps --chown=bun:bun /app/node_modules ./node_modulesCopies the installed dependencies, owned by the non-root bun user that ships with the image.
COPY --chown=bun:bun . .Adds your source files. Bun runs TypeScript natively, so there is no build stage or dist folder to manage.
USER bun EXPOSE 3000 CMD ["bun", "run", "index.ts"]Starts the server as the unprivileged user. Bun is the main process, so it receives stop signals directly.
Build and run
docker build -t bun-app .
docker run --rm -p 3000:3000 bun-appBuild from the project root and open http://localhost:3000. Configuration goes in environment variables at runtime, which Bun exposes through process.env and Bun.env.
Other Bun Dockerfile variants
The Alpine image keeps Bun containers small. A Debian-based oven/bun image suits dependencies that need glibc, and a compiled single-file executable variant can drop the runtime entirely. Verified variants appear in the table below.
| Variant | Runtime image | Image size | CI |
|---|---|---|---|
| Bun runtime on Alpine | oven/bun:1-alpine | 127.4 MB | Passing |
Troubleshooting
- error: lockfile had changes, but lockfile is frozen
- package.json changed without updating bun.lock. Run bun install locally, commit the new lockfile, and rebuild.
- COPY failed: file not found in build context: bun.lockb
- Older projects use the binary bun.lockb. Upgrade with bun install --save-text-lockfile, or change the COPY line to the file your project has.
- A package is missing at runtime
- It's listed under devDependencies, so --production skipped it. Move packages your code imports while running into dependencies.
FAQ
- Do I need a build step for TypeScript with Bun?
- No. Bun transpiles TypeScript on the fly when it loads your files. A build step only helps if you want a bundled or compiled single-file output.
- Why pin oven/bun:1 instead of a full version?
- The 1 tag gets fixes automatically, and the weekly CI rebuild catches breaking changes. Pin an exact version for fully reproducible production images.
- Can I use this with Hono or Elysia?
- Yes. Both run on Bun.serve. Keep the entry file as index.ts, or point the CMD line at your framework's entry file.