Containers & Kubernetes
Reproducible images, probes, signals, secrets, and scaling for Bun services
Last updated on
Production image
Prefer Bun's official image and pin a tested tag or digest. Separate development dependencies, verification, and production dependencies so the final image contains only runtime material:
FROM oven/bun:1 AS base
WORKDIR /app
FROM base AS install
COPY package.json bun.lock ./
RUN bun ci
FROM base AS verify
COPY --from=install /app/node_modules ./node_modules
COPY . .
RUN bun run typecheck && bun test && bun run build
FROM base AS production-deps
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile --production --ignore-scripts
FROM base AS release
ENV NODE_ENV=production
COPY --from=production-deps /app/node_modules ./node_modules
COPY --from=verify /app/dist ./dist
COPY package.json ./
USER bun
EXPOSE 3000
CMD ["bun", "run", "--no-env-file", "dist/index.js"]If a production dependency genuinely needs an install script, do not silently remove --ignore-scripts. Locate it with bun pm untrusted, review it, and grant the narrowest trustedDependencies exception.
This Dockerfile is a single-package baseline whose dist still needs production dependencies. A monorepo install stage must also copy the relevant workspace manifests. A fully bundled output may omit node_modules, but only after checking externals, dynamic files, and native dependencies. See the production engineering baseline for the complete release gate.
Service contract
const port = Number(Bun.env.PORT ?? 3000);
const server = Bun.serve({
port,
idleTimeout: 30,
maxRequestBodySize: 2 * 1024 * 1024,
routes: {
'/live': () => new Response('ok'),
'/ready': async () => {
const ready = await dependenciesAreReady();
return new Response(ready ? 'ok' : 'not ready', { status: ready ? 200 : 503 });
},
},
fetch: handleRequest,
});
async function shutdown(signal: string) {
console.info('shutdown_started', { signal });
await server.stop();
await closeDependencies();
}
process.once('SIGTERM', () => void shutdown('SIGTERM'));
process.once('SIGINT', () => void shutdown('SIGINT'));dependenciesAreReady() and closeDependencies() are project boundary functions, not Bun APIs. Put database, queue, and external-service checks there, with short timeouts so probes cannot overload the service.
Three probe meanings
| Probe | Question | Failure action |
|---|---|---|
| startup | Has initial startup completed? | Suppress other probes during startup |
| readiness | Can this instance accept new traffic now? | Remove it from load balancing; do not immediately restart |
| liveness | Is the process stuck and unrecoverable? | Restart the container |
Do not make liveness depend on a flaky external API. Kubernetes warns that incorrect liveness probes can cause cascading failures.
Cloud-native checklist
- Port: read
PORT; Cloud Run sends traffic to the configured container port. - State: keep sessions, jobs, and uploaded objects outside the replaceable container filesystem.
- Logs: emit structured stdout/stderr with request IDs; never log secrets or entire user prompts.
- Resources: set CPU/memory requests and limits; benchmark concurrency instead of inferring it from a laptop.
- Migrations: execute them as a separate job or release step, not concurrently in every replica.
- Secrets: use platform secret references. Environment-injected values commonly require a new deployment after rotation unless the app fetches them dynamically.
Official references: Bun Docker guide, Kubernetes probes, Kubernetes Secrets, and Cloud Run configuration.