Standalone deployment
Run AcasoOS inside a client's own cloud as an isolated single-tenant instance. Same repository, same container image, same code path as the shared deployment — one tenant per instance, with RLS still forced as defense in depth. Target: a client SRE without acaso context stands this up in under 4 hours. The in-repo ops counterpart is docs/runbooks/deploy-to-client-infra.md.
Note: Standalone is not a fork. The instance runs a tagged release image built from the
acaso-osmonorepo — including any bespoke code for this client, which lives in namespaced directories of that same repo. The client never patches a deployment by hand; changes go through the repo and arrive as the next release. That is what keeps Upgrades a tag swap instead of a migration project.
Requirements
| Component | Requirement |
|---|---|
| Postgres | 16, with pgcrypto, pgvector, and Apache AGE available. The repo's Postgres image (infra/docker/postgres/) bundles them; managed Postgres needs the extensions enabled via parameter group + infra/init-extensions.sql. |
| Redis | 7.x with persistence (streams + cache). |
| Compute | Container hosting for three services: DataHub API, AgentHub API, pipeline worker. |
| Ingress | Load balancer with TLS in front of the APIs. |
| LLM access | At least one provider key (OpenAI / Anthropic / OpenRouter / self-hosted via LITELLM_API_BASE). |
| Tooling | Terraform 1.7+ or Helm 3.14+, Docker. |
1. Provision infrastructure
Pick the matching IaC under infra/:
infra/terraform/standalone-aws/RDS Postgres, ElastiCache Redis, ECS Fargate services, ALB, Secrets Manager.infra/terraform/standalone-gcp/Cloud SQL, Memorystore, Cloud Run.infra/terraform/standalone-azure/Azure DB for Postgres, Azure Cache, Container Apps.infra/helm/acaso-os/Kubernetes chart against externally-provisioned Postgres + Redis.
cd infra/terraform/standalone-aws
terraform init
terraform apply -var "tenant_name=BigCorp" -var "region=us-east-1" -var "vpc_cidr=10.0.0.0/16"2. Build and push the release image
Always deploy a tagged release, never a branch build:
git checkout v0.1.0
docker build -t <REGISTRY>/acaso-os:v0.1.0 .
docker push <REGISTRY>/acaso-os:v0.1.0Clients with access to acaso's private registry pull the published image for the same tag instead.
3. Migrate, then bootstrap
Run migrations with the privileged database role, then create the tenant and the first admin key:
docker run --rm \
-e DATABASE_URL="postgresql+asyncpg://acaso:...@<DB_HOST>:5432/acaso_os" \
<REGISTRY>/acaso-os:v0.1.0 alembic upgrade head
docker run --rm \
-e DATABASE_URL="..." \
-e ACASO_BOOTSTRAP_TENANT_SLUG=bigcorp \
-e ACASO_BOOTSTRAP_TENANT_NAME="BigCorp" \
<REGISTRY>/acaso-os:v0.1.0 python -m datahub.interface.cli.bootstrapThe bootstrap CLI prints the initial admin API key once — capture it into the client's secrets manager immediately; it cannot be retrieved later. It also seeds the default roles and the management-console Application.
4. Wire the service environment
Minimum production set per container:
DATABASE_URLthe non-privilegedacaso_approle. Runtime never uses the superuser.ADMIN_DATABASE_URLtheacasoowner role; only the migration step uses it.REDIS_URLthe Redis endpoint.ENVIRONMENTstandalone-<client_slug>.RELEASE_VERSIONbare semver matching the image tag without thev(imagev0.1.0→0.1.0).- One or more LLM provider keys (
OPENAI_API_KEY,ANTHROPIC_API_KEY,OPENROUTER_API_KEY). ACASO_AGENT_MODULESset explicitly: exactly the client's agent modules (agenthub.custom.<client_slug>.<module>, comma-separated). Left unset, the dev default loads the example agents — never what a client deployment wants. An empty string loads none.ACASO_MCP_TOOL_MODULESonly if this client exposes internal (non-agent) tools over MCP (modules whose import callsregister_mcp_tool, comma-separated). Agents that setprotocols = (..., "mcp")are exposed without it; empty (the default) publishes no internal tools.ACASO_PUBLIC_BASE_URLoptional; the public base URL (e.g.https://os.aca.so) used to build the A2A Agent Cardurlwhen AgentHub sits behind a proxy / load balancer. Falls back to the request host if unset.ACASO_CONSOLE_ORIGINSthe console origin(s) allowed to connect, e.g.https://os.aca.so; leave empty to disable console access entirely.- Optional, omittable observability:
SENTRY_DSN,LANGFUSE_HOST+LANGFUSE_PUBLIC_KEY+LANGFUSE_SECRET_KEY. The platform runs fully without them — nothing phones home from an air-gapped deployment.
Full catalog with defaults: Reference: Environment variables.
5. Smoke test
DEPLOYMENT_URL="https://acaso.bigcorp.internal" ADMIN_KEY="<from-bootstrap>" ./scripts/smoke_test.shThe script exits 0 only if /health and /ready return 200, /v1/ingest accepts a sample event, the worker drains it within 30s, and the sample agent runs end-to-end.
6. Connect the console and finish provisioning
The management console is a static bundle; it talks to whatever instance you point it at. Add the console origin to ACASO_CONSOLE_ORIGINS, connect with the admin key, and complete the client setup — users, roles, keys, cultivator bindings — exactly as in Provision a new client from step 2 onward (the bootstrap already created the tenant).
Hardening checklist
- Application containers hold only
DATABASE_URL(app role);ADMIN_DATABASE_URLlives in the migration job, not the services. - Admin key and LLM keys in the client's secrets manager; nothing in plain env files.
ACASO_CONSOLE_ORIGINSis an explicit list — wildcard*is rejected at startup by design.- Database backups / snapshots scheduled before the first real data lands (they are also the upgrade safety net).
- TLS on the load balancer; the services never face the internet directly.
White-labeling the console
A standalone instance usually wants the console to read as the client's own brand — name, logo, and accent colour — rather than acaso's. This is build-time configuration via NEXT_PUBLIC_BRAND_* env vars, baked into the static export, so set them before building the console image and rebuild whenever they change. Leave everything unset for the stock acaso look; see packages/web/.env.example for the annotated list.
Name. Setting the brand word is usually enough — the product name and wordmark derive from it, and it flows to the browser title, the sidebar, the Connect modal, and every UI string:
NEXT_PUBLIC_BRAND_NAME=Natura # → product "Natura OS"
# NEXT_PUBLIC_BRAND_PRODUCT_NAME=Natura Intelligence # only if not "<name> OS"
# NEXT_PUBLIC_BRAND_WORDMARK_SUFFIX=OS # the muted "OS"; "" hides itLogo. Drop the client's mark into packages/web/public/brand/ and point at it. The mark sits in a rounded tile; tune or remove the tile to suit the asset:
NEXT_PUBLIC_BRAND_LOGO_SRC=/brand/natura-mark.svg
# NEXT_PUBLIC_BRAND_LOGO_TILE=none # default: acaso's dark gradient
# NEXT_PUBLIC_BRAND_LOGO_SIZE=contain # default: "64% 64%"
# NEXT_PUBLIC_BRAND_FAVICON_SRC=/brand/natura-favicon.svgAccent colour. Provide one base hex — the brand "500". The full light→dark ramp is generated from it, and every accent, border, focus ring, and ambient glow re-themes from that ramp:
NEXT_PUBLIC_BRAND_COLOR=#00857AAfter building, the exported out/index.html <title> should read <Brand> OS · Management Console, and the sidebar, Connect modal, and UI copy should reference the client brand throughout rather than acaso.