Deployment

Building, running, and observing the worker in production.

Build and start

bunx openqueue build

build compiles every discovered task, its config, and the worker into a self-contained Nitro node-server bundle at .output (configurable via build.outDir) and prints what it found:

OpenQueue build wrote 14 tasks, 5 queues, 3 schedules, 5.6 MB to .output
bunx openqueue start

start prefers the built artifact — it spawns .output/server/index.mjs, polls /health until ready, and forwards SIGTERM/SIGINT so in-flight jobs drain before exit. When no artifact exists it falls back to booting the worker from source. It injects PORT/NITRO_PORT, preserving the worker's 8090 default.

The artifact is a Node bundle, so start runs it under a node binary from PATH when one satisfies the floor (^20.19 || >=22.12), falling back to Bun (the CLI's own runtime) when no suitable Node is found. It logs which it chose:

[openqueue] artifact runtime: Node v22.12.0 (/usr/local/bin/node)

Node is preferred because Bun's node-compat carries a heavier resident-memory profile for the long-lived worker; install Node in the runtime image to pin it.

Running the artifact directly

The .output bundle is a standard Nitro node-server; run it without the CLI on Node ^20.19 || >=22.12 or Bun:

PORT=8090 node .output/server/index.mjs

Without PORT, a direct boot defaults to Nitro's port 3000 — set PORT explicitly (or use openqueue start, which injects it).

Sourcemaps

build: { sourcemap: true } in worker.config.ts emits .map files alongside the bundle in .output/server — for error symbolication (e.g. a Sentry sourcemap upload). Maps are off by default and add tens of MB: upload them in CI, then strip *.map from the deployed image.

Docker

The repository ships a worker Dockerfile under docker/worker. The image runs openqueue build under Bun, then openqueue start — which runs the built artifact under Node when the image provides one, else Bun. The only required runtime input is REDIS_URL.

FROM oven/bun:1 AS base
WORKDIR /app
COPY . .
RUN bun install --frozen-lockfile && bunx openqueue build
CMD ["bunx", "openqueue", "start"]

To run the artifact on Node in production, build it under Bun and copy .output into a slim Node image — no Bun or source at runtime:

FROM oven/bun:1 AS build
WORKDIR /app
COPY . .
RUN bun install --frozen-lockfile && bunx openqueue build

FROM node:22-slim
WORKDIR /app
COPY --from=build /app/.output ./.output
ENV PORT=8090
EXPOSE 8090
CMD ["node", ".output/server/index.mjs"]

Scale horizontally by running more replicas — BullMQ coordinates workers through Redis, and concurrency.global caps each replica. To run heavy queues on their own, bigger machines, see Scaling.

Observability

  • OpenTelemetry — trace context is captured at enqueue and restored when the job runs, so a job appears as a child span of the request that triggered it. Spans are emitted into the process's existing tracer provider.
  • Drains — run lifecycle events (enqueued, started, completed, failed) can be fanned out to sinks. consoleDrain() is built in; composeDrains() combines several; implement QueueDrain to ship events anywhere else.
  • Metrics — per-queue counters and latency percentiles power the Workbench overview; enable persistence with the metrics config block.

Production checklist

  • Set a unique namespace per application sharing a Redis instance.
  • Put Workbench behind auth or mount it inside your authenticated app.
  • Wire postgresAdapter so run history survives Redis eviction.
  • Keep attempts/backoff deliberate on tasks with side effects — and make handlers idempotent, since at-least-once delivery is the contract.

On this page