deco Studio

Kubernetes

Deploy deco Studio and agent sandboxes on Kubernetes using Helm

This guide deploys deco Studio and its hosted agent sandboxes on Kubernetes.

Chart sources:

Easiest path: for a one-command install with bundled dependencies (local or a POC cluster), use the umbrella chart or selfhost/scripts/local-k8s.sh (see Quickstart). There’s also a guide for coding agents ( SKILL.md ) that interviews you and writes a reusable install directory — an umbrella chart with pinned OCI dependencies plus your values. This page is the chart reference for installing directly and for production — the full dependency guide (managed vs in-cluster Postgres/S3/ClickHouse, secrets with/without ESO, ingress, sandbox) lives in selfhost/production .

For the runtime architecture (web/API/worker split, Postgres, NATS, and AgentSandbox), see Architecture.

Deployment model

A production deployment uses three Helm releases:

Chart Scope Purpose
chart-deco-studio Once per Studio environment Runs an nginx front door with two API containers per pod, queue workers, NATS, persistence, and optional observability components
sandbox-operator Once per Kubernetes cluster Installs the upstream Kubernetes SIGs agent-sandbox controller and CRDs
sandbox-env Once per Studio environment Installs the environment’s SandboxTemplate , runner RBAC, sentinel Secret, and optional warm pool, preview Gateway, and idle housekeeper

The operator is shared cluster infrastructure. Each sandbox-env release is isolated by envName , so production, staging, and other Studio releases can share the operator and the agent-sandbox-system namespace without resource-name collisions.

When a hosted agent needs a sandbox, Studio creates a SandboxClaim . The operator turns that claim into an ephemeral sandbox pod from the environment’s SandboxTemplate . Studio reaches the daemon through Kubernetes port-forward for control traffic. If preview routing is enabled, Studio also creates one HTTPRoute per claim so the wildcard Gateway routes directly to the sandbox Service on port 9000.

Prerequisites

  • Kubernetes 1.32+ and Helm 3
  • kubectl configured for the cluster
  • A PostgreSQL database; the current API/worker topology requires PostgreSQL
  • A StorageClass for Studio and the chart-managed NATS JetStream PVCs
  • An S3-compatible object store and credentials for the organization filesystem (org-fs)
  • Support for spec.hostUsers: true and privileged sidecars in agent-sandbox-system ; the mandatory org-fs sidecar uses FUSE and bidirectional mount propagation
  • For public preview URLs only: Gateway API CRDs, a Gateway controller, wildcard DNS, and either cert-manager with a DNS-01 ClusterIssuer or load-balancer TLS termination

For production, schedule sandbox pods on a dedicated, tainted node pool. Sandbox pods run user-authored code and should not share nodes with Studio, Postgres, NATS, or observability workloads.

Install Studio with agent sandboxes

The commands below pin chart versions explicitly. Pin them in your own deployment too, instead of tracking whatever is newest.

 export STUDIO_CHART_VERSION=0.13.1
export SANDBOX_OPERATOR_CHART_VERSION=0.1.4
export SANDBOX_ENV_CHART_VERSION=0.15.3 

The charts are published as public OCI artifacts, so no registry credentials are needed. To pick a newer version, list what is published:

 helm show chart oci://ghcr.io/decocms/chart-deco-studio | grep '^version:'
helm show chart oci://ghcr.io/decocms/studio/charts/sandbox-operator | grep '^version:'
helm show chart oci://ghcr.io/decocms/studio/charts/sandbox-env | grep '^version:' 

Every published version is also listed on the package pages: chart-deco-studio, sandbox-operator, sandbox-env.

1. Install the cluster-wide sandbox operator

The operator chart has no configurable values and must be installed in agent-sandbox-system because the vendored controller manifest targets that namespace.

 helm install sandbox-operator \
  oci://ghcr.io/decocms/studio/charts/sandbox-operator \
  --version "$SANDBOX_OPERATOR_CHART_VERSION" \
  --namespace agent-sandbox-system \
  --create-namespace 

Verify the controller and CRDs:

 kubectl rollout status deployment/agent-sandbox-controller \
  -n agent-sandbox-system --timeout=300s
kubectl get crd sandboxclaims.extensions.agents.x-k8s.io \
  sandboxtemplates.extensions.agents.x-k8s.io \
  sandboxwarmpools.extensions.agents.x-k8s.io 

2. Configure and install Studio

Hosted sandboxes require S3-compatible object storage. Studio considers it configured only when all four required variables are present:

Variable Required Description
S3_ENDPOINT Yes Full S3-compatible API endpoint, such as https://s3.us-east-1.amazonaws.com
S3_BUCKET Yes Bucket that backs org-fs
S3_ACCESS_KEY_ID Yes Access key used by Studio
S3_SECRET_ACCESS_KEY Yes Secret key used by Studio
S3_REGION Provider-specific Defaults to auto ; set the actual region for AWS S3
S3_FORCE_PATH_STYLE Provider-specific Set false for AWS S3 virtual-hosted addressing; commonly true for MinIO and other path-style providers

The credentials need s3:ListBucket on the bucket and s3:GetObject , s3:PutObject , and s3:DeleteObject on its objects. These variables configure the org filesystem and are separate from the optional s3Sync.* backup sidecar and MONITORING_S3_* settings.

Store the S3 variables alongside DATABASE_URL , BETTER_AUTH_SECRET , and ENCRYPTION_KEY in the Kubernetes Secret consumed by Studio. Do not put credentials in configMap.meshConfig or in the sandbox-env release; sandbox pods access org-fs through Studio’s authenticated API and do not receive the S3 keys directly.

Create studio-values.yaml . envName , the template suffix, Studio namespace, release name, and service-account name must agree with the sandbox-env release installed in the next step.

 database:
  engine: postgresql
  url: "" # supplied as DATABASE_URL by the existing Secret

serviceAccount:
  create: true
  name: deco-studio
  automount: true

configMap:
  meshConfig:
    BETTER_AUTH_URL: "https://studio.example.com"
    BASE_URL: "https://studio.example.com"
    STUDIO_AGENT_SANDBOX_ENABLED: "true"
    STUDIO_ENV: "production"
    STUDIO_SANDBOX_TEMPLATE_NAME: "studio-sandbox-production"

secret:
  secretName: deco-studio-secrets 

configMap.meshConfig is a deprecated chart-key name retained for upgrade compatibility. Its values configure the Studio API.

The existing deco-studio-secrets Secret must contain DATABASE_URL , BETTER_AUTH_SECRET , ENCRYPTION_KEY , S3_ENDPOINT , S3_BUCKET , S3_ACCESS_KEY_ID , and S3_SECRET_ACCESS_KEY , plus provider-specific S3_REGION and S3_FORCE_PATH_STYLE . Prefer externalSecret or another secret manager in production; do not commit these values to Git.

Provision deco-studio-secrets in the deco-studio namespace before running the Helm install below. Alternatively, configure the chart’s externalSecret values to create it from AWS Secrets Manager.

Install the application chart:

 helm install deco-studio \
  oci://ghcr.io/decocms/chart-deco-studio \
  --version "$STUDIO_CHART_VERSION" \
  --namespace deco-studio \
  --create-namespace \
  --values studio-values.yaml 

A first install against an empty database migrates before it serves. Studio takes a Postgres advisory lock around the DBOS system-schema migration, so whichever process starts first migrates and the others wait — no replica-count juggling is needed, with or without Argo CD. The chart also ships a single-writer migration Job ( migrateJob.enabled , on by default). It does not provide the guarantee — the lock does — but it gives the migration one named place that either succeeded or failed, instead of whichever pod happened to win. Under Argo CD the sync wave holds the Deployments until the Job finishes; under plain helm install the Job starts alongside the first pods, which is harmless once the pods carry the lock.

The main Deployment contains nginx and two API containers. The separate worker Deployment is required and enabled by default. The chart also enables its NATS dependency by default and requires PostgreSQL so API and worker pods share the DBOS run queue.

3. Install the per-environment sandbox resources

 helm install sandbox-env-production \
  oci://ghcr.io/decocms/studio/charts/sandbox-env \
  --version "$SANDBOX_ENV_CHART_VERSION" \
  --namespace agent-sandbox-system \
  --set envName=production \
  --set mesh.namespace=deco-studio \
  --set mesh.serviceAccountName=deco-studio 

This creates SandboxTemplate/studio-sandbox-production and grants only the named Studio service account permission to manage claims, read sandboxes, port-forward to sandbox pods, patch their Services, and manage per-claim HTTPRoute objects in agent-sandbox-system .

The sandbox chart’s mesh.* values key is a deprecated name retained for upgrade compatibility; it points to the Studio release. mesh.serviceName , mesh.servicePort , and mesh.podSelectorLabels are deprecated and unused by the current per-claim preview routing design. New installations do not need to set those three fields.

4. Verify the complete deployment

 helm status deco-studio -n deco-studio
helm status sandbox-operator -n agent-sandbox-system
helm status sandbox-env-production -n agent-sandbox-system

kubectl get deployments,pods -n deco-studio
kubectl get deployment -n agent-sandbox-system
kubectl get sandboxtemplate,sandboxwarmpool -n agent-sandbox-system
kubectl auth can-i create sandboxclaims.extensions.agents.x-k8s.io \
  --as=system:serviceaccount:deco-studio:deco-studio \
  -n agent-sandbox-system 

To access Studio before configuring ingress:

 kubectl port-forward svc/deco-studio 8080:80 -n deco-studio 

Studio is then available at http://localhost:8080 .

5. Expose Studio

The chart’s Service is ClusterIP and the Ingress is optional and off by default, because the controller, class, TLS issuer, and annotations differ per cluster. Pick the option that matches what you already run.

Ingress (chart-managed). Set ingress.enabled and the chart renders an Ingress backed by this release’s Service — you don’t hardcode the generated Service name, and the object joins the Helm release, so helm rollback and helm uninstall cover it:

 ingress:
  enabled: true
  className: nginx
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt
  hosts:
    - host: studio.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - hosts:
        - studio.example.com
      secretName: studio-tls 

Studio serves the whole app from the root, so one / Prefix rule per host is the normal shape. With ingress.enabled=true , the chart fails to render if hosts is empty or a host has no paths — an Ingress without rules is accepted by controllers and silently routes nothing.

LoadBalancer. No controller in the cluster: set service.type: LoadBalancer and point DNS and TLS at the load balancer.

Gateway API. Leave ingress.enabled: false and create a Gateway and HTTPRoute targeting the same Service. This is separate from the sandbox preview Gateway, which the sandbox-env chart configures.

Whichever you choose, set BASE_URL and BETTER_AUTH_URL to the same public https:// URL. Auth callbacks are built from those values, not from the incoming request host, so a mismatch produces sign-in redirects that fail.

6. Connect an AI provider

A self-hosted deployment ships with no model access. Agents and Decopilot stay unusable until an organization owner connects a provider under Settings → AI Providers with a key you own — Anthropic, Google Gemini, OpenRouter, or any OpenAI-compatible endpoint, including a self-hosted vLLM server. The hosted deco AI Gateway and its starter credits are a feature of the managed offering; do not plan a self-hosted rollout around them. See AI Providers.

Egress note: unless you run the models in your own network, Studio pods need outbound HTTPS to the provider’s API.

7. Configure authentication

Studio uses Better Auth. For Kubernetes, configure it with environment variables in the same Secret or ConfigMap the chart already injects — there is nothing extra to mount:

Method Variables
Social login AUTH_GOOGLE_CLIENT_ID / AUTH_GOOGLE_CLIENT_SECRET , AUTH_GITHUB_CLIENT_ID / AUTH_GITHUB_CLIENT_SECRET
Deployment-wide SSO AUTH_SSO_DOMAIN plus the Microsoft Entra ID or Google Workspace variables
Magic link, email OTP, email/password AUTH_MAGIC_LINK_ENABLED , AUTH_EMAIL_OTP_ENABLED , AUTH_EMAIL_PASSWORD_ENABLED , plus a mail provider ( AUTH_RESEND_API_KEY or AUTH_SENDGRID_API_KEY )

See Authentication for the full reference.

There’s no auth-config.json file anywhere — auth is configured entirely through the AUTH_* environment variables above. Keep provider secrets in the Secret, not in a ConfigMap.

Sandbox configuration

Isolation and egress

The sandbox container runs as a non-root user with privilege escalation disabled, all Linux capabilities dropped, a RuntimeDefault seccomp profile, and a read-only root filesystem. Writable emptyDir volumes are mounted where the runtime needs them.

The org-fs sidecar is mandatory: it uses privileged FUSE mounts to expose organization skills, uploads, outputs, and home directories under /app/org . Only this sidecar is privileged. disableFsSidecar: true is a debugging escape hatch, not a supported production mode.

By default, a short-lived NET_ADMIN init container installs iptables rules and exits before user code starts. It blocks private, link-local, and shared-address IPv4/IPv6 ranges—which cover the pod and Service CIDRs on most clusters—and allows outbound DNS plus HTTPS. Override the CIDR lists when your cluster uses different ranges. The chart does not currently create a Kubernetes NetworkPolicy . If your CNI or external firewall owns sandbox egress, set netinit.enabled: false and supply equivalent controls.

Recommended production scheduling:

 nodeSelector:
  workload: sandbox

tolerations:
  - key: workload
    operator: Equal
    value: sandbox
    effect: NoSchedule

topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: ScheduleAnyway
    labelSelector:
      matchLabels:
        app.kubernetes.io/name: studio-sandbox-production 

Warm pool

Warm pools are disabled by default because every idle warm pod consumes the full sandbox resource request. Enabling one also requires Studio and the sandbox template to share a sentinel token. Studio uses it only for the first daemon configuration request and immediately rotates the daemon to a per-claim token.

Generate the token once, store it in the Studio Secret as STUDIO_SANDBOX_SENTINEL_TOKEN , and pass the same value to sandbox-env :

 export SANDBOX_SENTINEL_TOKEN="$(openssl rand -base64 48)"

helm upgrade sandbox-env-production \
  oci://ghcr.io/decocms/studio/charts/sandbox-env \
  --version "$SANDBOX_ENV_CHART_VERSION" \
  --namespace agent-sandbox-system \
  --reuse-values \
  --set-string sentinel.token="$SANDBOX_SENTINEL_TOKEN" \
  --set warmPool.enabled=true \
  --set warmPool.size=2 

If sentinel.token is omitted, the chart generates and preserves a Secret for sandbox pods, but Studio still needs the same value before it can consume warm-pool pods. For GitOps, generate the value in your secret manager and deliver it to both namespaces. Do not place it in configMap.meshConfig .

The optional warm-pool HPA requires warmPool.enabled: true , warmPool.autoscaling.enabled: true , and at least one explicit autoscaling/v2 metric. The chart intentionally provides no default metric.

Idle housekeeper

Studio refreshes claim activity, while the optional housekeeper CronJob cleans up idle or unrecoverable claims and orphaned routes/pods. Its defaults run every five minutes and reap claims idle for 15 minutes.

 housekeeper:
  enabled: true
  schedule: "*/5 * * * *"
  idleTtlSeconds: 900 

STUDIO_ENV is required when the housekeeper is enabled because its default selectors are environment-scoped. Without the matching label, it intentionally selects no claims.

Preview Gateway

Treat public preview URLs as a later phase. They need Gateway API CRDs, a Gateway controller, wildcard DNS, and a wildcard certificate via DNS-01 — dependencies that usually belong to another team and another schedule. Studio, agents, and code execution all work without them; only shareable preview links are missing.

Without a preview Gateway, Studio can use its legacy in-process preview proxy when a preview URL pattern and the required external routing are provided. The current production design uses Gateway API and routes each preview directly to its sandbox daemon:

 Browser -> load balancer -> Gateway -> per-claim HTTPRoute -> sandbox Service:9000 

Configure sandbox-env :

 previewGateway:
  enabled: true
  gatewayClassName: istio
  namespace: istio-system
  domain: preview.example.com
  tlsTermination: gateway
  clusterIssuer: letsencrypt-dns 

Then add the matching values to Studio:

 configMap:
  meshConfig:
    STUDIO_SANDBOX_PREVIEW_URL_PATTERN: "https://{handle}.preview.example.com"
    STUDIO_SANDBOX_PREVIEW_GATEWAY_NAME: "agent-sandbox-preview-production"
    STUDIO_SANDBOX_PREVIEW_GATEWAY_NAMESPACE: "istio-system" 

The Gateway name and namespace must either both be set or both be absent. Create wildcard DNS for *.preview.example.com pointing to the Gateway load balancer. With tlsTermination: gateway , the ClusterIssuer must support DNS-01 because wildcard certificates cannot use HTTP-01. Set tlsTermination: loadBalancer instead when the external load balancer owns the certificate.

The sandbox handle in the hostname is the preview route’s only built-in authorization. Treat preview URLs as secrets: handles can appear in CDN, load-balancer, and proxy logs. Add Gateway-level authorization (for example, Istio AuthorizationPolicy or Envoy external auth) when hostname secrecy is not sufficient.

Each environment that enables previews needs a distinct previewGateway.domain ; two Gateways cannot safely bind the same wildcard hostname.

Main Studio values

These defaults come from deploy/helm/studio/values.yaml :

Parameter Description Default
replicaCount API pod replicas 1
image.repository / image.tag Studio API/worker image (tags) ghcr.io/decocms/studio/studio / unset
nginx.image.repository / nginx.image.tag Front-door image (tags) ghcr.io/decocms/studio/studio-nginx / unset
service.type / service.port Service exposure ClusterIP / 80
ingress.enabled / ingress.className / ingress.hosts Optional chart-managed Ingress false / "" / []
database.engine Database engine postgresql
worker.enabled / worker.replicaCount Queue worker Deployment true / 2
worker.autoscaling.enabled Worker HPA true
nats.enabled NATS subchart with JetStream true
persistence.enabled / persistence.size Studio PVC true / 10Gi
autoscaling.enabled API HPA false
metrics.scrape Prometheus pod annotations true

image.tag and nginx.image.tag are unset by default, so both fall back to the chart’s appVersion , which is latest . Set both explicitly in production to a released version (for example 4.152.1 ) — otherwise a pod restart can silently pull a different build than the one you validated. A digest ( sha256:… ) is also accepted. The sandbox chart already pins its own image; do not override it with latest .

Main sandbox values

Parameter Description Default
envName DNS-label suffix for environment resources Required
image.repository / image.tag Sandbox daemon image (tags) ghcr.io/decocms/studio/studio-sandbox-go / pinned by the chart
resources.requests Per-sandbox request 500m CPU / 2Gi memory
resources.limits Per-sandbox limit 2 CPU / 4Gi memory / 10Gi ephemeral storage
mediumResources Overrides for the second, roomier -medium SandboxTemplate (headless agent-loop claims) 3Gi request / 6Gi limit
terminationGracePeriodSeconds Time for final git sync and unmount 90
netinit.enabled Install default iptables egress policy true
readOnlyRootFilesystem Read-only sandbox root filesystem true
depsCache.enabled / depsCache.golden Node-local dependency caches false / false
warmPool.enabled / warmPool.size Pre-warmed sandboxes false / 0
previewGateway.enabled Wildcard preview Gateway false
housekeeper.enabled Idle-claim cleanup CronJob false

Upgrades and removal

Render and lint all three charts before applying changes:

 helm lint deploy/helm/studio \
  --set database.url=postgresql://user:pass@postgres.example.com:5432/studio
helm lint deploy/helm/sandbox-operator \
  --namespace agent-sandbox-system
helm lint deploy/helm/sandbox-env \
  --namespace agent-sandbox-system \
  --set envName=production

helm template deco-studio deploy/helm/studio --values studio-values.yaml
helm template sandbox-operator deploy/helm/sandbox-operator \
  --namespace agent-sandbox-system
helm template sandbox-env-production deploy/helm/sandbox-env \
  --namespace agent-sandbox-system \
  --set envName=production 

Files under a Helm chart’s crds/ directory are installed once but are not upgraded by helm upgrade . When the sandbox operator’s upstream appVersion changes, apply the new CRDs before upgrading the operator:

 kubectl apply -f deploy/helm/sandbox-operator/crds/agent-sandbox-crds.yaml
helm upgrade sandbox-operator deploy/helm/sandbox-operator \
  --namespace agent-sandbox-system 

Uninstall environment resources before the shared operator. Removing the operator first leaves claims and custom resources without a controller.

 helm uninstall sandbox-env-production -n agent-sandbox-system
helm uninstall deco-studio -n deco-studio
# Only after every environment has removed sandbox-env:
helm uninstall sandbox-operator -n agent-sandbox-system 

Per-PR preview environments

The chart carries an optional preview block. It turns a release into a short-lived, self-contained environment: an ephemeral Postgres in the same namespace, migrations run once by a Job instead of racing across pods, and an HTTPRoute onto a shared wildcard Gateway . It is meant for reviewing a change before it merges — one throwaway Studio per pull request.

preview.enabled is false by default and every preview template is gated on it, so a normal install renders exactly what it rendered before. You do not need this to self-host Studio, and you should leave it off unless you are building a review pipeline.

Turning it on requires an orchestrator to create and destroy an environment per pull request. The suggested one is Argo CD, and the chart-deco-studio-previews chart in this repository ships that half: an ApplicationSet whose pullRequest generator watches a repository for a label, plus the shared Gateway . Point it at your own repository, domain and object storage — it has no defaults.

 helm template studio-previews deploy/helm/studio-previews \
  --set domain=pr.example.com \
  --set applicationSet.repo.owner=your-org \
  --set applicationSet.repo.name=your-fork \
  --set applicationSet.repo.tokenSecret.name=preview-github-token \
  --set studioChart.repoURL=oci://ghcr.io/decocms \
  --set studioChart.version="$STUDIO_CHART_VERSION" \
  --set studioChart.valuesRepo.url=https://github.com/your-org/your-fork.git \
  --set images.api=ghcr.io/your-org/studio-preview \
  --set images.nginx=ghcr.io/your-org/studio-nginx-preview \
  --set studioValues.externalSecret.secretPath=preview/studio \
  --set studioValues.externalSecret.secretStoreName=your-cluster-store \
  --set studioValues.objectStorage.endpoint=https://your-endpoint \
  --set studioValues.objectStorage.bucket=your-preview-bucket 

A preview is not a small production. It does not cover hosted agent sandboxes, OAuth sign-in, billing, the monitoring dashboard, outbound email, or anything that depends on more than one pod. It also authenticates nobody by itself — put an authorization policy on the Gateway listener before exposing it, because a preview accepts email/password signup and can drive LLM agents.

See deploy/helm/studio-previews/README.md for the full contract.

Found an error or want to improve this page?

Edit this page