# Deploy to Production: Docker, systemd and PM2

Production deployment guide for bunqueue with Docker, systemd, and PM2. Health checks, resource limits, backups, and monitoring for Bun.

Canonical: https://bunqueue.dev/blog/production-deployment/

---

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

<div class="bq-wrap bq-hero">
  <span class="bq-eyebrow">blog · operations</span>
  <h1 class="bq-hero-h1 bq-bench-h1">One instance, deployed <em>properly.</em></h1>
  <p class="bq-hero-sub">This article covers bunqueue's default single-instance SQLite deployment with Docker, systemd, and PM2. PostgreSQL 15–18 multi-broker mode is now also available, with 18.6 recommended; follow the current deployment guide for that topology.</p>
</div>

:::note[Storage update]
This article's commands and backup advice are for SQLite. For multiple active
brokers, see the current [deployment guide](/guide/deployment/) and
[storage guide](/guide/databases/).
:::

## Docker Deployment

The recommended approach for most teams:

```dockerfile
FROM oven/bun:1.4.3-alpine

WORKDIR /app

# Install bunqueue globally
RUN bun add -g bunqueue

# Create data directory
RUN mkdir -p /data

# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD wget -qO- http://localhost:6790/health || exit 1

EXPOSE 6789 6790

CMD ["bunqueue", "start", \
  "--tcp-port", "6789", \
  "--http-port", "6790", \
  "--data-path", "/data/bunqueue.db"]
```

Run with a persistent volume:

```bash
docker run -d \
  --name bunqueue \
  -p 6789:6789 \
  -p 6790:6790 \
  -v bunqueue-data:/data \
  --restart unless-stopped \
  bunqueue-server
```

<Aside type="caution">
  Always mount the data directory as a Docker volume. Without it, your SQLite database (and all
  jobs) are lost when the container restarts.
</Aside>

## systemd Service

For bare-metal or VPS deployments:

```ini
[Unit]
Description=bunqueue Job Queue Server
After=network.target

[Service]
Type=simple
User=bunqueue
Group=bunqueue
WorkingDirectory=/opt/bunqueue
ExecStart=/usr/local/bin/bun run bunqueue start \
  --tcp-port 6789 \
  --data-path /var/lib/bunqueue/queue.db
Restart=always
RestartSec=5

# Resource limits
LimitNOFILE=65535
MemoryMax=1G

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

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

```bash
sudo systemctl enable bunqueue
sudo systemctl start bunqueue
sudo systemctl status bunqueue
```

## Environment Variables

Configure bunqueue through environment variables or a [configuration file](/guide/configuration/) in production:

```bash
# Server
TCP_PORT=6789
HTTP_PORT=6790
HOST=0.0.0.0
BUNQUEUE_DATA_PATH=/var/lib/bunqueue/queue.db

# Authentication
AUTH_TOKENS=your-secret-token-here

# Timeouts
SHUTDOWN_TIMEOUT_MS=30000
WORKER_TIMEOUT_MS=30000

# S3 Backup
S3_BACKUP_ENABLED=1
S3_BUCKET=my-bunqueue-backups
S3_ACCESS_KEY_ID=your-key
S3_SECRET_ACCESS_KEY=your-secret
S3_REGION=us-east-1
S3_BACKUP_INTERVAL=21600000  # Every 6 hours
S3_BACKUP_RETENTION=7         # Keep the last 7 backups
```

## Health Checks

bunqueue exposes HTTP health endpoints:

```bash
# Basic health check
curl http://localhost:6790/health
# Returns: { "ok": true, "status": "healthy", "uptime": 3600,
#            "version": "...", "queues": {...}, "connections": {...} }

# Bare liveness probes (200 OK, no body inspection needed)
curl http://localhost:6790/healthz
curl http://localhost:6790/ready
```

From your application, verify the TCP side too:

```typescript
const queue = new Queue('test', {
  connection: { host: 'localhost', port: 6789 },
});
await queue.waitUntilReady(); // Pings the server
```

Use the HTTP health endpoint for load balancer checks, Docker HEALTHCHECK, and Kubernetes liveness probes.

## Graceful Shutdown

bunqueue handles SIGTERM for graceful shutdown:

<Steps>
  1. Stop accepting new connections 2. Wait for active jobs to complete (up to
  `SHUTDOWN_TIMEOUT_MS`) 3. Flush the write buffer to SQLite 4. Close the database 5. Exit cleanly
</Steps>

```bash
# Docker stop sends SIGTERM, waits 10s, then SIGKILL
docker stop bunqueue

# For longer running jobs, increase stop timeout
docker stop -t 60 bunqueue
```

<Aside type="tip">
  Set `SHUTDOWN_TIMEOUT_MS` to match your longest-running job. If a payment processing job takes up
  to 60 seconds, set the timeout to at least 65000ms.
</Aside>

## Resource Sizing

bunqueue's memory usage scales with the number of in-flight jobs:

| In-Flight Jobs | Approximate RAM |
| -------------- | --------------- |
| 1,000          | ~50 MB          |
| 10,000         | ~200 MB         |
| 100,000        | ~800 MB         |
| 1,000,000      | ~3 GB           |

The SQLite database size depends on job data size and retention settings. A typical deployment with `removeOnComplete: true` stays compact.

## Monitoring Checklist

Essential metrics to track in production:

```bash
# Queue metrics (via HTTP API)
curl http://localhost:6790/metrics

# Prometheus format
curl http://localhost:6790/prometheus
```

Key metrics to alert on:

- **DLQ size growing** - indicates systematic failures
- **Active jobs count > expected** - jobs may be stalled
- **Memory usage approaching limit** - adjust `maxEntries` settings
- **Waiting queue depth** - add more workers or increase concurrency

## Backup Strategy

Enable S3 backup for disaster recovery:

```bash
S3_BACKUP_ENABLED=1
S3_BACKUP_INTERVAL=21600000  # Every 6 hours
S3_BACKUP_RETENTION=7         # Keep the last 7 backups
```

For critical deployments, also consider:

- Filesystem-level snapshots (LVM, ZFS, or cloud provider snapshots)
- Replicating the SQLite file to a secondary location
- Monitoring backup success/failure with alerts