Run Workers on CircleCI
Goal: Run OpenCodeHub background workers using CircleCI instead of GitHub Actions.
OpenCodeHub’s worker (scripts/worker.ts) is a long-running background process that continuously processes:
- Merge queue items
- Mirror sync jobs
- Cleanup tasks
- Webhook deliveries
Since CircleCI is primarily a CI/CD platform (not a background process host), there are three approaches to run the worker on CircleCI infrastructure.
Approach 1: Self-Hosted CircleCI Runner (Recommended)
Section titled “Approach 1: Self-Hosted CircleCI Runner (Recommended)”Use CircleCI’s self-hosted runner on your own VPS. The runner stays online and executes the worker process continuously.
Why This Works Best
Section titled “Why This Works Best”- The runner is a persistent agent on your server
- Can run long-lived processes like the OpenCodeHub worker
- You control the machine (RAM, disk, Docker access)
- Can also run CI/CD jobs for repositories
Step 1: Install CircleCI Runner on Your VPS
Section titled “Step 1: Install CircleCI Runner on Your VPS”# Download and install the runnercurl -sSL https://runner.circleci.com/install.sh | bash
# Configure the runnersudo mkdir -p /var/lib/circleci-runnercat > /var/lib/circleci-runner/launch-agent-config.yaml << 'EOF'api: auth_token: "${CIRCLECI_RUNNER_TOKEN}"runner: name: opencodehub-worker-1 working_directory: /var/lib/circleci-runner/workdir cleanup_working_directory: trueEOF
# Start the runner servicesudo circleci-runner service installsudo circleci-runner service startStep 2: Create .circleci/config.yml in OpenCodeHub Repo
Section titled “Step 2: Create .circleci/config.yml in OpenCodeHub Repo”version: 2.1
executors: och-worker: machine: image: ubuntu-2204:current resource_class: your-namespace/opencodehub-runner # Your self-hosted runner
jobs: # ────────────────────────────────────────────── # Job 1: Run Background Worker (Continuous) # ────────────────────────────────────────────── worker: executor: och-worker environment: NODE_ENV: production DATABASE_URL: ${DATABASE_URL} REDIS_URL: ${REDIS_URL} JWT_SECRET: ${JWT_SECRET} WORKER_INTERVAL: 5000 WORKER_HEALTH_PORT: 9090 steps: - checkout
- run: name: Install Bun command: | curl -fsSL https://bun.sh/install | bash echo 'export PATH="$HOME/.bun/bin:$PATH"' >> $BASH_ENV source $BASH_ENV
- run: name: Install dependencies command: | for i in 1 2 3; do bun install --frozen-lockfile && break echo "Attempt $i failed, retrying..." sleep 10 done
- run: name: Start OpenCodeHub Worker command: | echo "Starting OpenCodeHub background worker..." bun run scripts/worker.ts & WORKER_PID=$! echo "Worker PID: $WORKER_PID"
# Wait for health check to pass for i in $(seq 1 30); do if curl -sf http://localhost:9090/health; then echo "✅ Worker is healthy" break fi echo "Waiting for worker to start... ($i/30)" sleep 2 done
# Keep the job alive (CircleCI will show it as "Running") echo "Worker running. Press Ctrl+C to stop." wait $WORKER_PID no_output_timeout: 24h # Keep worker running for up to 24 hours
- run: name: Worker crashed - restart logic command: | echo "Worker exited. Checking logs..." # CircleCI will automatically retry based on job settings when: on_fail
# ────────────────────────────────────────────── # Job 2: Run Worker Tasks On-Demand # ────────────────────────────────────────────── worker-tasks: executor: och-worker environment: NODE_ENV: production DATABASE_URL: ${DATABASE_URL} REDIS_URL: ${REDIS_URL} steps: - checkout
- run: name: Install Bun command: | curl -fsSL https://bun.sh/install | bash echo 'export PATH="$HOME/.bun/bin:$PATH"' >> $BASH_ENV source $BASH_ENV
- run: name: Install dependencies command: | for i in 1 2 3; do bun install --frozen-lockfile && break echo "Attempt $i failed, retrying..." sleep 10 done
- run: name: Run Merge Queue Processor command: | echo "Processing merge queues..." bun run scripts/worker.ts --once # If you add a --once flag # OR run a specific task script # bun run scripts/process-merge-queue.ts
- run: name: Run Mirror Sync command: | echo "Syncing mirrors..." # bun run scripts/sync-mirrors.ts
- run: name: Run Cleanup command: | echo "Running cleanup tasks..." # bun run scripts/cleanup-repos.ts
# ────────────────────────────────────────────── # Job 3: Health Check (Scheduled) # ────────────────────────────────────────────── worker-health-check: executor: och-worker steps: - run: name: Check Worker Health command: | curl -sf http://localhost:9090/health || exit 1 echo "✅ Worker is healthy"
workflows: version: 2
# Continuous worker (always running) worker-continuous: jobs: - worker: filters: branches: only: - main
# Scheduled tasks (every 5 minutes) worker-scheduled: triggers: - schedule: cron: "*/5 * * * *" filters: branches: only: - main jobs: - worker-tasks
# Health check every hour worker-health: triggers: - schedule: cron: "0 * * * *" filters: branches: only: - main jobs: - worker-health-checkStep 3: Set Environment Variables in CircleCI
Section titled “Step 3: Set Environment Variables in CircleCI”Go to Project Settings → Environment Variables:
| Variable | Value | Description |
|---|---|---|
DATABASE_URL | postgresql://... | Your PostgreSQL connection string |
REDIS_URL | redis://... | Your Redis connection string |
JWT_SECRET | ... | JWT secret |
SESSION_SECRET | ... | Session secret |
WORKFLOW_SECRET_ENCRYPTION_KEY | ... | For CI secrets |
CIRCLECI_RUNNER_TOKEN | ... | Runner registration token |
Approach 2: CircleCI Scheduled Pipelines (No Self-Hosted Runner)
Section titled “Approach 2: CircleCI Scheduled Pipelines (No Self-Hosted Runner)”If you don’t want to manage a self-hosted runner, use CircleCI’s scheduled pipelines to run the worker periodically.
Limitations
Section titled “Limitations”- Jobs have a maximum runtime (5 hours on free plan, longer on paid)
- Worker will restart every 5 hours
- Not truly “continuous” but works for light workloads
.circleci/config.yml
Section titled “.circleci/config.yml”version: 2.1
executors: bun-executor: docker: - image: oven/bun:1 resource_class: medium
jobs: merge-queue-worker: executor: bun-executor environment: NODE_ENV: production DATABASE_URL: ${DATABASE_URL} REDIS_URL: ${REDIS_URL} WORKER_INTERVAL: 5000 WORKER_STALE_TIMEOUT: 300000 steps: - checkout
- run: name: Install dependencies command: | for i in 1 2 3; do bun install --frozen-lockfile && break echo "Attempt $i failed, retrying..." sleep 10 done
- run: name: Run Merge Queue Worker command: | echo "Starting worker for 4.5 hours..." timeout 16200 bun run scripts/worker.ts || true echo "Worker cycle complete. Will restart on next schedule." no_output_timeout: 4.5h
mirror-sync: executor: bun-executor environment: NODE_ENV: production DATABASE_URL: ${DATABASE_URL} steps: - checkout
- run: name: Install dependencies command: bun install --frozen-lockfile
- run: name: Sync Mirrors command: bun run scripts/worker.ts --task=mirror-sync
cleanup: executor: bun-executor environment: NODE_ENV: production DATABASE_URL: ${DATABASE_URL} steps: - checkout
- run: name: Install dependencies command: bun install --frozen-lockfile
- run: name: Run Cleanup command: bun run scripts/worker.ts --task=cleanup
workflows: # Run merge queue worker every 5 hours merge-queue-schedule: triggers: - schedule: cron: "0 */5 * * *" filters: branches: only: main jobs: - merge-queue-worker
# Run mirror sync every hour mirror-sync-schedule: triggers: - schedule: cron: "0 * * * *" filters: branches: only: main jobs: - mirror-sync
# Run cleanup daily at 3 AM cleanup-schedule: triggers: - schedule: cron: "0 3 * * *" filters: branches: only: main jobs: - cleanupApproach 3: CircleCI Webhook-Triggered Worker
Section titled “Approach 3: CircleCI Webhook-Triggered Worker”Trigger the worker via API/webhook when specific events happen (push, PR created, merge queue updated).
API Triggered Workflow
Section titled “API Triggered Workflow”version: 2.1
executors: bun-executor: docker: - image: oven/bun:1
parameters: event_type: type: string default: "merge-queue" repo_id: type: string default: "" pr_id: type: string default: ""
jobs: process-event: executor: bun-executor environment: NODE_ENV: production DATABASE_URL: ${DATABASE_URL} REDIS_URL: ${REDIS_URL} steps: - checkout
- run: name: Install dependencies command: bun install --frozen-lockfile
- run: name: Process Event command: | echo "Processing event: << pipeline.parameters.event_type >>" case "<< pipeline.parameters.event_type >>" in merge-queue) bun run scripts/worker.ts --task=merge-queue --repo-id=<< pipeline.parameters.repo_id >> ;; mirror-sync) bun run scripts/worker.ts --task=mirror-sync ;; cleanup) bun run scripts/worker.ts --task=cleanup ;; *) echo "Unknown event type" exit 1 ;; esac
workflows: event-driven-worker: when: equal: [ webhook, << pipeline.trigger_source >> ] jobs: - process-eventTrigger via API from OpenCodeHub
Section titled “Trigger via API from OpenCodeHub”In your OpenCodeHub webhook handler, trigger CircleCI:
// When merge queue is updatedtransportToCircleCI({ event_type: "merge-queue", repo_id: repository.id, pr_id: pullRequest.id,});
async function transportToCircleCI(params: { event_type: string; repo_id: string; pr_id?: string }) { const response = await fetch( `https://circleci.com/api/v2/project/gh/${org}/${repo}/pipeline`, { method: "POST", headers: { "Circle-Token": process.env.CIRCLECI_API_TOKEN!, "Content-Type": "application/json", }, body: JSON.stringify({ branch: "main", parameters: params, }), } ); return response.json();}Approach Comparison
Section titled “Approach Comparison”| Feature | Self-Hosted Runner | Scheduled Pipelines | Webhook-Triggered |
|---|---|---|---|
| Continuous? | ✅ Yes | ⚠️ Every 5 hours | ✅ Event-driven |
| Setup Complexity | Medium (VPS needed) | Low | Medium |
| Cost | VPS cost only | CircleCI compute minutes | CircleCI compute minutes |
| Best For | Production workloads | Light workloads | Event-specific tasks |
| Docker Support | ✅ Full | ✅ Yes | ✅ Yes |
| Persistence | ✅ Local disk | ❌ Ephemeral | ❌ Ephemeral |
Recommended Setup for Production
Section titled “Recommended Setup for Production”Architecture
Section titled “Architecture”VPS (e.g., Hetzner/DigitalOcean)├── OpenCodeHub Main App (port 4321)├── PostgreSQL (port 5432)├── Redis (port 6379)└── CircleCI Self-Hosted Runner └── Runs: bun run scripts/worker.tsWhy Self-Hosted Runner Wins
Section titled “Why Self-Hosted Runner Wins”- Persistence — Runner stays on your VPS, has access to local git repos
- Network — Can reach PostgreSQL/Redis on
localhost - Cost — No CircleCI compute minutes used
- Control — You manage the machine
Quick Start Script
Section titled “Quick Start Script”#!/bin/bash# 1. Install CircleCI runnercurl -sSL https://runner.circleci.com/install.sh | bash
# 2. Create runner configmkdir -p /var/lib/circleci-runnercat > /var/lib/circleci-runner/launch-agent-config.yaml << EOFapi: auth_token: "$CIRCLECI_RUNNER_TOKEN"runner: name: "opencodehub-worker-$(hostname)" working_directory: /var/lib/circleci-runner/workdir cleanup_working_directory: trueEOF
# 3. Install and start servicecircleci-runner service installcircleci-runner service start
# 4. Verifysystemctl status circleci-runnerMonitoring
Section titled “Monitoring”Worker Health Check
Section titled “Worker Health Check”# Check if worker is runningcurl http://localhost:9090/health
# Expected output{ "status": "healthy", "uptime": 86400, "lastQueueRun": "2026-05-02T12:00:00.000Z", "consecutiveErrors": 0, "shuttingDown": false}CircleCI Dashboard
Section titled “CircleCI Dashboard”Monitor runner status at:
https://app.circleci.com/settings/runnerAlerts
Section titled “Alerts”Set up alerts in CircleCI when:
- Runner goes offline
- Job fails more than 3 times
- Worker health check returns
unhealthy
Troubleshooting
Section titled “Troubleshooting”| Problem | Solution |
|---|---|
| ”Runner not picking up jobs” | Check runner token is correct. Verify runner is online in CircleCI dashboard. |
| ”Database connection refused” | Ensure DATABASE_URL uses correct host. For self-hosted runner on same VPS, use localhost. |
| ”Worker exits after 5 hours” | Scheduled pipeline limit. Use self-hosted runner for continuous operation. |
| ”No output for 10 minutes” | CircleCI kills idle jobs. The worker logs regularly, but add no_output_timeout: 24h. |
| ”Cannot find module” | Make sure bun install ran successfully. Check node_modules exists. |
| ”Permission denied on repos” | Runner user needs read/write access to OpenCodeHub’s repo storage directory. |
Next Steps
Section titled “Next Steps”- CircleCI Integration Guide — For CI/CD pipelines
- Deployment Guide — Production setup
- Merge Queue — How merge queue works