Run and test
chalupa.yml reference
Configure a project declaratively without duplicating what Docker Compose already knows.
Reviewed 2026-10-04
This file belongs to the target project and sits next to its Compose file.
Commands find it in the current directory or pass it with --config; the
name field defines the Pulumi stack names.
Complete example
name: demo-harbor
compose: ./docker-compose.yml
services:
- api
- worker
- postgres
- redis
overrides:
api:
env:
LOG_LEVEL: info
mem_limit: 768m
worker:
command: ["bun", "run", "worker"]
mem_limit: 1g
compute:
region: nyc3
size: s-2vcpu-4gb
image: ubuntu-24-04-x64
prebaked: false # true when image is a golden snapshot with Docker and tools pre-installed
health:
url: http://localhost:8080/health
status: 200
timeoutSeconds: 300
ssh:
keys:
- demo-operator
allowedCidrs:
- 198.51.100.24/32
persist:
sizeGb: 20
idleShutdownMinutes: 45
seed: bun run seed:demo
cloud:
url: https://chalupa.example.com
heartbeatSeconds: 30
Do not put tokens in this file. The control-plane URL is not a secret; ingest keys are secrets and are delivered at runtime.
Top-level fields
Every key chalupa.yml accepts is in this table. Unknown keys are refused when
the config loads, so a typo cannot silently change a deployment.
| Field | Required | Purpose |
|---|---|---|
name |
yes | Slug used to name the environment and its Pulumi stacks. |
provider |
no | Host cloud. digitalocean (default), hetzner (BYOC), or chalupa (managed). Compute slugs (region, size, image) are interpreted by this provider. |
instance |
no | Optional suffix so one config can run several environments concurrently; the environment identity becomes name-instance (stack names, server, firewall, volume, and control-plane environment all follow). |
instances |
no | What a copy (chalupa up @COPY) gets on its first launch. data: auto (default: seed the copy's volume from the primary's newest data snapshot, else empty), clone (refuse the launch when there is no snapshot) or empty. cache: create (default when cache: is declared: from the primary's cache snapshot when there is one, else empty) or none. See copies. |
share |
no | { provider: ngrok }, the provider for remote URLs of services that declare expose.<service>.share. See expose and Shares. |
alternatives |
no | Up to 5 ordered fallback targets, each { provider?, region?, size? } with at least one key. When the primary target is refused for capacity, the pre-launch check prints them as suggestions; a launch never switches target by itself. See lifecycle. |
variants |
no | Named overlays for copies: chalupa up @light launches <name>-light with variants.light merged over the file (objects merge, lists and scalars replace). A variant cannot set name, instance, instances, variants, provider, credentials or cloud. |
compose |
yes, with services |
Path to the project's docker-compose.yml. Declared together with services, or both omitted for a workspace environment that runs only ci:. See compose and services. |
services |
yes, with compose |
Services sent to the remote environment. |
overrides |
no | Per-service adjustments applied after inference. See overrides. |
agentAccess |
no | Metadata-only read grants per selected service, for agent briefs. See agentAccess. |
expose |
no | Metadata about how each forwarded service is reached from your machine: protocol, the path chalupa open lands on, and a URL scheme for tcp services. See expose. |
probes |
no | Declared app probes: commands that print JSON counters, read on every heartbeat and charted on the environment page. At most 6. See probes. |
diagnostics |
no | Named read-only recipes that chalupa diagnose and chalupa evidence run inside a service on the box. At most 32. See diagnostics. |
compute |
no | Region, size, image, prebaked flag, health check and readiness probes. Accepts the deprecated droplet spelling with a warning. See compute. |
ssh |
yes, for your own cloud account | Authorized keys and firewall CIDRs. See ssh. |
tunnel |
no | extraPorts, onReuse, afterUp and local. See tunnel. |
boot |
no | Declarative host overlays: allowlisted drop-in files, once-only post-health commands, and the MongoDB replica set flag. See boot. |
persist |
no | Block storage with an independent lifecycle. See persist. |
cache |
no | A disposable volume for Docker images and build cache. See cache. |
apps |
no | Images you build yourself, and how a code change reaches a running one. See apps. |
cleanupRecipes |
no | Named, reviewed commands that clear or restore state between rounds. See cleanupRecipes. |
idleShutdownMinutes |
no | Idle window reported to Chalupa. See idleShutdownMinutes. |
seed |
no | Command that loads data, run locally through the tunnel or on the server. See seed. |
test |
no | Suites that chalupa test runs from your machine. See test. |
ci |
no | Declarative CI suites that run inside the server. Requires cloud.url. See Declarative CI. |
experiment |
no | A declared replay experiment: variants, rounds, reset (the name of a cleanupRecipes entry), trigger, measure, observeWindowMinutes, budgetMaxMinutes and onFinish. Run it with chalupa experiment plan|run|compare|report; see exporting an experiment run. |
inference |
no | Host-Ollama model on a GPU server. See Host-Ollama inference. |
model, loop |
no | Older GPU model and agent-loop declarations, kept so existing configs still load. chalupa loop was removed; new configs use inference. |
cloud |
no | Agent registration with a control plane: url, heartbeatSeconds, and services (named default or hidden, whether the launch report names your services on the Connect card). See cloud. |
credentials |
no | Which TinyVault project holds this config's own credentials (provider token, ingest key). See Choose the TinyVault source. |
session |
no | Ephemeral purpose, teardown deadline, warm window and bounded private-log policy. See session. |
observability |
no | Pinned, privacy-safe Monitor telemetry. See observability. |
report |
no | What chalupa report keeps of a failed run: a curated evidence pack, local by default. See report. |
provider: chalupa
Managed GPU provisions host Ollama through the console in Chalupa's account. The laptop authenticates with its account session; it needs no provider token.
compute.sizemust begpu-small,gpu-large, orgpu-max; raw provider slugs andautoare not accepted.compute.regiondefaults toca-east; accepted values areus-east,ca-east,eu-west, andus-south, subject to capacity. Older configs that spell a region by its provider slug still work.inferenceis required, including its context size and weight ceiling; the model defaults toqwen3.8:27b.session.expiresAfterMinutesis required and must be 15–480. The console enforces expiry;session.purposeis optional for managed hosts.sshis optional.ssh.identityFiledefaults to~/.config/chalupa/ssh/id_ed25519;upreads its.pubfile and detects the current public IPv4 /32. Account key selectors and configured CIDRs are not required.ssh.localPorts[11434]defaults to 11435.persist,compose, andservicesare not allowed. Managed hosts run host Ollama only.inference.cache.sizeGboptionally requests a weights cache.
See Managed GPU for a complete config and CLI commands.
compose and services
compose must point to the actual file. services uses the exact keys under
services: in Docker Compose:
compose: ./docker-compose.yml
services:
- api
- postgres
The preview fails if a service does not exist. When depends_on points to an
unselected service, Chalupa removes that reference; it does not include the
service implicitly.
Compose service labels
Compose remains the source of truth for databases, one-shot initialization, and log selection. Chalupa recognizes only these labels:
| Label | Accepted values | Purpose |
|---|---|---|
run.chalupa.role |
database, initializer, application, support |
Classifies the service in previews and the private console. |
run.chalupa.database.kind |
postgresql, mysql, mariadb, mongodb, redis, other |
Identifies the database family without inspecting credentials. |
run.chalupa.database.primary |
true, false |
Marks at most one selected database as primary. |
run.chalupa.initializes |
selected database service name | Declares the target of a one-shot initializer. |
run.chalupa.logs |
include, exclude |
Adds the service to the explicit private-log allowlist. |
Unknown run.chalupa.* labels fail validation so a typo cannot silently alter
the deployment plan. Known database image families are classified as advisory
metadata when labels are absent. Image inference never creates a database,
changes a command, generates SQL, or supplies credentials. All services remain
explicit entries in services.
Logs are excluded by default, including database and cache logs. An included
service receives a deterministic local journald sink. Chalupa rejects a
conflicting logging driver before provisioning; set the service to exclude
when its project-owned logging driver must remain unchanged. Only the
allowlisted classification is reported to the console. Arbitrary Compose
labels, environment values, container images, commands, and host identity are
not included in heartbeat metadata.
An environment can keep the same selection in chalupa.yml instead of the
project Compose file:
session:
purpose: test
expiresAfterMinutes: 120
logs:
services:
api: include
noisy-support-service: exclude
retentionDays: 7
Both forms are supported. session.logs.services wins when it names the same
service as run.chalupa.logs; a service named by only one source retains that
source's value. Every configured service must also appear in the top-level
services selection.
One-shot database initialization
Initialization stays inside the project's Compose model:
services:
database:
image: postgres:17-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"]
interval: 2s
timeout: 3s
retries: 30
labels:
run.chalupa.role: database
run.chalupa.database.kind: postgresql
run.chalupa.database.primary: true
initialize:
image: ghcr.io/example/demo-initializer:2026-07-24
command: ["/app/initialize"]
labels:
run.chalupa.role: initializer
run.chalupa.initializes: database
depends_on:
database:
condition: service_healthy
api:
image: ghcr.io/example/demo-api:2026-07-24
labels:
run.chalupa.role: application
run.chalupa.logs: include
depends_on:
initialize:
condition: service_completed_successfully
The initializer image, command, migrations, seed data, and credentials belong to the target project. Chalupa validates the dependency graph but never synthesizes them. Every selected service, including an initializer, must use a published image; see What Compose features are supported.
Compose $VAR and ${VAR...} interpolation is resolved from the operator
environment. chalupa up captures only names referenced by selected services
and their command/env overrides, then replays that exact snapshot inside the
Pulumi program. Unselected-service variables and unrelated shell state are
not forwarded. Runtime-owned names (CONFIG, CHALUPA_*, PULUMI_*, and
TVAULT_*) and known DigitalOcean credentials are forbidden. $$ remains a
literal Compose dollar escape. Volume mappings must remain literal so
persistent storage identity never comes from ambient state. The preview
redacts non-empty application values read from the shell; resolved tunnel
ports remain visible because they define the SSH command.
What Compose features are supported
Chalupa copies the selected services to a server whose only open port is SSH,
so it refuses Compose features that need files, privileges or networks the
server does not have. Every refusal happens offline, in chalupa preview,
before anything is created. Fields that are not listed here are copied to the
server unchanged.
Every selected service needs an image. Services are never built on the
server by Compose: publish the image first, or declare it under
apps so chalupa apps can build it.
Refused service fields. The preview stops with compose_feature_unsupported
when a selected service declares any of these:
| Group | Fields |
|---|---|
| Building | build |
| Networking and namespaces | networks, network_mode, links, external_links, ipc, pid, uts, userns_mode, cgroup |
| Privileges, devices and runtimes | cap_add, cap_drop, devices, device_cgroup_rules, security_opt, sysctls, runtime, gpus, isolation, credential_spec, use_api_socket |
| Files, secrets and composition | env_file, configs, secrets, extends, volumes_from |
| Orchestration and lifecycle | profiles, deploy, develop, post_start, pre_stop, provider, models |
env_file is the refusal people hit most. Pass values with
overrides.<service>.env instead; see
Application secrets.
Refused top-level keys. configs, include, networks and secrets.
Refused or rewritten at the service level.
| Feature | What happens |
|---|---|
privileged: true |
Refused. privileged: false is accepted. |
| Mounting a container-engine socket (Docker, containerd or Podman) | Refused. |
| UDP ports | Refused: only TCP can cross an SSH tunnel. |
| Ports without a host port, port ranges and IPv6 host addresses in short syntax | Refused. Write "8080:80" or the long syntax with published and target. |
Long port options other than app_protocol, host_ip, name, protocol, published, target |
Refused. |
| Two selected services publishing the same host port | Refused. |
| Host bind mounts and relative-path volumes | Refused. A bind mount a service can live without is dropped on purpose with overrides.<service>.omitBindMountTargets. |
| Anonymous volumes | Refused. Use a named volume. |
| Named volume options | Only ro, rw, nocopy and volume-nocopy are accepted. |
Top-level volumes that are external, set a custom name, use a driver other than local, or set driver_opts |
Refused. |
$VAR interpolation inside a volume mapping |
Refused: storage identity must be literal. |
Docker restart policies |
Removed with a warning, so no container restarts before Chalupa's metadata guard is active. Use overrides.<service>.restart: on-failure:N for a bounded, supervised restart. |
depends_on a service you did not select |
Removed with a warning. |
| Published ports | Rewritten to bind 127.0.0.1 on the server; the tunnel is how you reach them. |
ulimits accepts only nofile, nproc, core, memlock and stack.
overrides
Available overrides are env, mem_limit, and command:
overrides:
api:
env:
NODE_ENV: test
LOG_LEVEL: warn
mem_limit: 768m
command: ["bun", "run", "start:test"]
Entries under env replace Compose values with the same key. Use this
mechanism for non-sensitive behavior. Resolve secrets outside the versioned
file.
Four more overrides cover what a replay stack usually needs on its first boot:
overrides:
mongo:
ulimits:
nofile: { soft: 65536, hard: 65536 }
postgres:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
retries: 30
temporal:
waitFor: { postgres: healthy }
restart: on-failure:3
ulimitsis merged over the Composeulimitskey by key. Onlynofile(0–1,048,576),nproc,core,memlockandstackare accepted, as an integer or{soft, hard};-1means unlimited except fornofile. Compose files may declare the same subset directly.healthcheckreplaces the service's Compose healthcheck (test,interval,timeout,start_period,start_interval,retries).waitFormaps another selected service tostarted,healthyorcompleted, and becomes Compose's owndepends_oncondition, sodocker compose upholds the service until its dependency is ready. Waiting forhealthyon a service without a healthcheck, and wait cycles, are refused offline.restart: on-failure(oron-failure:N, N = 1–20, default 5) restarts the service after a non-zero exit, at most N times per boot. It is not a Docker restart policy: those stay stripped, because the Docker daemon starts policy-bearing containers before Chalupa's metadata guard is in place. A host unit (chalupa-restart-supervisor) that starts only after the Compose stack, and therefore after the guard, does the restarting and logs each attempt to the journal. A clean exit (code 0) is never restarted. Before stopping a supervised service by hand, runsystemctl stop chalupa-restart-supervisor, or it is started again.
Two more overrides adjust what is copied and how logs are written:
omitBindMountTargetsis a list of absolute container paths (1 to 64, unique). Each must match exactly one host bind mount of the service, which is dropped from the server's copy with a warning. Bind mounts are otherwise refused; named and anonymous volumes cannot be omitted this way.logging: keepstops Chalupa rewriting the service's logging driver to journald when its logs are collected. Use it only when the logging behaviour itself is what you are measuring.
chalupa preview shows the resulting Compose and lists supervised restarts
on their own line.
agentAccess
Optional, metadata-only grants per selected service. readOnly: true is the
only value — there is no write grant to express:
services: [api, worker, postgres]
agentAccess:
api:
readOnly: true
postgres:
readOnly: true
A grant marks which services an agent may inspect
(chalupa connect --format app-brief renders the brief). It changes nothing
at provision time: no port, credential or permission is derived from it, and
services without a grant simply do not appear in the brief. Grants may only
name selected services, and a workspace environment (no compose) has none.
expose
Optional, metadata-only, like agentAccess: it changes nothing on the server
(the Compose project and the firewall are untouched; only SSH is ever exposed).
It tells chalupa urls, chalupa open, chalupa exec --format env and the
launch report how to present a service you already forward.
services: [gateway, postgres, redis]
expose:
gateway:
protocol: http
open: /docs
postgres:
scheme: postgres
redis:
protocol: tcp
scheme: redis
visibility: private
| Field | Applies to | Meaning |
|---|---|---|
protocol |
any | http or tcp. Overrides the guess chalupa urls makes from the image and the port. |
open |
http |
A path starting with /. chalupa open <service> opens the service's address plus this path, and chalupa urls --json reports it as openPath. |
scheme |
tcp |
A URL scheme such as mongodb, postgres, amqp or redis. The service's url becomes <scheme>://127.0.0.1:<port> and chalupa exec --format env exports it as CHALUPA_<SERVICE>_URL, next to the unchanged CHALUPA_<SERVICE>_ADDR (host:port). |
visibility |
any | private is the only value. It is sent to the console with the service list so the Connect card does not offer the service for sharing. |
share |
http |
{ auth: console | none, ttlMinutes?: 15..1440 }. Lets chalupa share <service> publish the service at https://<service>--<environment>.box.chalupa.run (see Shares). Declaring it shares nothing by itself. console (the default to choose) admits only members of the environment's organization; none admits anyone with the URL and needs --confirm "share <environment> <service> publicly" every time. ttlMinutes is the default lifetime (60 when absent). Not with protocol: tcp or visibility: private. |
Keys are the service names chalupa urls prints: a selected Compose service,
ollama (with inference), or port-<n> for a tunnel.extraPorts entry. Any
other name is a config error. A key that parses but matches nothing that is forwarded (a service that
publishes no port, or a port-<n> whose port a Compose service already
publishes) is ignored with a warning on stderr from chalupa urls and the
launch report, because only the Compose inference can tell. The key is optional and has no defaults, so a
config without it behaves exactly as before. open is also sent to the console
(except with cloud.services: hidden); scheme never leaves your machine. A
shared service's hostname label, <service>--<environment> (plus ---public
for auth: none), must fit in 63 characters after lowercasing and turning _
into -, neither name may contain --, and a two-letter service name is
refused (it would look like an internationalized label); the config is refused
before anything runs otherwise. A top-level share: { provider: ngrok } picks
the share provider (ngrok, the only one available; naming vercel-ssh, which is reserved, is refused when the config is validated).
Not allowed with provider: chalupa.
probes
Optional, at most 6. A probe is a command that runs inside a selected compose service on every heartbeat and prints one flat JSON object of numeric counters on stdout; the environment page charts the counters next to the container timeline:
services: [api, worker]
probes:
orders:
service: api
command: "curl -fsS http://127.0.0.1:3000/metrics/counts | jq -c '{completed, queued}'"
queue:
service: worker
command: "redis-cli --raw --json LLEN jobs"
timeoutSeconds: 12
servicemust be one of the selectedservices; anything else is a config error before a server exists.command(max 500 chars) runs throughsh -lcinside the service container. It must print a single JSON object whose values are finite nonnegative numbers — strings, objects and arrays are dropped, and only the first 10 keys (sorted by key) are kept. Anything else — unparseable output, a non-object, a nonzero exit — is reported as an error reading, never a failed heartbeat.timeoutSecondsis an integer 1–30, default 5. The heartbeat runs probes sequentially, so the sum of the timeouts bounds how long a beat can take; a slow probe suite delays the next beat rather than overlapping it.- Probes are read-only by construction: the agent runs the command and
parses its stdout, nothing more. They are refused for managed hosts and
workspace (no
compose) environments. - Probes require
cloud.url— the readings travel in the heartbeat, and the charts live on the environment page of the console.
The command travels shell-quoted in /etc/chalupa/probes.json, so the
quoting you write is the quoting the service shell sees. The errors the
page can show are timeout, invalid (not a flat JSON object of numbers)
and exitN (the command exited with status N).
diagnostics
Optional, at most 32. A recipe is a named, read-only command you declare once
so chalupa diagnose and chalupa evidence can run it on the box, instead of a
campaign script re-implementing the same collector:
services: [mongodb, elasticsearch, api]
diagnostics:
mongo-rs-status:
service: mongodb
description: Replica set health
run: [mongosh, --quiet, --eval, "JSON.stringify(rs.status())"]
readOnly: true
es-health:
service: elasticsearch
run: [curl, -fsS, "http://127.0.0.1:9200/_cluster/health"]
readOnly: true
timeoutSeconds: 10
servicemust be one of the selectedservices. The command runs inside that service's container (docker compose exec -T).runis an argv array, never a shell string: there is no interpolation, no pipe and nosh -c. Shells and state-changing programs (sh,bash,env,xargs,sudo,rm,kill,chmod, ...) are refused at load, andcurlandwgetrefuse their write flags (-X,-d,--data,-F,-T,-o, ...), so a recipe can only GET.readOnly: trueis required. It is the author's statement that this recipe only reads state; leaving it out is a config error.timeoutSecondsis an integer 1-60 (default 20) andmaxBytescaps the captured output (1024-1048576, default 65536). JSON output is parsed and shown as JSON; anything else is shown as redacted text.- Only recipes declared here can ever run. The MCP server does not run them;
chalupa diagnose,chalupa evidence --liveand the snapshotdowntakes before the sink do. Recipes are refused for managed hosts and for workspace (nocompose) environments.
Declare recipes for state you would otherwise open a shell to check; see Logs, diagnose and evidence.
compute
compute describes the host to provision. region, size and image are
opaque provider slugs, so the same block works for any provider Chalupa grows
support for.
This block was called droplet in earlier versions. That spelling still parses
and is canonicalized to compute, but it raises a compute_key_deprecated
warning and will be removed. Setting both is an error rather than a silent
preference.
compute:
region: nyc3
size: s-2vcpu-4gb
health:
url: http://localhost:8080/health
status: 200
The health check runs inside the server, so it can use localhost. The schema
requires HTTP(S), loopback, and a URL without embedded credentials. It must
target a port published by one of the selected services; the preview fails
if the URL's port does not appear among the inferred tunnels. The check runs in
two places. On the server, cloud-init starts it (chalupa-health.service) and
the boot overlays that follow wait for it. After provisioning, chalupa up
opens a temporary tunnel, waits for the forwarded ports and for this URL to
answer with status, for up to timeoutSeconds, and fails if it never does.
A failed up leaves the server running and billing: inspect it, or run
chalupa down. chalupa test, chalupa exec and chalupa launch run the same
check through their own tunnel before they continue. It is a single endpoint
check, separate from compute.readiness, which up waits
for only with --wait.
The size directly affects estimated cost. Check the current provider price before creating the resource.
compute.prebaked is a boolean, default false. When true, the image
field must point to a golden snapshot that already ships Docker, tools, static
scripts, system users, and pre-enabled systemd units. cloud-init then skips
package installation and static provisioning entirely, writing only the
per-environment payload. Build the golden image with
chalupa snapshot build --config PATH --confirm "bake <name>" (it prints the
estimated cost and time first, and changes nothing without --confirm; with no
ssh.identityFile or CHALUPA_TASK_SSH_IDENTITY the ssh-agent must hold a key
registered in the account, checked before anything is created). The
agent, forwarder and Monitor on a prebaked box are still refreshed over SSH on
every chalupa up, so a golden image no longer freezes them at the day it was
baked; see how the runtime reaches the box.
Set compute.image: golden:latest (with prebaked: true) to follow the newest
golden this machine baked for the environment and region, instead of pasting a
snapshot id after every bake. chalupa snapshot status lists the goldens with
their age and the Chalupa and Monitor versions that built them, and
chalupa doctor --config warns when the one in use is older than 30 days or
was baked by an older Chalupa or Monitor than the config pins.
The golden image masks apt-daily and unattended-upgrades, so boxes booted
from it never run apt in the background; picking up OS updates means
re-baking. It also carries /etc/chalupa/golden.json (Chalupa version, bake
time, provider, Monitor version), so you can cat it on a box to see what the
image contains and how old it is.
compute.readiness
Typed probes a stack must ALL pass before it counts as up — the
replacement for hand-written wait-*.mjs scripts. Five shapes, not a DSL:
compute:
readiness:
- tcp: { service: postgres }
- http: { service: web, path: /healthz }
- http: { service: api, path: /, expect: { status: [200, 401] } }
- mongo: { service: mongo }
- amqp: { service: rabbit }
- cmd: { command: "node scripts/wait-es.mjs", timeoutSeconds: 30 }
A network target names a compose service — resolved to the same local
port chalupa tunnel forwards for it —, a remotePort that the tunnel
forwards (the way a tunnel.extraPorts entry is addressed, since
it has no service name), or an explicit host/port on your machine. The
cmd probe runs locally and exits 0 when ready; it is the escape hatch for
app-specific readiness. http passes on a 2xx or 3xx answer (redirects are
not followed), so a 404 or an error page is not ready. When a healthy app
answers something else, list the accepted codes with expect.status
([200, 401]); the list replaces the default range. mongo sends the driver handshake (an
OP_MSG hello, answered by MongoDB 3.6 through 8) and amqp sends the 0-9-1
protocol header and expects the server's Connection.Start (what a healthy
RabbitMQ answers), so no client libraries are needed.
A 200 is not always ready
An http probe can also read the body. json selects one value by a dotted
path (status, nodes[0].state) and compares it with in (a list) or
equals (one value) — exactly one of the two:
compute:
readiness:
- name: elasticsearch
http:
service: elasticsearch
path: /_cluster/health
json: { path: status, in: [yellow, green] }
A cluster that answers 200 while its status is red is not ready. The
status check runs first, then the body; a body that is not JSON, or a path
that is missing, fails with the value it found.
When the probe counts
Three options sit beside the probe kind (not inside it) and work on every kind:
| Option | Meaning |
|---|---|
name |
A slug, unique in the list, so chalupa wait --probe NAME and the ready-at lines can refer to the probe. |
timeoutSeconds |
How long this probe has to become ready, 1–1800. It replaces the wait-wide timeout (default 300 s) for this probe only; the first probe to run out of time ends the wait. On a cmd probe the timeout inside cmd is the ceiling for one attempt of the command, a different thing. |
stable |
{ count, intervalSeconds }: the probe is ready only after count (2–60) consecutive passes, intervalSeconds (1–60, default 2) apart. One miss starts the count over. Use it for a database that answers once during init and then restarts. |
compute:
readiness:
- name: mongo
mongo: { service: mongo }
stable: { count: 5, intervalSeconds: 2 }
- name: web
http: { service: web, path: /healthz }
timeoutSeconds: 600
Wait with chalupa wait once the
tunnel is up: it polls every probe until all pass together (a service that
reboots during seeding is re-checked), prints one line per probe with the
time it became ready, and exits 1 on timeout. chalupa preview shows the
declared probes offline. The same probes gate chalupa up --wait, the specs
of chalupa test, and the command of chalupa exec. See
Make servers ready faster.
tunnel
tunnel:
extraPorts: [8080, 9005, 9012]
onReuse: none
afterUp: none
| Field | Default | Meaning |
|---|---|---|
extraPorts |
none | One to 16 distinct loopback ports on the server that sit outside the Compose project (an app overlay, a host service). They ride the same supervised forwards as the inferred ports. Refused for provider: chalupa. |
onReuse |
keep |
keep or none. Whether a warm chalupa up that reuses a server re-opens the supervised tunnel. |
afterUp |
keep |
keep or none. Whether a cold chalupa up --wait leaves its readiness tunnel running. |
local |
canonical |
canonical or leased. canonical binds each forward on the same port number as the server (or its ssh.localPorts entry), so two environments of one Compose project collide. leased gives every forward a stable port in 40000-48999, saved in ~/.config/chalupa/leases/<environment>.json; no other environment and no running process shares it, and ssh.localPorts entries still win. The GPU Ollama port gets a lease too, so the 11435 default is not applied. chalupa urls prints the result. |
transport |
lazy with local: leased |
lazy or eager; only valid with local: leased. lazy: Chalupa itself listens on each leased port and opens that service's SSH connection on the first connection, so a tunnel at rest holds no connection to the server and the idle shutdown can fire (the server counts any open SSH connection as activity). eager keeps one SSH connection per port for as long as the tunnel runs, as canonical does. |
idleGraceMinutes |
5 |
1 to 60; only with the lazy transport. How long a service's SSH connection stays open after its last client connection closes. Keep it below idleShutdownMinutes: the config diagnostics warn when it is not. chalupa urls shows each service as idle (listening, no SSH yet), up, or failing (the SSH connection failed several times in a row, so Chalupa retries with a growing delay instead of once per connection). |
With local: leased, chalupa proxy start adds readable names for the web
services: http://<service>.<environment>.localhost:7355. See
chalupa proxy.
Each field is optional, and tunnel: { onReuse: none } on its own is valid.
Ports reach the tunnel through the stack's recorded outputs, so adding
extraPorts to a running environment takes effect on the next chalupa up,
not on the live server. How the supervised tunnel behaves, and why you would
set onReuse or afterUp, is in
Supervised tunnels.
ssh
For DigitalOcean, ssh.authorizedKeys accepts inline ssh-ed25519 … public
key lines. Each entry creates a deterministic account SSH key named
chalupa-<environment>-operator-<index>; its ID joins any keys selected by
ssh.keys or ssh.useAllAccountKeys on the server. Inline keys can also be
used alone. Private keys and authorized_keys command options are not accepted.
When omitted, account-key behavior is unchanged.
Authorize specific keys whenever possible:
ssh:
keys:
- demo-operator
allowedCidrs:
- 198.51.100.24/32
keys accepts names, IDs, or fingerprints from the account. allowedCidrs
controls the port 22 firewall and is required; Chalupa never supplies a
world-wide default. The example range belongs to RFC 5737. Replace it with the
narrowest operator-controlled source range, normally a /32 for one IPv4
address or /128 for one IPv6 address.
An isolated demo can use this explicit opt-in instead:
ssh:
useAllAccountKeys: true
allowedCidrs:
- 198.51.100.24/32
keys and useAllAccountKeys are mutually exclusive. Chalupa never
implicitly authorizes every account key. Explicit 0.0.0.0/0 or ::/0
entries produce a high-visibility validation warning and are reserved for
isolated synthetic diagnostics, not real environments.
identityFile names the private key every lifecycle SSH uses (tunnel,
bootstrap, teardown), absolute or ~/-relative to the operator home.
chalupa setup writes it as ~/.config/chalupa/ssh/id_ed25519, Chalupa's own
keypair: it never reuses a personal key, and only the public half is
registered with the provider. An explicit SSH_IDENTITY environment
variable still wins over the config value.
boot
Host-level tweaks that used to be SSH hand-edits — an nginx socket override,
a memory bump, a replica set — declared in the config so they survive every
up instead of dying with the server on down:
boot:
files:
- path: /etc/nginx/conf.d/upstream.conf
content: |
upstream app { server 127.0.0.1:8080; }
- path: /etc/systemd/system/redis.service.d/override.conf
content: |
[Service]
Environment=REDIS_MAXMEMORY=1g
commands:
- command: docker compose --project-directory /opt/chalupa exec -T api app reload
timeoutSeconds: 60
mongoReplicaSet: true
files (at most 16, 64 KiB each) are written by cloud-init as root with
0644 permissions before anything runs. Destinations are restricted to an
allowlist of drop-in locations: /etc/nginx/conf.d/*.conf,
/etc/systemd/system/ (drop-in shape only — a .conf inside a *.d
directory or a plain .conf; never a full unit, which would be a second,
unreviewed way to execute code at boot), and /etc/chalupa/overrides/ for
anything an app command reads. Any other path is a config error at load
time, offline, before a server exists.
commands (at most 8, 2000 characters each, timeoutSeconds 10–600
defaulting to 120) run once, as root, through bash -lc, after the stack
health probe has passed. A systemd oneshot guards them with a marker file,
so reboots and repeated provisioning never re-run what already succeeded;
the marker is written only after every command exited 0, so a half-applied
overlay retries on the next boot — write idempotent commands. Each
command's exit status lands in the journal (journalctl -u chalupa-overlays.service), and the first failure stops the unit without
blocking the stack itself: nothing in the Compose stack depends on it.
mongoReplicaSet: true configures a selected MongoDB service (detected from
its image or its run.chalupa.database.kind label) as a single-node replica
set named rs0, so transactions and change streams work. mongod gets
--replSet rs0 at Compose generation time, and a one-time unit initiates
the set after the stack is healthy — idempotently, so a persistent data
volume that was already initiated is left alone. The flag is refused
offline when no MongoDB service is selected.
When the service already declares a command (in Compose or through
overrides), the flags are appended rather than replacing it:
--replSet rs0 unless a --replSet is already present, and --bind_ip_all
unless a --bind_ip or --bind_ip_all is. This applies only to a command
that is mongod's own argument list (it starts with mongod or a flag); a
wrapper such as bash -c ... is still refused, because the flags would
reach the shell instead of mongod.
persist
persist:
sizeGb: 20
backup:
retentionCount: 3
verifyCommand: ls -la /mnt/chalupa-verify
onDemand:
retireAfterIdleHours: 24
| Field | Required | Meaning |
|---|---|---|
sizeGb |
yes | Size of the protected data volume, 1 to 16,384 GiB. The volume lives in the <name>-data stack and survives chalupa down. |
backup.retentionCount |
with backup |
How many snapshots to keep, 1 to 52. Backups are opt-in; without this block no snapshot task runs. |
backup.verifyCommand |
no | Read-only command that proves a snapshot restores. At most 500 characters. |
backup.verifyTimeoutSeconds |
no | Time limit for verifyCommand, 5 to 600. |
onDemand.retireAfterIdleHours |
no | 1 to 720. Opt in to a "warm day": once this machine's session ledger shows the environment unused for this many hours, chalupa storage sweep snapshots the volume, verifies the snapshot at the provider and then destroys the volume; the next chalupa up restores it from the newest snapshot. Requires backup, DigitalOcean, and no inference.cache. See Warm-day storage. |
Increasing capacity and deleting a volume have different consequences; read
Data and seeding before changing it. backup.consistency: quiesced is refused: snapshots are crash-consistent.
cache
cache:
sizeGb: 50
pruneAfterDays: 14
retireAfterIdleHours: 12
A disposable volume that holds Docker's images and build cache, so repeat boots
skip image pulls and rebuilds. It lives in its own <name>-cache stack, never
in <name>-data, so deleting it can never touch your data.
| Field | Default | Meaning |
|---|---|---|
sizeGb |
required | Volume size, 10 to 2,048 GiB. |
retireAfterIdleHours |
none | 1 to 720. Opt in to retiring the cache when it is unused: chalupa storage sweep deletes the volume after this many idle hours (by this machine's session ledger) and chalupa up recreates it, empty, under the launch confirmation. Omitted, the cache lives until cache-nuke. See Warm-day storage. |
pruneAfterDays |
14 |
1 to 90. Unused images built more than this many days ago are pruned. The age is the image's build date, not the last time it was used, so an image you use every day is still pruned once it was built long enough ago. The build cache is bounded by size instead. |
chalupa cache-up creates the volume (confirmation phrase cache <environment>)
and chalupa cache-nuke deletes it (drop cache <environment>); the next
chalupa up attaches it when it exists, and boots from the boot disk when it
does not. It bills monthly until you delete it, and cache-nuke refuses while
a server is attached. Available on DigitalOcean and Hetzner; a managed GPU host
and a workspace environment with no Compose stack are refused, and so is an
environment whose name ends in -cache. See
Reuse a server, a cache and a build.
To give copies (chalupa up @COPY) a warm start, run chalupa cache-snapshot
while the primary is down (it is refused while a server has the cache attached,
and asks for snapshot cache <environment>); a new copy's cache is created from
that snapshot instead of empty. See copies.
apps
Images you build yourself. The Compose file Chalupa copies must use published
images, so apps declares the ones you build instead and
chalupa apps build and chalupa apps up print the script that builds and
starts them. The scripts are printed, not run: running one is a separate step.
apps:
context: .
compose: ./docker-compose.apps.yml
images:
- name: web
dockerfile: ./Dockerfile
context: .
target: runtime
swap:
build: bun run build
workdir: .
copy:
- from: dist
to: /app/dist
keep: 3
build:
mode: remote
cacheVolume: true
memory: 8g
| Field | Default | Meaning |
|---|---|---|
context |
. |
Directory the build sources are taken from, relative to chalupa.yml. Relative paths only, with no ... |
compose |
none | The app Compose file merged with the image overlay. Required for apps up. It is not the infrastructure Compose file. |
images |
required | One to 32 images. Names are unique. |
images[].name |
required | The Compose service name; the overlay sets its image. |
images[].dockerfile |
required | Path to the Dockerfile. |
images[].context |
. |
Build context for this image. |
images[].target |
none | A Dockerfile stage name. |
images[].swap |
none | Opts this image into chalupa apps swap. |
build.mode |
required | remote builds on the server; local builds linux/amd64 on your machine and apps up loads the image over SSH. |
build.cacheVolume |
true |
Keep the BuildKit cache on the protected data volume so it survives chalupa down. Needs build.mode: remote and persist. This is a different cache from the cache: volume. |
build.memory |
8g on remote builds |
Memory cap of the build container, such as 8g or 512m. |
build.nodeHeapMb |
none | 512 to 16,384. Passed as the NODE_OPTIONS build argument; the Dockerfile must declare ARG NODE_OPTIONS. |
images[].swap fields, all optional except copy:
| Field | Default | Meaning |
|---|---|---|
copy |
required | One to 8 entries of from (relative to workdir) and to (an absolute container path outside the system directories). These are the only paths a swap replaces. |
build |
none | Local command that produces the copy sources. It runs on your machine from a clean export of the ref. Without it the sources must be committed files. |
workdir |
. |
Directory inside apps.context where build runs and from resolves. |
reuse |
none | Up to 8 untracked dependency directories (such as node_modules) copied from your checkout into the export before build runs. Refused when a lockfile or Dockerfile differs from the ref. |
chown |
none | user[:group] for the copied files, when the image does not run as root. |
replace |
true |
Remove each to before copying. Turn it off for images with no shell. |
keep |
3 |
2 to 20 swap tags kept per service, counting the live one and the previous one. |
ready |
the compute.readiness probes that name the service |
Up to 8 probes that decide whether the swapped service came back. They must name host and port, or be cmd probes. |
apps needs compose and services, and is refused for managed GPU hosts.
cleanupRecipes
Named, reviewed commands that clear or restore state between rounds. A recipe is written and reviewed once in the config, so an operator or an agent never has to type a free-form shell command to reset the box.
cleanupRecipes:
clear-queue:
description: Empty the jobs queue
service: redis
command: redis-cli FLUSHDB
timeoutSeconds: 30
load-orders:
description: Restore the orders dataset
restore:
dataset: orders
service: mongo
command: mongorestore --host mongo --archive=/dataset --gzip --drop
gate:
kind: count
command: mongosh --quiet --host mongo --eval 'db.getSiblingDB("shop").orders.countDocuments()'
min: 1000
Each key is the recipe's name (lowercase letters, digits and -, up to 40
characters). A recipe needs exactly one of command or restore.
| Field | Default | Meaning |
|---|---|---|
description |
required | What the recipe does, up to 200 characters. It is shown when you are asked to confirm. |
command |
none | The command, up to 4,000 characters. |
service |
host | Run command inside this Compose service instead of on the host. Not allowed with restore. |
timeoutSeconds |
120 |
10 to 600. |
restore.dataset |
required | The name given to chalupa data load --dataset. |
restore.service |
required | A selected service that is already running. Its image runs the restore in a throwaway container, so the restore tool is the version the stack uses. |
restore.command |
required | Runs inside that container with the dataset file mounted read-only at /dataset. |
restore.gate |
required | kind: count, a command that prints one integer on its last line, and exactly one of equals or min. The restore does not count as done unless the number matches. |
A restore recipe needs persist (the dataset lives on the protected
volume) and its service must be in services. Run a recipe with
RECIPE=<name> CONFIRM_CLEANUP="cleanup <name> <environment>" chalupa cleanup,
or restore a dataset with
CONFIRM_RESET="cleanup <name> <environment>" chalupa reset --dataset <dataset>.
Both are authorized with an environment variable, not --confirm. Recipes are
refused for managed GPU hosts. See
Load a dataset once.
idleShutdownMinutes
idleShutdownMinutes: 45
Omitting the field disables idle observation. A value from 5 to 10,080 minutes is reported by the agent and lets the console derive an absolute deadline.
The server never receives a deletion credential. When the control plane's
external idle reaper is deployed with DIGITALOCEAN_WRITE_TOKEN, the deadline
is an automatic compute-shutdown policy: the worker rechecks the Chalupa tag
and live-CI state before destroying the server, then records the transition.
The protected data stack is never targeted. Without that worker configuration
the deadline is informational and the operator still runs chalupa down.
For a separate demo or test deadline, configure session and register the
local expiry runner; it is independent from idle observation.
seed
seed:
command: ./scripts/restore-orders.sh
onBox: true
A bare string, seed: bun run seed:demo, still works and means a local
command with onBox: false.
| Field | Default | Meaning |
|---|---|---|
command |
required | The command to run. It should be idempotent or clearly document why it is not. |
onBox |
false |
false runs command on your machine while the tunnel is open. true runs it on the server instead, from /opt/chalupa, with the services on 127.0.0.1 at their published ports and CHALUPA_DATASETS_DIR pointing at the datasets that chalupa data load put on the volume. |
Run it with chalupa seed. An on-box seed needs no tunnel and makes no
per-record round trip over SSH, which is what makes loading a large dataset
slow through the tunnel. Do not embed credentials in the command: it is stored
as a local Pulumi output. See Data and seeding.
test
test:
runner: cairn
config: ./cairntrace.config.yml
environment: chalupa
suitesRoot: ./flows
suites:
smoke: {}
checkout:
flows: ./flows/checkout-v2
Suites that chalupa test runs from your machine against the server's tunnelled
ports. Beta. The full field list, the confirmation phrase, the options and the
exit codes are on Run a suite with chalupa test.
| Field | Required | Meaning |
|---|---|---|
runner |
yes | Must be cairn. |
config |
yes | Path to the CairnTrace config file. |
environment |
yes | The CairnTrace environment name. |
suites |
yes | Map of suite name to { flows, sinceRef }. At least one. |
suitesRoot |
one of | The flows directory for a suite with no flows: <suitesRoot>/<suite>. |
secretsEnv |
no | TinyVault environment CairnTrace reads secrets from. |
labels, env |
no | Run labels, and literal non-secret variables for the runner. |
report |
no | Publish results to the console. Requires cloud.url. Default false. |
onFinish |
no | local command and timeoutSeconds (5 to 600, default 60) run when the run finalizes. |
cloud
cloud:
url: https://chalupa.example.com
heartbeatSeconds: 30
This enables agent installation and configuration. The URL must be an HTTPS
origin without a path, query, or fragment. heartbeatSeconds accepts values
from 15 to 300 seconds and defaults to 30. The chalupa up flow obtains the HMAC
key from the secret store; it is never declared here.
services controls whether the launch report tells the console which services
the environment forwards, so its Connect card can list them. named (the
default) sends each service's name, protocol and remote port; the card then
offers chalupa open <service>. hidden still sends protocol and port but
replaces every name with service-1, service-2, ... in remote-port order and
never sends an open path. It only covers the launch report and the Connect
card: the box's heartbeat still names its containers and services, and the
environment page shows them. Local ports are never sent.
cloud:
url: https://chalupa.example.com
services: hidden
credentials
credentials:
tvault:
project: acme-work
Names the TinyVault source for this config's own credentials, so an employer
account and a personal account can use different projects. project defaults
to chalupa. Environment groups are not supported (tvault reads them without
its agent and from the group's own project). Only names live here; values stay
in TinyVault. The
control-plane runtime credentials (database, admin, cron) are not affected.
report
report:
evidence:
publish: local # off | local
when: failed # failed | always
maxBytesPerRun: 16777216
retentionDays: 7
chalupa report (and the results phase of chalupa test) builds a small
evidence pack for each failed run from the run directory the test runner wrote
on your machine. Every field is optional and the values above are the defaults.
publish: localkeeps the files on your machine withfcheap saveand a time-to-live ofretentionDays; only a reference travels to the console, which shows the restore command.offkeeps nothing. Uploading evidence so the run page can open it directly (cloud) is coming next and is refused for now.when: failedpacks only runs that did not pass;alwayspacks every run.includeandexcludeare run-relative globs. The defaults includereport.json,run.json,agent_context.md,outcomes/*,clips/*(andvideos/clips/*) and the screenshots of the failing step (the reserved entry@failing-step). They excludenetwork/**,traces/**,console/**and*.har, which carry cookies and request bodies.excludealways wins. Settingincludereplaces the default list.maxBytesPerRuncaps one pack (default and maximum 16 MiB). When it bites, the failing screenshot and the structured results are kept and the largest low-priority files are dropped.
Symlinks are never followed, and a pack only ever contains regular files from
inside the run directory. Evidence is best effort: if fcheap is missing or
an upload is refused, the run is still reported and the report prints which
evidence was not published.
session
An explicit session describes a disposable demo or test run:
session:
purpose: test
expiresAfterMinutes: 120
maxLifetimeHours: 8
maxExtensions: 2
keepFor: 45m
reuse: auto
logs:
retentionDays: 7
maxTotalBytes: 67108864
| Field | Default | Meaning |
|---|---|---|
purpose |
required for your own cloud account | demo or test. Optional for managed GPU hosts. |
expiresAfterMinutes |
required | The deadline, 5 to 10,080 minutes. |
maxLifetimeHours |
24 (12 for inference) | 1 to 168. Caps the whole lease, counted from the first teardown register: neither session extend nor session renew can move the deadline past it. |
maxExtensions |
2 |
0 to 12. How many times session extend may push the deadline, at least one hour each. expiresAfterMinutes plus maxExtensions hours must fit inside the lifetime cap, or the config is refused at load. |
keepFor |
none | A duration such as 45m, 1h or 1h30m, at least five minutes and no longer than expiresAfterMinutes. Opts in to a warm window: chalupa session release and chalupa test keep the server up that long instead of sinking it. Shown with its maximum cost at the launch confirmation. |
reuse |
auto with keepFor, otherwise never |
auto or never. Whether chalupa up reuses a live, unchanged server instead of launching another. |
logs |
none | Bounded private-log policy. Required before any service may set run.chalupa.logs=include. |
Sessions require cloud.url. The deadline is control-plane metadata, not a
deletion credential on the server: a local teardown runner is what destroys an
expired server. See Expiry and the local teardown runner,
Warm boxes and reuse, and
chalupa status --json, which reports what is left as extensionsLeft.
session.logs
The effective retention is the smaller of retentionDays and your plan's cap:
7 days on Free and Solo, 30 days on Crew. A longer value is clamped, not
rejected, so a lapsed plan shortens retention without breaking the run.
| Field | Default | Hard maximum |
|---|---|---|
retentionDays |
7 | 30 |
maxTotalBytes |
64 MiB | 256 MiB |
maxBytesPerSecond |
256 KiB | 10 MiB |
maxChunkBytes |
1 MiB | 2 MiB |
maxLineBytes |
8 KiB | 16 KiB |
Other session.logs keys select what is collected: services (a map of
service to include or exclude; the pseudo-service box selects the server's
own units), projects (other Compose projects on the same box, up to 16) and
filters (up to 32 entries of service, keep and drop regular expressions
and a per-service maxBytes, applied on the server before anything is
uploaded).
maxChunkBytes cannot exceed maxTotalBytes. Database and cache logs stay
excluded unless a project opts them in. How the bytes are collected, shared
between source sessions and retained is in
Private log artifacts.
observability
Monitor is opt-in. With telemetry on (the default) it requires cloud.url:
observability:
monitor:
version: 2.2.0
sha256:
amd64: "1dc2311b80c1e7d0c34c038819dde1c19a87d9c64b18351a17667c3a542d4131"
arm64: "2d9cd0d209cec0472c4406e4e829966a511d1a6fa6f30bbe333babba21f9c6f8"
sampleSeconds: 5
windowSeconds: 30
These independently verified digests pin the exact Linux archives published
for Monitor v2.2.0. When changing the version, replace both values with the
matching lowercase 64-character digests from that release's checksums.txt
before validation. Chalupa derives the GitHub release URL; configuration cannot
redirect the root installer to another host or path.
Instead of copying a version and two digests, write version: reviewed and
leave sha256 out. It resolves to the Monitor release this Chalupa has
verified, together with that release's digests, so the pin cannot rot; naming
your own digests next to it is refused.
observability:
monitor:
version: reviewed
Fields, relative to observability:
| Field | Default | Meaning |
|---|---|---|
monitor.version |
required | An exact major.minor.patch release of at most 32 characters, 1.14.0 or later, or reviewed. No ranges and no latest. |
monitor.sha256 |
required unless reviewed |
The lowercase digests of the amd64 and arm64 archives from the release's checksums.txt. |
monitor.sampleSeconds |
5 |
1 to 60. |
monitor.windowSeconds |
30 |
30 to 300, and divisible by sampleSeconds. |
monitor.telemetry |
true |
false installs only the verified Monitor binary for local diagnostics, such as CI onFailure.investigate, with no producer or forwarder. Then cloud.url is not required and nothing is sent to the control plane. |
crashCapture.projects |
required with crashCapture |
One to 8 Compose project names whose container deaths are captured. Beta. |
crashCapture.maxPerHour |
20 |
1 to 120. Bounds both the work the box does and what it sends under a crash loop. |
crashCapture.culpritInConsole |
false |
Send the exception type and culprit file:line, which are source-code metadata, to the console. |
On Hetzner the Monitor installer shares the 32 KiB user-data limit with the rest of the startup configuration, so a large Compose stack may not fit it.
The pieces that run on the box, and what each leaves the box with, are described in Performance telemetry:
- How the runtime reaches the box
(delivered over SSH on every
chalupa up, refreshed withchalupa box refresh, Beta). - Crash capture (Beta, for container deaths in the Compose projects you list).
- Reading Monitor from your terminal
(
chalupa monitor VERB, a read-only pass-through).
Mandatory validation
Before chalupa up, preview the config:
export DEMO_CONFIG="$PWD/chalupa.yml"
chalupa preview --config "$DEMO_CONFIG"
The output must match the remote Compose stack you intend to operate. up also
runs a lighter launch gate (preview and CI plan) by itself;
CHALUPA_STRICT_GATE=1 makes it run the full chalupa validate gate instead.
Host-Ollama inference
The experimental inference block replaces the need for a legacy loop when
Chalupa hosts the model; your agent harness runs locally:
inference:
model: qwen3.8:27b
pull: true
maxModelBytes: 25769803776
session:
purpose: test
expiresAfterMinutes: 60
Weights use temporary host storage by default. Add inference.cache.sizeGb
only when you want a separately billed retained volume.
Use chalupa setup --inference --provider digitalocean to generate the surrounding provider and SSH
configuration. The weight limit is required; pull defaults to false.
contextSize is optional: omitted or auto negotiates the session window at
launch, while an integer from 1024 to 1,048,576 fixes the default window for BYOC.
Managed fixed contexts are limited to 2048–262144 tokens; its cache is limited to
20–500 GiB.
A small fixed window can trigger early compaction when an agent’s system
prompt consumes most of it; prefer auto for coding agents. inference requires a session expiry and cannot be combined with
loop. The default local tunnel port is 11435; the remote port is 11434.
cloud is optional for this workflow. See inference setup and limits.
inference.agents
chalupa launch starts a coding agent against the tunnelled model. The
model, endpoint and context always come from inference; agents holds
the per-project knobs on top of the launcher's defaults:
inference:
model: qwen3.8:27b
maxModelBytes: 25769803776
agents:
default: opencode # a bare `chalupa launch` starts this one
opencode:
args: [--agent, plan] # appended before any `-- ARGS` at launch
config: # deep-merged over the generated opencode.json
small_model: chalupa/qwen3.8:27b
omp:
args: [--no-session] # ephemeral sessions for throwaway runs
config: # deep-merged into omp's per-run --config overlay
modelRoles:
smol: ollama/qwen3.8:27b
| Field | Applies to | Effect |
|---|---|---|
default |
launch | Agent used when chalupa launch is given no name. |
<agent>.args |
launch | Extra command-line arguments, before -- ARGS. |
<agent>.env |
launch, exec | Extra environment for that agent's process only; CHALUPA_* names are refused. |
<agent>.config |
launch, exec, connect | Overrides merged over the generated config: OpenCode's opencode.json (also inline through OPENCODE_CONFIG_CONTENT), omp's per-run config.yml overlay. Supported for OpenCode and oh-my-pi. |
Model selectors inside config use each agent's own syntax: OpenCode sees the
GPU as provider chalupa, omp as provider ollama. The primary model never
needs to be repeated; it is filled in from inference.model. Keep separate
configs (for example chalupa.e2e.yml) for scenarios that need a different
model, context or agent defaults, and select one with --config.
For GPU inference reporting, cloud.ingestKeyId optionally identifies the per-environment ingest key. This public ID selects the organization and environment at ingestion; the matching secret remains in TinyVault. Scoped keys support heartbeat, Monitor and local reports. Configurations combining a scoped key with remote CI, loops or private-log collection are refused. Without it, the legacy key writes only to the platform organization. See GPU samples and model reports.
When inference.model is omitted, Chalupa uses qwen3.8:27b (~18 GB of Ollama weights). Allowing room for context and runtime requires gpu-large or gpu-max; preview and up reject this default on gpu-small (20 GiB VRAM).