# Deploying workers

> Register your jobs with runnel deploy, run workers in containers, on Kubernetes or as functions, and shut them down without losing work.

Source: https://galley.themes.dev/docs/guides/deploying/

Deploying Runnel has two halves. `runnel deploy` tells Runnel which jobs, workflows and schedules exist in an environment. Your workers, which you run wherever you run the rest of your code, do the work. They connect out to Runnel over HTTPS, so they need no open ports and no public address.

## runnel deploy

Run `runnel deploy` from your project, usually as a step in CI after your tests pass. It reads `runnel.config.ts`, loads your worker file and registers every definition it finds as a new version.

```bash
$ RUNNEL_API_KEY=rk_live_… runnel deploy --env production
Loading src/worker.ts
  jobs        send-invoice, sync-supplier, usage-report
  workflows   onboard-customer
  schedules   nightly-usage (0 2 * * * Europe/London), supplier-sync (*/15 * * * *)
Deployed v42 to production (3 jobs, 1 workflow, 2 schedules)
```

Versions are numbered per environment. Each run records the version it was enqueued under, which you can see in the dashboard and in the [Runs API](/docs/reference/runs-api/). Deploying does not interrupt runs in progress.

> **Note** Deploy definitions and roll out workers together. A run for a job that no worker knows about waits in the queue, and the dashboard flags it as "no worker", until one that has the job connects.

## Running workers

A worker is a long-running process that calls `runnel.worker(...).start()`. It needs two environment variables: `RUNNEL_API_KEY` for the environment, and `RUNNEL_ENV`. Run as many copies as you need; Runnel hands each run to exactly one of them.

```dockerfile title="Dockerfile"
FROM node:22-slim
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile --prod
COPY dist ./dist
ENV RUNNEL_ENV=production
STOPSIGNAL SIGTERM
CMD ["node", "dist/worker.js"]
```

The `concurrency` option on the worker sets how many runs one process handles at once. For I/O-bound jobs (HTTP calls, email, database writes) 50 is a reasonable start. For CPU-bound jobs, set it near the number of cores and scale with more processes instead.

## Kubernetes

Workers are an ordinary Deployment. Give them a grace period a little longer than the worker's own (below) and scale on queue depth rather than CPU: Runnel exposes `runnel_queue_depth` per job on the worker's metrics endpoint (`:9464/metrics` when `metrics: true`).

```yaml title="k8s/worker.yaml"
apiVersion: apps/v1
kind: Deployment
metadata: { name: runnel-worker }
spec:
  replicas: 3
  selector: { matchLabels: { app: runnel-worker } }
  template:
    metadata: { labels: { app: runnel-worker } }
    spec:
      terminationGracePeriodSeconds: 45
      containers:
        - name: worker
          image: registry.example/acme/worker:1.18.0
          envFrom: [{ secretRef: { name: runnel } }]
```

## Serverless functions

If you would rather not run long-lived processes, Runnel can push runs to an HTTPS endpoint instead. Export a handler and register its URL with `runnel deploy --push-url`:

```ts title="api/runnel.ts"
import { runnel } from '@runnel/sdk';
import { sendInvoice } from '../src/jobs/send-invoice';

export const POST = runnel.handler({ jobs: [sendInvoice] });
```

Each request carries one attempt and is signed with `Runnel-Signature`, which the handler checks. Mind your platform's time limit: a job whose `timeout` is longer than the function's maximum duration will be cut off. Workflows suit this model well, because each step can finish inside one short request.

## Graceful shutdown

When a worker receives `SIGTERM`, it stops taking new runs, lets in-flight runs finish and then exits. It waits up to 30 seconds by default; change it with `shutdownGracePeriod`.

```ts
runnel.worker({ jobs, concurrency: 50, shutdownGracePeriod: '40s' }).start();
```

Runs still going at the end of the grace period are aborted through `ctx.signal`, and their attempt is released back to the queue without counting as a failure. If a worker disappears without warning (a killed container, a lost node), Runnel notices the missing heartbeat within 30 seconds and requeues its runs.

> **Tip** Set your platform's grace period about five seconds above the worker's, so the worker exits on its own terms before it is killed.

## Environments and keys

Create a key per environment with `runnel keys create --env staging` and keep it in your secret store. Keys starting with `rk_dev_` only work against development; `rk_live_` keys are for everything else. See the [CLI reference](/docs/reference/cli/) for the full list of commands.
