Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions cloud/deploy/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Keep the build context small — the images sync from pyproject + uv.lock, not these.
.git
.venv
**/__pycache__
**/*.pyc
**/node_modules
apps/web/.next
apps/vscode/dist
website
**/.pytest_cache
**/.mypy_cache
**/.ruff_cache
*.db
.volo
26 changes: 26 additions & 0 deletions cloud/deploy/Dockerfile.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Volo Cloud — API image (the FastAPI control plane).
# Build context is the REPO ROOT (the app is a uv-workspace member):
# docker build -f cloud/deploy/Dockerfile.api -t volo-cloud-api .
#
# Syncs the whole workspace so transitive workspace deps (volo-core, volo-api for the auth seam,
# volo-runner, …) and the `serve` extra (uvicorn) are present. Optimizing to a slim, cloud-only
# image is a follow-up.
FROM python:3.12-slim

# uv provides fast, reproducible installs from the committed uv.lock.
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \
PYTHONUNBUFFERED=1 \
# deny anonymous callers in a hosted deployment (M30 auth gate)
VOLO_REQUIRE_AUTH=true

WORKDIR /app
COPY . .
RUN uv sync --frozen --all-packages --all-extras

EXPOSE 8080
# uvicorn calls the app factory create_cloud_app()
CMD ["uv", "run", "uvicorn", "volo_cloud.app:create_cloud_app", \
"--factory", "--host", "0.0.0.0", "--port", "8080"]
22 changes: 22 additions & 0 deletions cloud/deploy/Dockerfile.worker
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Volo Cloud — sim-minutes worker image (M27).
# Build context is the REPO ROOT:
# docker build -f cloud/deploy/Dockerfile.worker -t volo-cloud-worker .
#
# Runs the queue drain loop: claim a queued SimJob, run the reliability suite, meter wall-clock as
# sim-minutes, charge the workspace quota, store the report. Point it at the same VOLO_DB_URL as the
# API. A job's agent is only executed if it is in VOLO_SIM_AGENT_ALLOWLIST (safe-by-default,
# ADR-0033) — production should additionally sandbox this container per job.
FROM python:3.12-slim

COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \
PYTHONUNBUFFERED=1

WORKDIR /app
COPY . .
RUN uv sync --frozen --all-packages --all-extras

# --poll: seconds between queue polls when idle. Override with --once to drain and exit.
CMD ["uv", "run", "volo-cloud-worker", "--poll", "2"]
50 changes: 50 additions & 0 deletions cloud/deploy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Deploying Volo Cloud (hosted sim-minutes)

The commercial control plane (M26/M30) + the sim-minutes worker (M27) run locally on SQLite with
zero infra. This directory packages them for a hosted deployment: two container images (API +
worker) and a Fly.io manifest. **Nothing here changes application behavior** — it's packaging.

## Images

Both build from the **repo root** (the cloud app is a uv-workspace member):

```bash
docker build -f cloud/deploy/Dockerfile.api -t volo-cloud-api .
docker build -f cloud/deploy/Dockerfile.worker -t volo-cloud-worker .
```

- **API** — `uvicorn volo_cloud.app:create_cloud_app --factory` on port 8080.
- **Worker** — `volo-cloud-worker --poll 2`, the queue drain loop.

## Configuration (env / secrets)

| Variable | Purpose |
|---|---|
| `VOLO_DB_URL` | **Postgres** DSN shared by API + worker (e.g. `postgresql://…`). Defaults to local SQLite if unset. |
| `VOLO_REQUIRE_AUTH` | `true` in a hosted deployment — denies anonymous callers (set in the image/manifest). |
| `VOLO_JWT_JWKS` *or* `VOLO_JWT_SECRET` | SSO: an IdP's JWKS (RS256) or a shared secret (HS256). `VOLO_JWT_ISS` / `VOLO_JWT_AUD` optional. |
| `VOLO_SIM_AGENT_ALLOWLIST` | Comma-separated agents the **worker** may execute (safe-by-default, ADR-0033). |

> **Security:** the worker executes a job's agent code only if it's allowlisted. In production also
> sandbox the worker container per job — treat it as an untrusted-code boundary.

## Fly.io

`fly.toml` runs one image with two processes (`app`, `worker`). A first deploy:

```bash
fly launch --copy-config --no-deploy # uses cloud/deploy/fly.toml
fly postgres create && fly postgres attach … # sets VOLO_DB_URL (DATABASE_URL) as a secret
fly secrets set VOLO_JWT_JWKS='{"keys":[…]}' VOLO_SIM_AGENT_ALLOWLIST='my_pkg.agent:run'
fly deploy
```

Scale the worker independently of the API:

```bash
fly scale count app=1 worker=2
```

The same images run on any container host (Cloud Run, ECS, a plain VM) — set the same env and run
the two commands. A slimmer cloud-only image (syncing just `volo-cloud` instead of the whole
workspace) is a reasonable follow-up optimization.
29 changes: 29 additions & 0 deletions cloud/deploy/fly.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Fly.io manifest for the Volo Cloud control plane (API + sim-minutes worker).
# One image, two processes. Set secrets (DB, auth, allowlist) with `fly secrets set` — see README.
app = "volo-cloud"
primary_region = "iad"

[build]
dockerfile = "cloud/deploy/Dockerfile.api"

[env]
# Deny anonymous callers; a real IdP is wired via VOLO_JWT_JWKS / VOLO_JWT_SECRET (secrets).
VOLO_REQUIRE_AUTH = "true"

# The API serves HTTP; the worker drains the sim-minutes queue. Both run from the same image;
# the worker process overrides the command.
[processes]
app = "uv run uvicorn volo_cloud.app:create_cloud_app --factory --host 0.0.0.0 --port 8080"
worker = "uv run volo-cloud-worker --poll 2"

[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = "stop"
auto_start_machines = true
min_machines_running = 0
processes = ["app"]

[[vm]]
size = "shared-cpu-1x"
memory = "512mb"
Loading