Cloud & deploymentContainers & Kubernetes

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:

Dockerfile
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

src/index.ts
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

ProbeQuestionFailure action
startupHas initial startup completed?Suppress other probes during startup
readinessCan this instance accept new traffic now?Remove it from load balancing; do not immediately restart
livenessIs 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.