Deploying Silas

A Silas app is an ordinary Rails app, so it deploys the way your Rails apps already deploy — a single cheap VPS with Kamal works fine; there is no second runtime or control plane to stand up. The one thing you MUST get right: the Solid Queue worker has to run, because the durability contract (a turn surviving a crash / deploy) depends on it. A web-only deploy will accept turns and never run them.

Status: this is the reference configuration. It is correct by construction (worker wired, rescuer active, secrets injected) but has not yet been run against a live VPS end-to-end — that’s the final launch step.

1. The worker must run

Queue adapter: use Solid Queue. Silas’s durability and exactly-once guarantees require a durable, serializing, DB-backed adapter. Do not run on the in-process :async adapter (Rails’ development default) — it runs continuation retries on a thread pool concurrently with the original job, which double-executes steps and breaks exactly-once. Silas warns at boot if it detects the Async adapter. For scripts and single-process demos, the synchronous :inline adapter is safe (but has no durability).

rails g silas:install already appends the durability rescuer to config/recurring.yml:

production:
  silas_dead_job_rescuer:      # retries jobs failed by dead-worker reaping/pruning
    class: Silas::DeadJobRescuerJob
    schedule: every 30 seconds

Pick one way to run Solid Queue:

  • Simple (one container): run the worker inside Puma — set SOLID_QUEUE_IN_PUMA=true (Rails 8 default plugin). Fine for a demo box.
  • Recommended (durable): a dedicated bin/jobs process, so a kill -9 of the worker is isolated from the web tier and recovery is observable.

2. Kamal config (config/deploy.yml)

A standard rails new ships a Dockerfile and config/deploy.yml. Add a job role so the worker is its own container:

service: my-agent
image: you/my-agent
servers:
  web:
    - 1.2.3.4          # your £5/month VPS
  job:
    hosts:
      - 1.2.3.4
    cmd: bin/jobs      # the Solid Queue worker (+ dispatcher + scheduler)

registry:
  username: you
  password:
    - KAMAL_REGISTRY_PASSWORD

env:
  secret:
    - RAILS_MASTER_KEY
    - ANTHROPIC_API_KEY        # the :ruby_llm engine authenticates with this
  clear:
    SOLID_QUEUE_IN_PUMA: false # the job role runs the worker instead

# Persist SQLite + Solid Queue on a volume if you're using SQLite (or point at Postgres).
volumes:
  - "my_agent_storage:/rails/storage"

Secrets (Slack signing secret / bot token, model keys) go in .kamal/secrets (pulled from your env or a vault), never in the image.

3. Deploy

kamal setup     # first time
kamal deploy    # subsequent

Mid-deploy safety is built in: a turn interrupted by the SIGTERM of a rolling deploy re-enqueues and resumes (chaos-gated). Recovery latency after a hard crash ≈ SolidQueue.process_alive_threshold + the rescuer’s 30s cadence.

3½. Template apps deploy stock

An app generated from templates/*.rb needs no deploy surgery: Rails 8.1’s generated config/deploy.yml already runs Solid Queue inside Puma for the single-server SQLite shape (SOLID_QUEUE_IN_PUMA: true — that satisfies “the worker must run”), the template adds ANTHROPIC_API_KEY to Kamal’s secret list and .kamal/secrets, and config/recurring.yml already carries the rescuer. Set the two secrets in your shell, point deploy.yml at your server and registry, kamal setup. Scale beyond one box by moving to the dedicated worker role in §1 and a real Postgres in §2.

4. Cost

The whole thing runs on one VPS + your model spend. The engine authenticates with an API key for whatever provider you configure through RubyLLM. Your data and the ledger never leave your own Postgres, and there is no Silas platform bill or per-run metering.

5. Local development note (macOS + Postgres)

Solid Queue forks workers, and on macOS the pg gem needs PGGSSENCMODE=disable OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES in the environment or forked workers crash inside libpq. SQLite development needs nothing.


This site uses Just the Docs, a documentation theme for Jekyll.