Operating model
chalupa.yml reference
Configure a project declaratively without duplicating what Docker Compose already knows.
Reviewed 2026-07-26
This file belongs to the target project and sits next to its Compose file.
Tasks receive its path through 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
| 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, droplet, firewall, volume, and control-plane environment all follow). |
compose |
yes | Path to the project's docker-compose.yml. |
services |
yes | Services sent to the remote environment. |
overrides |
no | Per-service adjustments applied after inference. |
compute |
no | Region, size, image, prebaked flag, and health check. Accepts the deprecated droplet spelling with a warning. |
ssh |
yes | Authorized keys and firewall CIDRs. |
persist |
no | Block storage with an independent lifecycle. |
idleShutdownMinutes |
no | Idle window reported to Chalupa. |
seed |
no | Local command run while the tunnel is open. |
cloud |
no | Agent registration with a control plane. |
session |
no | Ephemeral purpose, teardown deadline, and bounded private-log policy. |
observability |
no | Pinned, privacy-safe Monitor telemetry. |
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 totor1; accepted values aretor1,nyc2,ams3, andatl1, subject to capacity.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. Until safe remote builds are added, every selected service,
including an initializer, must use a published image rather than build:.
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.
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.
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 droplet, 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. cloud-init uses
the check as a readiness diagnostic, but chalupa up does not wait synchronously
for the probe to complete.
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
task snapshot CONFIG=... CONFIRM_SNAPSHOT_BUILD=<name>.
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 droplet. 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.
persist
persist:
sizeGb: 20
sizeGb creates or references a volume in the data stack. Increasing capacity
and deleting a volume have different consequences; read
Data and seeding before changing it.
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 droplet 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 droplet, 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: bun run seed:demo
The command runs on the local machine while the tunnel is open. It should be idempotent or clearly document why it is not. Do not embed credentials in the command; it is stored as a local Pulumi output.
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.
session
An explicit session describes a disposable demo or test run:
session:
purpose: demo
expiresAfterMinutes: 120
logs:
retentionDays: 7
maxTotalBytes: 67108864
maxBytesPerSecond: 262144
maxChunkBytes: 1048576
maxLineBytes: 8192
purpose is demo or test. expiresAfterMinutes accepts 5 through 10,080.
The optional logs object is required before any selected Compose service may
set run.chalupa.logs=include. Its defaults are seven retention days, a
64 MiB deployment budget, 256 KiB per second, 1 MiB per object, and 8 KiB per
line. The hard maxima are 30 days, a 256 MiB deployment budget, 10 MiB per
second, 2 MiB per object, and 16 KiB per line. maxChunkBytes cannot exceed
maxTotalBytes. At most eight source sessions and 256 chunks share that one
deployment budget; restarting a producer never multiplies storage allowance.
Sessions require cloud.url. The deadline is durable control-plane metadata,
not an in-droplet deletion credential. Because Pulumi uses a local backend,
neither Vercel nor the droplet can safely destroy the compute stack when the
deadline passes. An operator can explicitly register the exact local deployment
with chalupa teardown register --config /absolute/path/to/chalupa.yml, review
due records with task teardown:run, and opt in to a scheduled local
task teardown:run-armed TEARDOWN_ENABLED=1. The runner rechecks the config
digest and provider ID, then delegates only to task down; it never targets a
data stack.
Log bytes use the service allowlist from Compose, a local journald reader, and direct private-object uploads authorized by the control plane. file.cheap owns the objects. Chalupa stores searchable metadata and bounded excerpts in Neon; it does not proxy full log bodies through Vercel Functions. Database and cache logs remain excluded unless a project explicitly opts them in.
observability
Monitor is opt-in and requires cloud.url:
observability:
monitor:
version: 1.15.0
sha256:
amd64: "f8c729041cdd10681f3adc7ec40f9f4e9c1f0e25215b078a028e401a0e9048ef"
arm64: "1712d96f9803e297440b3baffbe9e6d09fb4bbe4afaf88be19aef3aa2bf77f4e"
sampleSeconds: 5
windowSeconds: 30
These independently verified digests pin the exact Linux archives published
for Monitor v1.15.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.
version must be an exact major.minor.patch release of at most 32 characters,
without a range or latest, and must be 1.14.0 or later (the first release
with monitor telemetry).
sampleSeconds accepts 1 through 60 and defaults to 5.
windowSeconds accepts 30 through 300, defaults to 30, and must be divisible
by sampleSeconds. One window can never exceed Monitor's 3,600-sample
contract.
Monitor runs without the ingest key. A separate hardened adapter validates, enriches, signs, and retries its bounded output. See Performance telemetry for the data and privacy contract.
Monitor v1.15.0 additionally recognizes the CHALUPA_CI_RUN_ID,
CHALUPA_CI_STEP_ID, CHALUPA_CI_SUITE, CHALUPA_CI_ATTEMPT, and
CHALUPA_CI_ENVIRONMENT values already injected into CI checks. Explicit
monitor investigate/issue evidence can therefore be correlated with a
Chalupa run without adding those identifiers to the automatic telemetry lane.
Mandatory validation
Before chalupa up, run the repository's own gate from the checkout:
export DEMO_CONFIG="$PWD/chalupa.yml"
cd "$CHALUPA_HOME"
task validate CONFIG="$DEMO_CONFIG"
The output must match the remote Compose stack you intend to operate.
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
cache:
sizeGb: 60 # retained weights volume; omit to pull on every up
session:
purpose: test
expiresAfterMinutes: 60
Use chalupa setup --inference 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.
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).