Skip to content
Get started
Get started
Deploy bunqueue: Docker, systemd & PM2
guide · deployment

From laptop to production.

This page shows the smallest working way to run bunqueue in production, then ready-to-paste configs for Docker, systemd, and PM2, plus health checks and backups.

bunqueue runs as a single process that stores everything in one SQLite file. There is no clustering and no multi-node setup to plan. Deploying it means answering one question: does the queue live inside your app process, or in its own process?

Embedded mode means the queue runs inside your application process, no separate queue server at all. Point it at a file path so jobs survive restarts:

app.ts
import { Queue, Worker } from 'bunqueue/client';
const queue = new Queue('emails', {
embedded: true,
dataPath: './data/bunq.db', // jobs persist here
});
await queue.add('send', { to: 'user@example.com' });
new Worker('emails', async (job) => {
await sendEmail(job.data);
return { sent: true };
}, { embedded: true, dataPath: './data/bunq.db', concurrency: 5 });

Deploy your app the way you already deploy it. The queue ships with it. Done.

The moment your API and your workers are separate processes (or separate containers), run the bunqueue server. One server owns the SQLite file; every other process talks to it over TCP:

Terminal window
bunqueue start --data-path ./data/bunq.db
// api.ts, pushes jobs (no embedded flag = TCP client to localhost:6789)
import { Queue } from 'bunqueue/client';
const queue = new Queue('tasks');
await queue.add('process', { data: '...' });
// worker.ts, a separate process, restart and scale it independently
import { Worker } from 'bunqueue/client';
new Worker('tasks', async (job) => {
return { done: true };
}, { concurrency: 10 });

You can also push jobs without any SDK, via CLI or HTTP:

Terminal window
bunqueue push emails '{"to": "user@example.com"}'
curl -X POST http://localhost:6790/queues/emails/jobs \
-H "Content-Type: application/json" \
-d '{"data": {"to": "user@example.com"}}'
FROM oven/bun:1-alpine
WORKDIR /app
COPY package.json bun.lock* ./
RUN bun install --frozen-lockfile --production
COPY . .
RUN mkdir -p /app/data
ENV DATA_PATH=/app/data/bunq.db
ENV NODE_ENV=production
# wget ships with the Alpine base, curl does not
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --spider -q http://127.0.0.1:6790/health || exit 1
EXPOSE 6789 6790
CMD ["bun", "run", "start"]
docker-compose.yml
services:
bunqueue:
build: .
ports:
- "6789:6789" # TCP (SDK clients)
- "6790:6790" # HTTP (health, metrics, REST)
volumes:
- bunqueue-data:/app/data
environment:
- DATA_PATH=/app/data/bunq.db
- AUTH_TOKENS=${AUTH_TOKENS}
- S3_BACKUP_ENABLED=1
- S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID}
- S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY}
- S3_BUCKET=${S3_BUCKET}
- S3_REGION=${S3_REGION}
restart: unless-stopped
deploy:
resources:
limits:
memory: 512M
volumes:
bunqueue-data:

For bare-metal or VM deployments:

/etc/systemd/system/bunqueue.service
[Unit]
Description=bunqueue Job Queue
After=network.target
[Service]
Type=simple
User=bunqueue
Group=bunqueue
WorkingDirectory=/var/lib/bunqueue
ExecStart=/usr/local/bin/bunqueue start
Restart=always
RestartSec=5
Environment=NODE_ENV=production
Environment=DATA_PATH=/var/lib/bunqueue/bunq.db
EnvironmentFile=/etc/bunqueue.env
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/bunqueue
MemoryMax=512M
[Install]
WantedBy=multi-user.target
Terminal window
sudo systemctl daemon-reload
sudo systemctl enable --now bunqueue
sudo journalctl -u bunqueue -f

To upgrade: stop the service, bun install -g bunqueue@latest, start it again, then confirm with curl http://localhost:6790/health | jq .version. Updating the package does not restart the service.

Build the standalone binary first (bun run build in the repo produces dist/bunqueue, a self-contained executable that bundles the Bun runtime), or install globally.

ecosystem.config.js
module.exports = {
apps: [{
name: 'bunqueue',
script: '/usr/local/bin/bunqueue',
args: 'start',
instances: 1, // single instance only, no cluster mode
exec_mode: 'fork',
autorestart: true,
max_memory_restart: '512M',
env: {
NODE_ENV: 'production',
DATA_PATH: '/var/lib/bunqueue/bunq.db',
TCP_PORT: 6789,
HTTP_PORT: 6790,
},
}]
};
Terminal window
pm2 start ecosystem.config.js
pm2 save && pm2 startup # survive reboots

The HTTP port (default 6790) serves everything an orchestrator needs:

Terminal window
curl http://localhost:6790/health # detailed: status, version, job counts, memory
curl http://localhost:6790/healthz # bare liveness, returns OK
curl http://localhost:6790/ready # readiness
curl http://localhost:6790/prometheus # metrics in Prometheus text format

/health and /ready return 503 when persistent storage reports a full disk; /healthz remains a pure process-liveness check. If METRICS_AUTH=true, also configure AUTH_TOKENS and the scraper bearer token; otherwise /prometheus fails closed with 503.

# Kubernetes
livenessProbe:
httpGet: { path: /healthz, port: 6790 }
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet: { path: /ready, port: 6790 }
initialDelaySeconds: 5
periodSeconds: 5

Alerting thresholds and dashboards are covered in Monitoring and Production Operations.

The whole queue is one SQLite file, so back up that one file. The built-in S3 backup uploads periodic snapshots (works with AWS S3, Cloudflare R2, MinIO):

Terminal window
S3_BACKUP_ENABLED=1
S3_BUCKET=my-bunqueue-backups
S3_ACCESS_KEY_ID=...
S3_SECRET_ACCESS_KEY=...
S3_REGION=us-east-1 # or S3_ENDPOINT for R2/MinIO
S3_BACKUP_INTERVAL=3600000 # every hour (default 6h)
S3_BACKUP_RETENTION=24 # snapshots kept

The persistent data path configured earlier is mandatory. The server flushes its pending write buffer, then asks SQLite for a VACUUM INTO snapshot that includes committed WAL frames; do not replace this with a raw live-file copy.

To restore after a disk loss:

Terminal window
systemctl stop bunqueue
bunqueue backup list
bunqueue backup restore backups/bunq-2026-01-30T12:00:00.db --force
systemctl start bunqueue

Provider-specific setup (R2, MinIO, Spaces) and how restore verifies SHA-256, checks SQLite integrity, and quarantines stale sidecars before replacing anything: S3 Backup guide.

VariableWhat it doesDefault
BUNQUEUE_DATA_PATHSQLite file path (aliases, in priority order: BQ_DATA_PATH, DATA_PATH, SQLITE_PATH)in-memory
TCP_PORTPort for SDK clients6789
HTTP_PORTPort for health, metrics, REST6790
AUTH_TOKENSComma-separated tokens clients must presentnone (open)
TLS_CERT_FILE / TLS_KEY_FILEPEM cert and key for native TLS on TCP and HTTP. Set both or neither; setting only one is a startup erroroff
S3_BACKUP_ENABLEDTurn on S3 snapshots0
SHUTDOWN_TIMEOUT_MSHow long a graceful shutdown waits for active jobs30000

The full list, including all S3 and cloud variables, lives in Environment Variables. The same options are available as CLI flags: bunqueue start --tcp-port --http-port --data-path --auth-tokens --tls-cert --tls-key.

bunqueue is I/O bound, not CPU bound; one core handles most workloads.

Jobs per dayRAM
under 10k128-256 MB
10k-100k512 MB
over 100k1 GB+

Disk grows with retained jobs. Set removeOnComplete: true in job options to drop completed jobs, and keep removeOnFail: false so failures stay inspectable. Scale vertically: more RAM, faster disk (NVMe), higher worker concurrency.

  • No horizontal scaling. No multi-node, no failover, no distributed processing. If you need those, reach for Redis + BullMQ with Sentinel, Kafka, or a managed queue like SQS. Multiple worker processes on one machine are fine, through the TCP server.
  • Two embedded processes, one file, is silent corruption. See the warning above; always go through the server for multi-process.
  • Copying the database while the server runs can produce a torn copy. SQLite runs in WAL mode (writes go to a .db-wal sidecar file first). Stop the server, then copy bunq.db and bunq.db-wal together, or just use the built-in S3 backup, which snapshots safely while live.
  • AUTH_TOKENS is not set by default. Anyone who can reach the ports can push and pull jobs. Set tokens (openssl rand -hex 32) and either enable native TLS or keep the ports on a private network. See Security.
  • Jobs you cannot afford to lose in a hard crash should be durable: true. By default writes are buffered for up to 10 ms for throughput; durable writes hit disk before the add returns. Details in Production Operations.