---
title: "Bunqueue Production Deployment Guide: Docker, Systemd & PM2"
description: Deploy bunqueue to production with Docker, systemd, or PM2. S3 backups, health checks, resource limits, and disaster recovery strategies.
head:
  - tag: meta
    attrs:
      property: og:image
      content: https://bunqueue.dev/og/server-mode.png
---

import { Tabs, TabItem, Aside, Steps } from '@astrojs/starlight/components';

This guide covers deploying bunqueue in production. bunqueue is designed as a **single-instance** job queue - it doesn't support clustering or horizontal scaling.

<Aside type="caution">
bunqueue runs as a **single process**. There's no built-in clustering or multi-node support. Tested and validated up to **1 million jobs** with 100% data integrity.
</Aside>

## Architecture Overview

```
┌─────────────────────────────────────────────────────────────┐
│                     Your Application                         │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐         │
│  │   Web App   │  │   API       │  │   Workers   │         │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘         │
│         │                │                │                 │
│         └────────────────┼────────────────┘                 │
│                          │                                  │
│                          ▼                                  │
│              ┌───────────────────────┐                     │
│              │      bunqueue         │ ◄── Single instance │
│              │   (embedded mode)     │                     │
│              └───────────┬───────────┘                     │
│                          │                                  │
│                          ▼                                  │
│              ┌───────────────────────┐                     │
│              │   SQLite Database     │ ◄── Local file      │
│              │   (./data/bunq.db)    │                     │
│              └───────────────────────┘                     │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼ (optional)
              ┌───────────────────────┐
              │      S3 Backup        │
              │  (disaster recovery)  │
              └───────────────────────┘
```

## Deployment Options

### Option 1: Embedded Mode (Recommended)

Run bunqueue directly in your application process. No separate server needed.

<Aside type="caution">
Set `DATA_PATH` environment variable or use a [configuration file](/guide/configuration/) for SQLite persistence. Without it, data is stored **in-memory only** and will be lost on restart.
</Aside>

```bash
# Enable SQLite persistence
export DATA_PATH=./data/bunq.db
```

<Aside type="tip" title="Config File">
Instead of environment variables, you can centralize all settings in a typed `bunqueue.config.ts`:

```typescript
import { defineConfig } from 'bunqueue';

export default defineConfig({
  storage: { dataPath: './data/bunq.db' },
  auth: { tokens: [process.env.AUTH_TOKEN!] },
  backup: { enabled: true, bucket: 'my-backups' },
});
```

See [Configuration File](/guide/configuration/) for the full reference.
</Aside>

```typescript
// app.ts
import { Queue, Worker } from 'bunqueue/client';

// Your web framework (Hono, Elysia, Express...)
const app = new Hono();

// Queue is embedded in the same process (uses DATA_PATH for persistence)
const emailQueue = new Queue('emails', { embedded: true });

app.post('/send-email', async (c) => {
  const { to, subject } = await c.req.json();
  await emailQueue.add('send', { to, subject });
  return c.json({ queued: true });
});

// Worker runs in the same process
new Worker('emails', async (job) => {
  await sendEmail(job.data);
  return { sent: true };
}, { embedded: true, concurrency: 5 });

export default app;
```

**Pros:**
- Simplest setup
- No network latency
- Single deployment unit

**Cons:**
- Queue dies if app dies
- Harder to scale workers independently

### Option 2: Separate Worker Process

Run your API and workers as separate processes sharing the same SQLite database.

```bash
# Both processes MUST use the same DATA_PATH
export DATA_PATH=./data/bunq.db
```

```typescript
// api.ts - Your web server
import { Queue } from 'bunqueue/client';

const queue = new Queue('tasks', { embedded: true });

app.post('/task', async (c) => {
  await queue.add('process', { data: '...' });
  return c.json({ ok: true });
});
```

```typescript
// worker.ts - Separate process
import { Worker } from 'bunqueue/client';

new Worker('tasks', async (job) => {
  // Heavy processing here
  return { done: true };
}, { embedded: true, concurrency: 10 });

console.log('Worker started');
```

```bash
# Run both (same DATA_PATH)
DATA_PATH=./data/bunq.db bun run api.ts &
DATA_PATH=./data/bunq.db bun run worker.ts &
```

**Pros:**
- Workers can be restarted independently
- Better resource isolation

**Cons:**
- Two processes to manage
- Still single SQLite file (no true distribution)

### Option 3: Server Mode (CLI)

Run bunqueue as a standalone server. Interact via CLI or HTTP API.

```bash
# Start server
bunqueue start --tcp-port 6789 --http-port 6790
```

```bash
# Add jobs via CLI
bunqueue push emails '{"to": "user@example.com", "subject": "Hello"}'

# Or via HTTP API
curl -X POST http://localhost:6790/queues/emails/jobs \
  -H "Content-Type: application/json" \
  -d '{"data": {"to": "user@example.com"}}'
```

<Aside type="tip">
The `Queue` and `Worker` classes from `bunqueue/client` connect to the TCP server by default (when `embedded` is not set to `true`). You can also use the CLI or HTTP API.
</Aside>

```typescript
import { Queue, Worker } from 'bunqueue/client';

// Connects to localhost:6789 by default
const queue = new Queue('emails');
await queue.add('send', { to: 'user@example.com' });

// Worker also connects to the server
const worker = new Worker('emails', async (job) => {
  await sendEmail(job.data);
  return { sent: true };
});
```

## Docker Deployment

### Dockerfile

```dockerfile
FROM oven/bun:1

WORKDIR /app

# Copy package files
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production

# Copy application
COPY . .

# Create data directory
RUN mkdir -p /app/data

# Environment
ENV DATA_PATH=/app/data/bunq.db
ENV NODE_ENV=production

# Health check
HEALTHCHECK --interval=30s --timeout=3s \
  CMD curl -f http://127.0.0.1:6790/health || exit 1

EXPOSE 6789 6790

CMD ["bun", "run", "start"]
```

### Docker Compose

```yaml
version: '3.8'

services:
  bunqueue:
    build: .
    ports:
      - "6789:6789"  # TCP
      - "6790:6790"  # HTTP
    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
        reservations:
          memory: 256M

volumes:
  bunqueue-data:
```

## Systemd Service

For bare-metal or VM deployments:

```ini
# /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
Environment=NODE_ENV=production
Environment=DATA_PATH=/var/lib/bunq.db
EnvironmentFile=/etc/env

# Security
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/bunqueue

# Resource limits
MemoryMax=512M
CPUQuota=200%

[Install]
WantedBy=multi-user.target
```

```bash
# Install
sudo systemctl daemon-reload
sudo systemctl enable bunqueue
sudo systemctl start bunqueue

# Check status
sudo systemctl status bunqueue
sudo journalctl -u bunqueue -f
```

### Updating bunqueue

To update bunqueue on a systemd-managed server:

```bash
# 1. Stop the service
sudo systemctl stop bunqueue

# 2. Update the package
bun install -g bunqueue@latest

# 3. Verify the new version
bunqueue --version

# 4. Restart the service
sudo systemctl start bunqueue

# 5. Confirm the running version
curl http://localhost:6790/health | jq .version
```

:::caution
Always verify the running version with `curl /health` after restart. Simply running `bun update` does **not** restart the service — you must explicitly stop and start it with `systemctl`.
:::

## Building from Source

Compile bunqueue into a standalone executable for production deployment.

### Build Command

```bash
# Clone the repository
git clone https://github.com/egeominotti/bunqueue.git
cd bunqueue

# Install dependencies
bun install

# Build standalone binary
bun run build
```

This creates `dist/bunqueue` (~56 MB), a self-contained executable with no runtime dependencies.

### Verify Build

```bash
# Check version
./dist/bunqueue --version

# Show help
./dist/bunqueue --help

# Start server
./dist/bunqueue start
```

### Install Globally

```bash
# Copy to system path
sudo cp dist/bunqueue /usr/local/bin/

# Verify installation
bunqueue --version
```

<Aside type="tip">
The compiled binary includes the Bun runtime. No need to install Bun on the target server.
</Aside>

---

## PM2 Process Manager

For cross-platform process management with PM2:

### Compiled Binary (Recommended)

First, build the standalone executable:

```bash
bun run build
```

Then configure PM2:

```javascript
// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'bunqueue',
    script: '/usr/local/bin/bunqueue',  // Compiled binary
    args: 'start',
    instances: 1,  // Single instance only - no cluster mode
    exec_mode: 'fork',
    autorestart: true,
    watch: false,
    max_memory_restart: '512M',
    env: {
      NODE_ENV: 'production',
      DATA_PATH: '/var/lib/bunq.db',
      TCP_PORT: 6789,
      HTTP_PORT: 6790,
    },
    error_file: '/var/log/error.log',
    out_file: '/var/log/out.log',
    log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
    merge_logs: true,
  }]
};
```

### Development Mode (with Bun)

For development or when using the source directly:

```javascript
// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'bunqueue',
    script: 'bun',
    args: 'run start',
    cwd: '/opt/bunqueue',
    instances: 1,
    exec_mode: 'fork',
    autorestart: true,
    max_memory_restart: '512M',
    env: {
      NODE_ENV: 'production',
      DATA_PATH: '/var/lib/bunq.db',
      TCP_PORT: 6789,
      HTTP_PORT: 6790,
    },
  }]
};
```

### PM2 Commands

```bash
# Start
pm2 start ecosystem.config.js

# Restart
pm2 restart bunqueue

# Stop
pm2 stop bunqueue

# View logs
pm2 logs bunqueue

# Monitor
pm2 monit

# Save process list for startup
pm2 save
pm2 startup
```

<Aside type="caution">
Do not use PM2 cluster mode (`instances > 1`). bunqueue is a single-instance queue and multiple instances will cause data corruption.
</Aside>

## Environment Variables

| Variable | Description | Default |
|----------|-------------|---------|
| `DATA_PATH` | SQLite database path | `in-memory` |
| `TCP_PORT` | TCP server port | `6789` |
| `HTTP_PORT` | HTTP server port | `6790` |
| `AUTH_TOKENS` | Comma-separated auth tokens | - |
| `S3_BACKUP_ENABLED` | Enable S3 backups | `0` |
| `S3_ACCESS_KEY_ID` | S3 access key | - |
| `S3_SECRET_ACCESS_KEY` | S3 secret key | - |
| `S3_BUCKET` | S3 bucket name | - |
| `S3_REGION` | S3 region | `us-east-1` |
| `S3_ENDPOINT` | Custom S3 endpoint | - |
| `S3_BACKUP_INTERVAL` | Backup interval (ms) | `21600000` (6h) |
| `S3_BACKUP_RETENTION` | Backups to keep | `7` |

## S3 Backup Configuration

<Aside type="tip">
Always enable S3 backups in production. SQLite is a single file - if it corrupts or the disk fails, you lose everything.
</Aside>

### AWS S3

```bash
S3_BACKUP_ENABLED=1
S3_ACCESS_KEY_ID=AKIA...
S3_SECRET_ACCESS_KEY=...
S3_BUCKET=my-bunqueue-backups
S3_REGION=us-east-1
S3_BACKUP_INTERVAL=3600000  # Every hour
S3_BACKUP_RETENTION=24       # Keep 24 backups
```

### Cloudflare R2

```bash
S3_BACKUP_ENABLED=1
S3_ACCESS_KEY_ID=...
S3_SECRET_ACCESS_KEY=...
S3_BUCKET=bunqueue-backups
S3_ENDPOINT=https://ACCOUNT_ID.r2.cloudflarestorage.com
S3_REGION=auto
```

### MinIO (Self-hosted)

```bash
S3_BACKUP_ENABLED=1
S3_ACCESS_KEY_ID=minioadmin
S3_SECRET_ACCESS_KEY=minioadmin
S3_BUCKET=bunqueue
S3_ENDPOINT=http://minio:9000
S3_REGION=us-east-1
```

## Health Checks

bunqueue exposes health endpoints:

```bash
# HTTP health check (detailed)
curl http://localhost:6790/health
# {"ok":true,"status":"healthy","uptime":3600,"version":"2.5.7",
#  "queues":{"waiting":5,"active":2},"connections":{"ws":0,"sse":0},
#  "memory":{"heapUsed":45,"heapTotal":64,"rss":128}}

# Simple liveness probe
curl http://localhost:6790/healthz
# OK

# Readiness probe
curl http://localhost:6790/ready
# {"ok":true,"ready":true}

# Queue stats
curl http://localhost:6790/stats
# {"ok":true,"stats":{"waiting":5,"active":2,"completed":1000,"dlq":0}}

# Prometheus metrics (text format)
curl http://localhost:6790/prometheus
```

### Kubernetes Probes

```yaml
livenessProbe:
  httpGet:
    path: /healthz
    port: 6790
  initialDelaySeconds: 5
  periodSeconds: 10

readinessProbe:
  httpGet:
    path: /ready
    port: 6790
  initialDelaySeconds: 5
  periodSeconds: 5
```

## Resource Requirements

### Memory

| Workload | Recommended RAM |
|----------|-----------------|
| Light (&lt;1k jobs/day) | 128 MB |
| Medium (1k-10k jobs/day) | 256 MB |
| Heavy (10k-100k jobs/day) | 512 MB |
| Very Heavy (&gt;100k jobs/day) | 1 GB+ |

### Disk

SQLite database size depends on:
- Number of jobs retained
- Job data size
- `removeOnComplete` setting

```typescript
// Reduce disk usage
new Queue('tasks', {
  defaultJobOptions: {
    removeOnComplete: true,  // Don't keep completed jobs
    removeOnFail: false,     // Keep failed for debugging
  }
});
```

### CPU

bunqueue is I/O bound, not CPU bound. A single core handles most workloads.

## Production Checklist

<Steps>
1. **Enable S3 backups**

   Don't skip this. SQLite corruption = data loss.

2. **Set auth tokens**

   ```bash
   AUTH_TOKENS=token1,token2,token3
   ```

3. **Use `durable: true` for critical jobs**

   ```typescript
   // Payments, orders, and critical events
   await queue.add('payment', data, { durable: true });
   ```

4. **Configure resource limits**

   Prevent runaway memory/CPU usage.

5. **Set up monitoring**

   Scrape `/prometheus` with Prometheus or similar.

6. **Configure log aggregation**

   Send logs to a central system.

7. **Test backup restoration**

   ```bash
   # List backups first
   bunqueue backup list
   # Then restore by key
   bunqueue backup restore backups/bunq-2026-01-30T12:00:00.db --force
   ```

8. **Set up alerts**

   - DLQ count > threshold
   - Waiting jobs growing
   - Worker not processing

</Steps>

## Scaling Limitations

<Aside type="caution" title="No Clustering">
bunqueue does **not** support horizontal scaling or clustering. It's a single-instance queue.
</Aside>

### What this means

- ❌ No multi-node deployment
- ❌ No automatic failover
- ❌ No distributed processing across machines
- ✅ Multiple workers in same process (concurrency)
- ✅ Multiple worker processes on same machine (shared SQLite)

### When bunqueue is enough

| Scenario | Jobs/day | bunqueue? |
|----------|----------|-----------|
| Small SaaS | &lt;10k | ✅ Perfect |
| Medium app | 10k-100k | ✅ Fine |
| Large app | 100k-1M | ✅ Tested |
| Enterprise | &gt;1M | ⚠️ Test first |

### When to use something else

If you need:
- **High availability** → Redis + BullMQ with Sentinel
- **Distributed processing** → Kafka, RabbitMQ
- **Multi-region** → Managed queues (SQS, Cloud Tasks)
- **Complex workflows** → Temporal, Inngest

### Vertical scaling

bunqueue scales **vertically** well:
- More RAM = more jobs in memory
- Faster disk (NVMe) = faster SQLite
- More CPU cores = more worker concurrency

```typescript
// Scale worker concurrency with available CPUs
import { cpus } from 'os';

new Worker('tasks', processor, {
  concurrency: cpus().length * 2
});
```

## Disaster Recovery

### Backup Strategy

```
Every 1 hour  → S3 backup
Every 6 hours → Verify backup integrity
Every day     → Test restore in staging
```

### Recovery Steps

1. **Stop bunqueue**
   ```bash
   systemctl stop bunqueue
   ```

2. **List available backups**
   ```bash
   bunqueue backup list
   ```

3. **Restore from backup**
   ```bash
   bunqueue backup restore backups/bunq-2024-01-30T12:00:00.db --force
   ```

4. **Start bunqueue**
   ```bash
   systemctl start bunqueue
   ```

### Point-in-Time Recovery

SQLite WAL mode allows recovery to recent states:

```bash
# Backup includes WAL file
cp data/bunq.db data/bunq.db-wal /backup/

# Restore
cp /backup/bunq.db* data/
```

## Security

### Network

- Run behind reverse proxy (nginx, Caddy)
- Use TLS for external connections
- Firewall TCP/HTTP ports

### Authentication

```bash
# Generate strong tokens
AUTH_TOKENS=$(openssl rand -hex 32),$(openssl rand -hex 32)
```

### File Permissions

```bash
# Restrict database access
chmod 600 /var/lib/bunq.db
chown bunqueue:bunqueue /var/lib/bunq.db
```

## Monitoring with Prometheus

```yaml
# prometheus.yml
scrape_configs:
  - job_name: 'bunqueue'
    static_configs:
      - targets: ['localhost:6790']
    metrics_path: /prometheus
```

### Key Metrics

| Metric | Alert Threshold |
|--------|-----------------|
| `bunqueue_jobs_waiting` | &gt; 1000 for 5 min |
| `bunqueue_jobs_dlq` | &gt; 10 |
| `bunqueue_jobs_active` | 0 for 5 min (workers dead?) |
| `bunqueue_jobs_failed_total` | increasing rapidly |

## Example: Full Production Setup

```bash
# /etc/env
NODE_ENV=production
DATA_PATH=/var/lib/bunq.db
TCP_PORT=6789
HTTP_PORT=6790
AUTH_TOKENS=prod-token-abc123,deploy-token-xyz789

# S3 Backups
S3_BACKUP_ENABLED=1
S3_ACCESS_KEY_ID=AKIA...
S3_SECRET_ACCESS_KEY=...
S3_BUCKET=company-bunqueue-backups
S3_REGION=eu-west-1
S3_BACKUP_INTERVAL=3600000
S3_BACKUP_RETENTION=48
```

```typescript
// production.ts
import { Queue, Worker } from 'bunqueue/client';

const queue = new Queue('production-tasks', {
  embedded: true,
  defaultJobOptions: {
    attempts: 5,
    backoff: 5000,
    removeOnComplete: true,
  }
});

// Configure DLQ alerts
queue.setDlqConfig({
  maxEntries: 1000,
  maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
});

// Regular jobs (buffered writes - high throughput)
await queue.add('send-email', { to: 'user@example.com' });

// Critical jobs (immediate disk write - no data loss)
await queue.add('process-payment', { orderId: '123' }, { durable: true });

// Worker with production settings
new Worker('production-tasks', async (job) => {
  await job.updateProgress(0, 'Starting...');

  try {
    const result = await processJob(job.data);
    await job.log(`Completed: ${JSON.stringify(result)}`);
    return result;
  } catch (error) {
    await job.log(`Error: ${error.message}`);
    throw error;
  }
}, {
  embedded: true,
  concurrency: 10,
  heartbeatInterval: 5000,
});
```
