Local Docker Deployment
Run Studio as a self-hosted Docker stack on your own machine using vibedata. After a handful of one-time commands, the CLI handles every remaining detail — rendering .env, pulling images, starting services, applying migrations, and importing laptop GitHub auth.
Setup path
Local Docker Studio uses the non-prod compose auth model: sign in to GitHub on the laptop before install, and sign in to Azure inside the container only when Fabric needs it. Generated compose/env files set NODE_ENV=development and AUTH_ENABLED=false; local delegated-auth/SSO mode is not a supported install compose output.
| Path | When to use | What it adds |
|---|---|---|
| Development/non-prod compose | Single operator, local validation, demos | NODE_ENV=development, AUTH_ENABLED=false, no mandatory web-login gate |
| Production/cloud | Shared deployment with SSO and real owners | Use production bootstrap plus Org Settings → Identity Providers and Org Settings → GitHub |
What you'll need
- Docker Engine or Docker Desktop running on the host
- Access to a web browser on your laptop for the device-code prompts
- GitHub CLI (
gh) on the host for the ambient GitHub session imported into the API container - Azure CLI (
az) only when Fabric workflows need the API container to hold an Azure session - For production provider setup, cloud-admin authority in the provider portals: Entra/OIDC app registration rights and GitHub App registration/installation rights. Production SSO is configured through Studio bootstrap and Org Settings, not
install compose.
How to install vibedata
Install the vibedata binary for your platform and put it on your PATH:
curl -fsSL https://github.com/accelerate-data/vibedata-official/releases/latest/download/install.sh | shThe script downloads the binary for your OS/architecture from the latest release on accelerate-data/vibedata-official.
Verify the install:
vibedata versionHow to install Studio
vibedata install composeinstall compose installs the Studio version this CLI was built for — the CLI and Studio are paired strictly 1:1, so there is no version to choose. It reads everything it needs from DATA_DIR/state.json: host port choices, generated database secrets, the installation data-encryption key and explicit key identifier, and observability credentials. A fresh installation exposes Studio on port 5173; use --frontend-port <port> only when that port is unavailable. The selected port is persisted, so a later install without the flag keeps it. It does not prompt for anything on a fresh install, and it always renders development/non-prod auth (NODE_ENV=development, AUTH_ENABLED=false). Delegated auth and bootstrap-key configuration are production/cloud concerns, not supported compose installer inputs. Operators may manually edit generated files, but that is outside the supported installer contract.
LLM provider: Studio's LLM provider — the API key and optional base URL — is configured inside Studio after install, under Org Settings → LLM.
Default install (lean / core-only)
The default install compose ships only the Studio core services — no LGTM or Alloy. The lean install needs roughly 3.5 GB of images plus ~1 GB of DATA_DIR headroom; adding --with-observability (LGTM/Grafana/Alloy) needs more than that — check vibedata doctor compose for the exact resource floor before installing.
In lean mode the Studio backend's OpenTelemetry SDK falls back to its file exporter at DATA_DIR/logs/ (server.log, error.log, http.log). The success message prints the resolved logs path. The choice is persisted to state.json; pass --with-observability to opt in. Grafana credentials in state are preserved across the toggle so re-enabling never rotates them.
Opt-in observability
| Flag | What it adds |
|---|---|
--with-observability | LGTM (Grafana + Loki + Tempo + Prometheus) + Alloy, including LLM/agent trace visibility |
--no-observability | Removes them, leaving the core services |
--full-observability is retired — Studio no longer provisions Langfuse — and rejects with an actionable error instead of starting anything.
Requires Docker Desktop resources sized for the chosen profile.
Forgetting the flag on a later run never turns observability off. compose up with no flag starts whatever is installed, and re-running install compose without one stops and shows you both commands — the one that keeps your current profile and the one that removes it. Removal is always --no-observability, and even then the collected data stays under DATA_DIR, so re-enabling picks up where you left off.
Per-container memory caps
Every container carries a memory cap, so an operator can size the host for the chosen profile. If a container exceeds its cap the kernel stops it (exit code 137) and Compose restarts it — the stack can look healthy again while the symptom shows up elsewhere, usually as a page or request that never finishes. docker inspect --format '{{.State.OOMKilled}} {{.RestartCount}}' <container> tells you whether that happened.
Core services:
| Profile (flag) | Container | Memory cap |
|---|---|---|
| all | api | 1 GB |
| all | postgres | 512 MB |
| all | obot | 512 MB |
| all | frontend | 64 MB |
Optional observability containers:
| Profile (flag) | Container | Memory cap |
|---|---|---|
--with-observability (LGTM) | otel-lgtm | 2 GB |
--with-observability (LGTM) | alloy | 512 MB |
--with-observability (LGTM) | nginx | 64 MB |
--with-observability (LGTM) | nginx-exporter | 32 MB |
--with-observability (LGTM) | cadvisor | 256 MB |
--full-observability is retired — Studio no longer provisions Langfuse — and rejects with an actionable error instead of rendering anything.
cadvisor (256 MB, shown above) runs with privileged: true so it can read every other container's cgroups — that means the observability profile mounts the host root and the Docker socket into a privileged container, and the metrics it scrapes stay on the internal network (no host port is published). It is opt-in via --with-observability and is not present in a core install.
Core caps total ≈ 2.1 GB, and each activated Intent adds an agent container beyond that. LGTM adds ≈ 2.8 GB of capped headroom on top of core. Core installs have no memory admission gate — the observability profile does. These caps live in deploy/docker/docker-compose.yml and docker-compose.lgtm.yml — treat those files as the source of truth if the numbers here drift.
After resolving inputs, install compose:
- Validates Docker authority, host ports, and a writable
DATA_DIR - Renders
DATA_DIR/.envandDATA_DIR/docker-compose.yml - Pulls the selected Studio images, upstream PostgreSQL, and (in full mode) observability images
- Starts the Compose stack
- Applies database migrations
- Reports ready, degraded, or blocked
DATA_DIR defaults to ~/.vibedata/studio on macOS and Linux, %USERPROFILE%\.vibedata\studio on Windows. Override by exporting DATA_DIR before running the command.
Generated secret files
DATA_DIR/.env and DATA_DIR/state.json contain generated service, signing, encryption, and, when bundled companion apps are configured, OAuth-client values. On POSIX hosts, Vibedata writes both files as owner-readable only (0600) and preserves that restriction on rerender and update. Do not share these files with a group or commit or copy them into a shared location.
Docker's UID mapping can determine the owning account on the host. If a manual copy, restore, or filesystem migration broadens access, restore owner-only access before restarting or updating the stack:
chmod 600 "$DATA_DIR/.env" "$DATA_DIR/state.json"credentials.json is also owner-only, but stores CLI metadata only; login tokens remain in the operating-system keyring. The gh-config and azure-config directories are credential caches managed by the GitHub and Azure CLIs respectively, so keep the entire DATA_DIR private.
Accessing Studio from a different machine
Studio's UI binds all host interfaces (0.0.0.0), so from another machine on the same network just open http://<host-ip>:5173 — no SSH tunnel needed. Studio itself accepts whatever address you open it with, so a hostname works too.
To use any bundled companion app over the network, set --studio-url. Studio's own screens work off-host with no flag, but the companion apps — Obot (always installed) and, with observability, Grafana — sign in through Studio's OAuth, and that login must bounce back to a fixed pre-registered address. Install with --studio-url set to the address others will use:
vibedata install compose --studio-url http://<host-ip>:5173
# with observability:
vibedata install compose --with-observability --studio-url http://<host-ip>:5173This one value drives Studio and every companion app. It's persisted, so compose up, restart, and update keep it. To change it later, reinstall with the new --studio-url — the companion OAuth clients are re-registered to the new address automatically. It must be http://host or http://host:port only (no path, and HTTPS is not supported for Local Docker).
Security: Local Docker runs with no login, so anyone who can reach the host on port 5173 gets full access. Use your host firewall or network to control who can reach it. Only the Studio UI port is exposed this way; databases and other internal services stay private.
If the host isn't directly reachable (e.g. a cloud VM behind a firewall), forward the port over SSH instead and open http://localhost:5173:
ssh -N -L 5173:localhost:5173 <user>@<remote-host>How to sign in to Azure and GitHub
Before install, sign in to GitHub on your laptop with the account you want Studio to use for local ambient login and repo operations:
gh auth login --scopes repo,read:org,workflowDuring vibedata install compose, vibedata imports that laptop GitHub session into the Studio API container and stores it under DATA_DIR/gh-config. If GitHub is not signed in on the laptop, install blocks and asks you to run gh auth login, then rerun install.
Once the stack is up, sign Studio's container into Azure if you use Fabric. This command runs inside the Studio container, and the device-code prompt streams to your terminal.
The commands below assume the default
DATA_DIRof~/.vibedata/studio. If you installed with a customDATA_DIR, substitute that path for~/.vibedata/studio.
# Sign in to Azure (device-code flow; pick a subscription at the prompt
# when you have several)
docker compose -f ~/.vibedata/studio/docker-compose.yml exec -it api az login --use-device-code
# Verify GitHub in the API container
docker compose -f ~/.vibedata/studio/docker-compose.yml exec api gh auth token >/dev/null
# Verify Azure in the API container when Fabric needs it
docker compose -f ~/.vibedata/studio/docker-compose.yml exec api az account show --output tableSkip az login if you only use DuckDB-target pipelines (no Fabric). Laptop gh auth login is required to create a domain.
Local GitHub CLI identity
Local Docker uses the operator's laptop GitHub CLI session as Studio's user identity. vibedata install compose imports the laptop token into the API container; you do not need to mount ~/.config/gh or run gh auth login inside the container.
Verify the imported session from inside the running API container:
docker compose -f ~/.vibedata/studio/docker-compose.yml exec api gh auth token >/dev/nullIf that command fails, run gh auth login --scopes repo,read:org,workflow on the laptop and rerun vibedata install compose. Studio ignores GIT_PAT_TOKEN and GITHUB_PAT for ambient GitHub auth; the gh CLI session is the source of truth.
Upgrade note — GitHub workflow permission
The GitHub session needs the workflow scope when Studio pushes a Domain template that creates or changes a file in .github/workflows/. A deployment can keep working until that first changed workflow file is pushed, so a successful earlier Intent does not prove the scope is present.
Before or after updating Studio, check the API container's GitHub session:
docker compose -f ~/.vibedata/studio/docker-compose.yml exec api gh auth status -h github.comIf the output does not list workflow, refresh the persisted container session:
docker compose -f ~/.vibedata/studio/docker-compose.yml exec -it api gh auth refresh -h github.com -s workflowThe command updates DATA_DIR/gh-config; no Studio or Docker restart is needed. For a different laptop GitHub account, rerun vibedata install compose instead.
To switch the active Azure subscription later without signing in again, list available subscriptions and set by ID or display name (a bare number is not valid):
# List subscriptions to find the ID or name you want
docker compose -f ~/.vibedata/studio/docker-compose.yml exec -T api az account list --output table
# Set by subscription ID (preferred — UUID, no ambiguity)
docker compose -f ~/.vibedata/studio/docker-compose.yml exec -T api az account set --subscription 8b3a7f9d-4e2c-4af1-9234-1234abcd5678
# Or set by display name (quote it if it has spaces)
docker compose -f ~/.vibedata/studio/docker-compose.yml exec -T api az account set --subscription "My Subscription"These commands cover the provider tokens Studio's backend needs, separate from Studio's web-login gate (AUTH_ENABLED).
Persistence across container restart
Sessions live in bind-mounted folders under DATA_DIR (azure-config/ and gh-config/), so docker compose down && up keeps you signed in. You only sign in again when:
- You explicitly re-run
az loginin the container orgh auth loginon the laptop. - The refresh token expires (Azure refresh tokens are ~90 days sliding; GitHub tokens typically last until you revoke them).
- You delete
DATA_DIRor its cache folders.
How to update Studio
vibedata update composeReuses every value already in DATA_DIR/state.json — zero prompts. Pulls new images, recreates containers, reapplies migrations, and reconnects to your existing PostgreSQL and observability data. If anything fails, the command reports a blocked state and leaves the existing installation intact.
update compose preserves the persisted observability profile — if the install was lean, the update stays lean. To change topology, re-run install compose or compose up with the desired --with-observability flag (or no flag to return to lean).
update compose takes a backup for you before it replaces any container — see How to back up and restore.
It also stops with a blocked state if DATA_DIR/state.json has no data-encryption-key or no explicit data-encryption-key-id, rather than generating or defaulting either value. A changed keyring would leave every credential Studio has already encrypted permanently unreadable. Restore the original state.json from a backup, or restore a whole package with vibedata restore compose; an existing installation cannot be repaired by choosing a replacement key.
To change Studio's LLM provider, use Org Settings → LLM inside Studio.
What your users see during an update
update compose shows every Studio page a Studio is under maintenance notice for the whole window, and clears it once Studio answers its readiness check. You do not switch it on or off yourself.
It stays on if the update fails part-way. That is deliberate: a half-replaced Studio should not take traffic. Fix the cause, run the update again, and it clears itself when the run succeeds.
If you need to serve normally again before then — say you restored a backup instead — clear it yourself:
vibedata maintenance offYou can also take Studio offline deliberately, for a database migration or any other work:
vibedata maintenance onA window you open this way is yours to close. An update running inside it will not clear it for you.
The notice is served by the frontend container itself, so it keeps working while the API and database are down, and switching it needs no restart.
How to back up and restore
Database migrations have no down-path, so an update that goes wrong is recovered by restoring a backup, not by rolling the images back. A backup is therefore taken automatically before every update, and you can take one whenever you like.
Automatic, before every update
vibedata update compose backs up on its own: it holds the lifecycle lock, waits for the upgrade gate to clear, then packages the running install just before containers are replaced. If the backup fails, the update stops and the existing installation is left untouched. --skip-backup overrides that and says clearly in the output that the update has no restore point — a failure then has no way back.
Manual, any time
vibedata backup composePackages the running install into DATA_DIR/backups/manual-<version>-<timestamp>/ and prints the path. Backups taken by an update are named pre-upgrade-<target version>/ instead. The newest 2 packages are kept; older ones and any half-written folder are removed.
One package contains a dump per database (Studio, the agent service, and Langfuse when it is installed), a tar of the DATA_DIR content that exists nowhere else, a copy of .env and state.json, and a manifest.json written last. That last file is what marks the package complete — a folder without one is never restored from.
The backups folder is on the same disk as the data it protects. One machine has no second disk to offer, so a lost disk loses both. If this installation matters, copy the package folders somewhere else. They are as sensitive as the rest of
DATA_DIR: they hold the encryption key and plaintext connector secrets, so keep them private.
Restore
vibedata restore compose --from pre-upgrade-v1.2.3--from takes a folder name under DATA_DIR/backups/ or an absolute path to a package anywhere. The command prints the package's Studio version and date and asks for confirmation first; --yes skips the prompt for scripted runs. It then stops the stack, puts the package's .env and state.json back, restores the databases and data files, and starts the stack again on the package's own images — so a failed update lands you back on the version the package came from.
Langfuse's data is restored only when its local trace store is still in place. Without those traces its dashboards would point at nothing, so a package restored onto a wiped machine starts Langfuse empty.
After a restore, sign in again where Studio needs a platform session (gh on the laptop, and az inside the container if you use Fabric). Those token caches are deliberately not part of a package.
Moving an installation to another machine
Because .env and state.json ride inside the package, there is no key to move by hand. On the new machine, install Studio with the vibedata binary matching the package's Studio version, copy the package folder across, then restore it:
vibedata install compose
vibedata restore compose --from /path/to/pre-upgrade-v1.2.3How to control the running stack
| Command | What it does |
|---|---|
vibedata compose up [--with-observability] | Starts the stack from DATA_DIR. Bare invocation re-renders against the lean profile; pass --with-observability to opt in. Persists the choice. |
vibedata compose down | Stops services; preserves databases, app data, generated secrets, plugin storage, and observability state |
vibedata compose restart | Restarts services and re-runs readiness checks |
vibedata cleanup [--force | -y] [--keep-images] | Tears the stack down and reclaims Docker resources — removes this project's containers, network, volumes, and (by default) images. Preserves DATA_DIR (databases, secrets, generated state). Scoped to the Studio Compose project only. Prints a summary and prompts for confirmation unless --force/-y; --keep-images keeps pulled images. Idempotent on an already-clean environment. |
compose up/down/restart operate on the already-rendered docker-compose.yml in DATA_DIR and never modify state or prompt for input. cleanup is the exception: it removes Docker resources and prompts for confirmation first (skip with --force).
Production SSO and GitHub setup
Production/cloud deployments use AUTH_ENABLED=true and a bootstrap key, but that is not a local Docker installer mode. A cloud admin registers Entra/OIDC apps in the provider portal, then uses production Studio bootstrap to enter Org Settings → Identity Providers. The bootstrap key gets the operator into Org Settings only for SSO setup and the first-owner grant; it is not the steady-state admin identity.
After the Identity Provider test succeeds in production Studio, the operator grants a real user vibedata_owner with the Users API/UI, signs in normally through SSO as that owner, then configures Org Settings → GitHub, Org Settings → Data Platforms, users, service principals, and other admin surfaces. The GitHub App is created in GitHub developer settings, installed on the target org/repositories, and registered in Org Settings → GitHub after owner sign-in.
Global options
These work on every vibedata command:
| Option | Effect |
|---|---|
--json | Emits machine-readable output. Long-running commands emit event streams. |
--verbose | Extended diagnostic output. Raw secrets are still redacted. |
What you'll see (states)
- Ready — installation complete, app entrypoint reachable, migrations applied, durable storage writable
- Degraded — install or update succeeded, but a non-critical surface (such as observability or one provider's sign-in) is reporting an issue; the command names the affected check and the operator action
- Blocked — the command cannot proceed safely; reasons include missing Docker authority, port conflicts, registry failure, missing provider authority, missing CLI sign-in, stale generated files, or unrecoverable encryption material. The output names the precondition that failed and the operator action required.
- Not configured —
DATA_DIRis missing, unreadable, or not avibedata-managed installation
Quick reference
Default (no SSO)
| Command | What it does |
|---|---|
vibedata install compose | First-time install or recovery from durable state (installs the Studio version this CLI was built for, lean profile by default). |
vibedata install compose --frontend-port <port> | Use another browser-facing host port when 5173 is unavailable. It persists for later lifecycle commands until explicitly changed. |
vibedata install compose --with-observability | Install with LGTM + Alloy |
vibedata install compose --studio-url http://<host-ip>:5173 | Set the external URL others use to reach Studio over the LAN; drives Studio and companion-app (Obot always, Grafana with observability) logins. http://host[:port] only. |
docker compose -f ~/.vibedata/studio/docker-compose.yml exec -it api az login --use-device-code | Sign Studio's container into Azure (device-code flow) |
docker compose ... exec -it api az account set --subscription <subscription-id> | Switch the active Azure subscription without re-signing in |
gh auth login --scopes repo,read:org,workflow | Sign the laptop into GitHub; vibedata install compose imports this session into Studio's container |
vibedata compose up [--with-observability] / down / restart | Start (lean by default, or opt into the observability stack), stop (preserving data), or restart |
vibedata update compose | Roll the install to the Studio version this CLI was built for; preserves data and backs up first |
vibedata backup compose | Package the running install into DATA_DIR/backups/; keeps the newest 2 packages |
vibedata restore compose --from <folder | path> [--yes] | Put a package back in place, on the version it came from. Confirms first unless --yes |
vibedata cleanup [--force | -y] [--keep-images] | Tear the stack down and reclaim Docker resources (containers, network, volumes, images); preserves DATA_DIR. Prompts unless --force; --keep-images keeps images |
vibedata version | Print the CLI version |
Exit codes: 0 success · 1 generic error · 2 misuse · 3 blocked · 4 not configured.
Single-user evaluation settings
Local Docker has two single-user evaluation modes:
| Mode | Use it when | Observability |
|---|---|---|
| Base | You need the smallest Studio evaluation install. | No Grafana or Alloy services are rendered. |
--with-observability | You want evaluation diagnostics, including agent and LLM trace visibility. | Grafana, Loki, Tempo, Prometheus, and Alloy collect Studio telemetry. |
--full-observability is retired — Studio no longer provisions Langfuse — and rejects with an actionable error instead of rendering anything.
For either observability mode, the supported retention settings in the generated DATA_DIR/.env are LOKI_RETENTION_HOURS (default 168), TEMPO_RETENTION_HOURS (default 72), and METRICS_RETENTION_HOURS (default 168). Edit only those values, then recreate the already-selected topology without re-rendering generated files:
vibedata compose up --use-rendered-configKeep each retention value between 1 and 168 hours inclusive for a local evaluation host — this bound is uniform across all three variables. Values outside these bounds are rejected before Docker starts.
If an existing install's DATA_DIR/.env still has METRICS_RETENTION_HOURS set above 168 (for example, the historical 336-hour default), vibedata compose up --use-rendered-config now rejects it, since that command reads the file as-is instead of re-rendering it. Lower the value to 1-168 or remove the line to fall back to the default. A normal compose up, install compose, or update is unaffected because it re-renders .env before validation runs.
Regardless of observability mode, the generated DATA_DIR/.env also supports LOG_LEVEL (default info; one of trace, debug, info, warn, error, fatal, silent), OPENHANDS_LOG_LEVEL (default INFO; one of DEBUG, INFO, WARNING, ERROR, CRITICAL), and LOG_LOCAL_RETAIN_HOURS (default 168; 1-168 hours inclusive, the same ceiling as the retention variables above). Apply changes the same way as the retention variables above: edit DATA_DIR/.env, then run vibedata compose up --use-rendered-config.
A normal compose up, install, or update re-renders .env and restores these defaults. Changing arbitrary generated environment variables is unsupported: internal URLs, network names, images, secrets, and raw OpenTelemetry settings can break the deployment. The complete defaults, restart scope, and boundaries are in docs/design/container-architecture/README.md under "Single-user local Docker tuning contract."
These modes do not size Studio for multiple concurrent agent turns. Kubernetes capacity settings for that workload are being defined separately in VD-3518.
Related
- Functional spec:
docs/functional/vibedata-cli/README.md - Functional spec:
docs/functional/container-deployment/README.md - Architecture:
docs/design/container-architecture/README.md