This doc describes how Rascal deploys rascald to a single host using
blue/green slots, what is running where, and what happens during cutover and
drain.
Detached runner containers now preserve in-flight runs across deploys and process restarts. Blue/green remains in place for readiness-checked cutover, webhook/API continuity, and rollback safety rather than for keeping active runs alive.
Rascal deploy uploads/builds these artifacts on the server:
/opt/rascal/rascald(Linux binary)/opt/rascal/runner/rascal-runner(Linux runner binary copied into image build context)/etc/systemd/system/rascal@.service(slot unit)/etc/rascal/rascal.env(shared runtime env)/etc/rascal/rascal-blue.envand/etc/rascal/rascal-green.env(slot env)/etc/caddy/Caddyfile+/etc/caddy/rascal-upstream.caddy(proxy target)- Docker images for the configured runner tags (defaults:
rascal-runner-goose-codex:latest,rascal-runner-codex:latest,rascal-runner-claude:latest, andrascal-runner-goose-claude:latest)
It also writes:
/etc/rascal/active_slotwithblueorgreen
Rascal host setup is split into three phases:
provision: create the VM and network resources onlybootstrap: install OS-level dependencies and host prerequisitesdeploy: upload Rascal artifacts, build runner images, switch slots, and reload services
The source of truth for host packages now lives in
internal/deploy/assets/bootstrap_host.sh. Package additions should be made
there, not inline in deploy.go.
rascal@bluelistens on127.0.0.1:18080rascal@greenlistens on127.0.0.1:18081- Caddy proxies external traffic to the currently selected slot
- Caddy also enforces request-shape guardrails before
rascaldsees traffic: method/path/header gating, request body size caps, header-size caps, and server-side read/write timeouts - Legacy single-unit
rascalservice mode is not supported - Slot identity is set by env:
rascal@bluegetsRASCAL_SLOT=bluerascal@greengetsRASCAL_SLOT=green
Default deployed env also includes agent session persistence knobs (enabled by default):
RASCAL_TASK_SESSION_MODE=allRASCAL_TASK_SESSION_ROOT=/var/lib/rascal/agent-sessionsRASCAL_TASK_SESSION_TTL_DAYS=14RASCAL_RUNNER_IMAGE_GOOSE_CODEX,RASCAL_RUNNER_IMAGE_CODEX,RASCAL_RUNNER_IMAGE_CLAUDE, andRASCAL_RUNNER_IMAGE_GOOSE_CLAUDEset the runtime-specific runner images- Docker runner hardening defaults to
baselinewith:RASCAL_RUNNER_DOCKER_SECURITY_MODE=baseline,RASCAL_RUNNER_DOCKER_CPUS=2,RASCAL_RUNNER_DOCKER_MEMORY=4g,RASCAL_RUNNER_DOCKER_PIDS_LIMIT=256 strictcurrently adds a size-bounded/tmpmount viaRASCAL_RUNNER_DOCKER_TMPFS_TMP_SIZE- Runner secrets default to file-scoped injection:
RASCAL_RUNNER_ALLOW_ENV_SECRETS=falsemounts per-run secrets read-only at/run/rascal-secrets RASCAL_AGENT_RUNTIMEis optional and overrides the default runtime when set
Given active slot A and inactive slot B, deploy does:
- Build
rascaldfor Linux and upload artifacts. - Build
rascal-runnerfor Linux and upload artifacts. - Upload bootstrap assets and execute
bootstrap_host.shto ensure base packages (docker,caddy,curl,sqlite3,ripgrep) and host layout. - Install uploaded
rascal-runnerinto/opt/rascal/runner/rascal-runner. - Build/update runner images on host.
- Install/update systemd unit and env files.
- If slot
Bis still draining from the previous deploy, reclaim it first: cancel its remaining active runs, allow a short cleanup window, and stop it. - Start/restart slot
B. - Wait for slot
Breadiness (/readyzonBport). - Update Caddy upstream to slot
Band reload Caddy. - Verify proxy readiness via Caddy.
- Write
/etc/rascal/active_slot = B. - Signal old slot
Ainto deploy-drain mode. - Keep new slot unit enabled/active.
Important: deploy success is no longer coupled to waiting for old-slot run completion.
Detached execution means blue/green is no longer required to preserve active task execution during deploy. Its remaining value is:
- readiness-checked cutover before traffic moves
- rollback if proxy activation fails
- overlap safety while both slots are briefly alive
- avoiding API/webhook downtime during
rascaldreplacement
Deploy cutover uses an explicit two-slot policy:
- The immediately previous slot is allowed to drain indefinitely.
- The next deploy may reclaim that draining slot if it is needed again.
Deploy drain (SIGUSR1) does this:
- Enters draining mode.
- Stops accepting new work and shuts down HTTP listeners.
- Keeps supervising any active runs already assigned to that slot.
- Uses no fixed timeout cancellation.
- Exits only after those active runs finish, unless a later deploy reclaims it.
Deploy reclaim (SIGUSR2) is used only when the inactive slot is still draining
during the next deploy:
- Enter drain mode and stop HTTP listeners.
- Stop run supervisors (release in-memory cancel funcs) but do not touch detached containers.
- Exit immediately. Detached containers continue executing.
- The new slot adopts them via
RecoverRunningRuns()on startup. - Stop the slot so deploy can reuse it.
Generic host/service shutdown is separate from deploy drain and may still use a bounded wait on exit.
- Container entrypoint script is intentionally minimal (
runner/entrypoint.sh). - It only executes
/usr/local/bin/rascal-runner. - Task workflow behavior (git/agent/PR/meta handling) is implemented in Go in
cmd/rascal-runner.
During overlap, only active slot may process webhooks:
rascaldreads/etc/rascal/active_slotper webhook request.- If instance slot (
RASCAL_SLOT) does not match active slot, webhook is accepted-but-skipped.
Additional safeguards:
- Webhook delivery dedupe is atomic claim/finalize (no check-then-insert race).
- Run start is DB-atomic (
queued -> running) with task-level exclusivity, so two instances cannot both start work for the same queued run/task. - Detached execution handles are persisted, so startup recovery can adopt active runs immediately after slot rotation.
- Cancel intent is persisted in
run_cancels. - Active cancellation stops the detached container by persisted execution handle, even after supervision handoff.
- Final run state is written after terminal observation/finalization.
- Containers are explicitly removed during terminal cleanup.
If Caddy reload/readiness fails after switching upstream during deploy:
- Deploy attempts rollback:
- restore upstream to previous slot
- reload/restart Caddy
- stop new slot and restart previous slot
ssh root@HOST 'cat /etc/rascal/active_slot'
ssh root@HOST 'systemctl status rascal@blue --no-pager'
ssh root@HOST 'systemctl status rascal@green --no-pager'
ssh root@HOST 'cat /etc/caddy/rascal-upstream.caddy'
ssh root@HOST 'curl -fsS http://127.0.0.1:18080/readyz || true'
ssh root@HOST 'curl -fsS http://127.0.0.1:18081/readyz || true'Example: blue is active and running a job, deploy is triggered.
- Deploy prepares
greenand passesgreenreadiness. - Caddy upstream switches to
green. active_slotflips togreen.bluegets a deploy-drain signal and stops accepting new work.bluekeeps supervising its active run until that run finishes.- Deploy returns success without waiting for
blueto finish. - If another deploy happens before
blueis done, deploy reclaimsblue, cancels its remaining work, and reuses that slot. - Otherwise
blueexits naturally after its active work completes.