stackbone package

stackbone package runs inside a project folder and targets no installation, so it takes no --agent. Its --json payload uses the standard envelope. The folder it writes carries real secrets in its .env; the CLI itself prints none of them, see secrets are never printed.

Turn this project into a folder someone else can run, with no access to Stackbone and no knowledge of it. The folder holds your compiled workspace and five generated files: a two-line Dockerfile, a docker-compose.yml, an .env carrying generated secrets, an .env.example you can commit, and a README.md written for whoever receives it.

--target decides which of two folders you get.

Target Containers Who operates it
byoc (default) Your agent, a Postgres with pgvector, a Redis, a MinIO and the one-shot that seeds it You, from app.stackbone.ai, after registering the box. Your browser then calls it.
self-host The same, plus a control-plane container serving Studio from inside your network Whoever holds the deployment's one static token. Nothing outside the folder.
stackbone package                          # writes dist/deploy, the agent alone
stackbone package --target self-host       # the agent plus the dashboard
stackbone package --out-dir build/handoff  # anywhere else (relative to the project)
stackbone package --tag acme-agent:v3      # name the image the folder builds
stackbone package --offline                # put the Stackbone images in the folder too
Flag Type Description
--target string byoc (agent only) or self-host (agent + control plane + Studio). Default: byoc. Any other value is refused before the compile runs.
--out-dir string Where to write the deploy folder. Relative paths resolve against the project. Default: dist/deploy.
--tag string Image tag the deliverable builds and runs. Default: <agent-slug>:latest, or stackbone-agent:latest if never linked.
--base string Runtime base image to derive from. Defaults to the published Stackbone runtime base.
--offline boolean Also save the Stackbone images into the folder: agent-image.tar (around 150 MB) for byoc, stackbone-images.tar (around 300 MB) for self-host. Default: false.

Both targets ship the same compiled workspace, the same agent image and the same three infrastructure services, and both mint STACKBONE_SECRET_KEY, HMAC_SECRET and MINIO_ROOT_PASSWORD into the .env. Everything else that differs follows from where the dashboard lives: inside the folder, or at app.stackbone.ai.

byoc: the agent alone

The default, and the folder to hand over when the people who run it can open a browser on app.stackbone.ai.

docker compose up -d
curl http://localhost:8080/health

Then register the box. Open Workspaces in the dashboard, choose Open on the installation, and the wizard walks through it: Connect your box. It asks for the HMAC_SECRET line in this folder's .env, which is already filled in. stackbone link writes the same record from a terminal.

Registering records where the box lives. From then on your browser calls the box directly, and the box decides who you are by verifying the short-lived identity token the control plane minted for you, against that control plane's public keys. Two things follow from that, and both are in the generated README:

  • The address you register has to resolve from the machine with the browser on it, not only from the host running Docker. Nothing is proxied.
  • The dashboard's origin has to be on the box's CORS allowlist. Left unset it already is; STACKBONE_CORS_ALLOW_ORIGINS replaces the built-in list rather than extending it, so setting it means listing https://app.stackbone.ai again.

The folder ships no STUDIO_STANDALONE_TOKEN, and that absence arms the identity gate. See The one variable that changes the gate.

self-host: the agent and the dashboard

--target self-host adds a second Stackbone container. control-plane is a published image (ghcr.io/stackbone/stackbone-control-plane:node24) on port 3000: it serves the Studio SPA, the token login and the credential-reveal call, and it reaches the agent over the Compose network at http://agent:8080. Both containers read the same .env and share the Postgres, the Redis and the object store.

docker compose up -d
curl http://localhost:3000/api/v1/self-host/health    # the dashboard
curl http://localhost:8080/health                     # the agent

The dashboard is at http://localhost:3000. It asks for the STUDIO_STANDALONE_TOKEN line in .env and nothing else: no account, no email, no password. Both containers check that one string, so rotating it means restarting both.

Putting this on hostnames or behind TLS adds rules the two containers cannot enforce for you: the CORS allowlist, the five streaming routes, the Authorization header, one scheme for both. Self-host behind a proxy has them, with a reverse-proxy sample.

What the folder contains

It always compiles first. package runs the same compile stackbone build does, then packages what came out of it, rather than whatever is sitting in dist/. The folder you hand over days later still carries the code you packaged.

dist/deploy/
  workspace/             # the compiled bundle, exactly what `stackbone build` writes
  Dockerfile             # FROM the published runtime base, COPY workspace/
  docker-compose.yml     # the agent (and the control plane, on self-host), a Postgres
                         # with pgvector, a Redis, a MinIO, a bucket-creating one-shot
  .env                   # generated secrets; nothing you must fill in
  .env.example           # the same variables with no values, safe to commit
  README.md              # how to start it, use your own services, run it without Compose
  agent-image.tar        # byoc, only with --offline
  stackbone-images.tar   # self-host, only with --offline

The Dockerfile is two lines because the published base already carries the server and its pinned dependencies. Your image adds only the workspace, so there is no install step and the build takes seconds.

The compose file creates the bucket for you. The agent never creates one, so the file runs a one-shot container that creates it in the bundled MinIO and then exits. Creating it again is a no-op, so a later docker compose up -d costs nothing. The agent waits for that container to succeed, not just for MinIO to answer: a first upload against a bucket that does not exist yet fails with a NoSuchBucket nobody would trace back to the compose file.

Packaging for a machine with no registry access

By default whoever runs the folder pulls the Stackbone images, and every one of them is public, so they have to reach a registry once. --offline puts them in the folder instead, and the two targets save different archives:

Target Archive Holds
byoc agent-image.tar The agent image, built on your machine.
self-host stackbone-images.tar The agent image and the pulled control-plane image, together.

The receiving side runs docker load -i <archive> before docker compose up -d with nothing left to build. That variant needs Docker installed and running where you package, and it emits no build: block in the compose file: the flag exists to avoid building.

It is not a self-contained archive. The Postgres, the Redis and the MinIO are third-party images, so the first docker compose up still pulls them from a registry. A machine with no registry access at all needs those three saved and loaded separately.

Three things to tell whoever runs it

All three are already in the generated README, and each fails in a way that reads like a bug in your agent.

The database needs the pgvector extension available. The bundled compose file uses pgvector/pgvector:pg17 and is fine as it ships. Point the agent at a database of your own and the extension has to be available there, which on many managed databases only an administrator can create. See What you set on the deployed container below for the failure and the one-line fix.

Downloads from Studio's file browser need a reachable endpoint. The bundled MinIO answers to http://minio:9000, a name that only exists inside the Compose network. Everything the agent itself does with files works with that address, and so does RAG ingestion. A download link is the exception: it is signed and handed to a browser, which cannot resolve that name. If anyone will download files, set STACKBONE_S3_ENDPOINT in .env to an address both the container and the browser reach, such as http://the-host-you-run-this-on:9000. The compose file publishes port 9000 for that purpose.

The agent serves on port 8080, or on PORT when the host sets one. Hosts that assign a port and expect the application to adopt it (Cloud Run, Railway, Heroku, App Runner) need no extra configuration. The generated compose file pins PORT on purpose, because its port mapping and its health check are both written against 8080. Point a managed platform's health check at /live rather than /health: /live answers as soon as the server binds, while /health also probes the database, which the very first boot is still creating.

You can point it at a managed bucket instead

Any S3-compatible storage works (AWS S3, Cloudflare R2, Azure Blob, Google Cloud Storage, Railway Buckets, another MinIO). Create the bucket yourself first, because the compose file makes one only for the bundled MinIO, then fill in the STACKBONE_S3_* variables in .env. One of them decides how the bucket is addressed. STACKBONE_S3_FORCE_PATH_STYLE defaults to true, which puts the bucket in the path (https://host/bucket/key) and is what MinIO and Cloudflare R2 accept. Set it to false for a backend that only answers to the subdomain form (https://bucket.host/key), such as AWS S3 and Railway Buckets. The wrong value fails every call with NoSuchBucket or a bare 403 while the credentials are valid, so check it before you suspect the keys. See Path-style or subdomain.

The secrets in the .env

Hand the folder over intact. The generated .env holds the only copy of STACKBONE_SECRET_KEY, the key the agent encrypts everything it stores with.

Secret Target What it is
STACKBONE_SECRET_KEY both Encrypts every secret the agent stores. There is no recovery if it is lost.
HMAC_SECRET both On byoc, what registration proves and what the control plane signs its calls with. On self-host, what the two containers sign to each other with.
MINIO_ROOT_PASSWORD both Root password of the bundled object store, and the agent's STACKBONE_S3_SECRET_KEY.
STUDIO_STANDALONE_TOKEN self-host The one credential guarding the dashboard, and the only value anyone types.

Compose itself refuses to start on a missing HMAC_SECRET or MINIO_ROOT_PASSWORD, and on self-host also on a missing STUDIO_STANDALONE_TOKEN, STACKBONE_PUBLIC_URL or STACKBONE_SECRET_KEY; the message names the variable. A byoc folder missing STACKBONE_SECRET_KEY reaches the agent's own boot check instead, which stops the container before it opens a port and prints the same name.

The .env.example next to it carries the same variables with no values, so the folder can go into a repository while the .env stays out of it.

JSON payload

{
  "schema_version": 1,
  "outDir": "/abs/path/dist/deploy",
  "files": ["Dockerfile", "docker-compose.yml", ".env", ".env.example", "README.md"],
  "target": "byoc", // or "self-host"
  "imageTag": "acme-agent:latest",
  "baseImage": "ghcr.io/stackbone/stackbone-workspace-runtime:node24-base",
  "controlPlaneImage": null, // the published image on self-host, null on byoc
  "workflows": ["onboarding", "rag-ingest", "mapping-suggest"],
  "deepAgents": ["support"],
  "degradedWorkflows": [], // shipped, but they will not run
  "port": 8080,
  "controlPlanePort": null, // 3000 on self-host, null on byoc
  "offline": false,
}

Exit codes: 0 ok, 1 a compile failed, --target named something other than byoc or self-host, a flag was passed with an empty value, or --offline could not run Docker (the message carries Docker's own output).

What you set on the deployed container

The agent boots on three values and works the rest out for itself.

Variable What it is
DATABASE_URL The Postgres the runtime keeps its runs, sessions and secrets in. A completely empty database is fine: the first boot creates every table it needs. It does need pgvector, see below.
WORKFLOW_REDIS_URL The Redis backing the durable workflow engine.
STACKBONE_SECRET_KEY The key that encrypts every secret the runtime stores. Generate one with openssl rand -base64 32.

On a byoc box

Three more decide what address the box answers on and whose browsers it lets in. The generated .env ships all three commented out, because every unset behaviour is already the right one for a box registered from app.stackbone.ai.

Variable What it is
STACKBONE_PUBLIC_URL The box's own public origin. Nothing in the compose file needs it; Stackbone Connect builds an OAuth redirect_uri from it, as <value>/api/connect/callback. Set it once the box is behind its real address.
STACKBONE_CONTROL_PLANE_URL Whose browser tokens the box trusts. Unset means https://api.stackbone.ai. Set it only for a control plane of your own: the box derives the issuer, the audience and the JWKS URL from this one value.
STACKBONE_CORS_ALLOW_ORIGINS Comma-separated origins the box accepts browser calls from. It replaces the built-in list (https://app.stackbone.ai, https://chat.stackbone.ai, http://localhost:*), so anything you set is the whole allowlist.

package generates HMAC_SECRET into the .env rather than leaving it to the box, and that is the value the registration wizard asks for. A box started without one mints its own and prints it in the log once, in the clear; a value set before the first start is never printed at all.

On a self-host deployment

Running the dashboard beside the agent adds five more. The generated .env sets STUDIO_STANDALONE_TOKEN, HMAC_SECRET and STACKBONE_PUBLIC_URL, and documents the other two. The ones marked both have to hold the same value on each container: that is what a shared .env is for.

Variable Read by What it is
STUDIO_STANDALONE_TOKEN both The bearer the dashboard logs in with, and the only credential guarding the deployment. Any non-empty string. Change it and restart both containers, or they enforce two different tokens.
HMAC_SECRET both Signs the dashboard's server-side calls to the agent. A mismatch answers every one of them with 401 invalid_signature.
STACKBONE_PUBLIC_URL both Where a browser reaches the agent. Absolute http:// or https://, no credentials, no path, no query, no fragment. The dashboard is handed this at login and calls the agent with it.
STACKBONE_RUNTIME_INTERNAL_URL dashboard Where the dashboard container reaches the agent, server-side. Same shape rules. Defaults to http://agent:8080, the Compose service name. 127.0.0.1 here names the dashboard itself.
STACKBONE_CORS_ALLOW_ORIGINS agent Comma-separated origins the agent accepts browser calls from. It replaces the agent's built-in list, so anything you set is the whole allowlist. * matches one component.

Two more name the tenant, and both containers read them from the same lines: AGENT_ID and STACKBONE_ORG_SLUG. The dashboard seeds its organization and agent rows from them; the agent prefixes its storage keys and log records with them. Changing one after the first boot strands whatever the old value keyed. A byoc folder sets neither, because there is no second container to agree with.

CONTROL_PLANE_PORT moves the host port the dashboard is published on. The container always serves on 3000.

The one variable that changes the gate

STUDIO_STANDALONE_TOKEN decides which of two authentication strategies the agent arms, and the agent arms exactly one:

  • Unset, which is every byoc folder: the agent verifies short-lived identity tokens against the control plane's published keys, and admits only the installation it was registered as.
  • Set, which is every self-host folder: the agent turns that verification off, refuses every control-plane identity token, and accepts as its operator anyone presenting that one string as a bearer.

The variable belongs to the folder that ships its own dashboard, and only there. If an orchestrator template, a secret store or a copied .env line feeds it into a byoc container, that box admits anyone holding the string instead of the operators you gave Stackbone accounts to. Nothing about the container looks different: it stays healthy and it keeps answering.

The agent names the strategy it armed in its boot log. When a dashboard that worked starts refusing you, run docker compose logs agent | grep "identity gate". See A self-host box takes one operator token.

There is no STACKBONE_INSTALLATION_ID in a generated folder

That is on purpose. Each container mints its own installation id on first boot and keeps it, and nothing compares one against another. Setting it in .env would reach the agent too, which validates it as a UUID and exits on anything else.

Rules that hold on any host

Keep STACKBONE_SECRET_KEY stable and keep it safe. It cannot live in the database it protects, so it is the one value you always supply, and changing it makes every secret already stored unreadable, with no recovery.

The database needs the pgvector extension available. While creating its schema the first boot runs CREATE EXTENSION IF NOT EXISTS vector. A stock Postgres applies most of the schema and then stops with extension "vector" is not available, which reads like a bug in your agent and is not one. Nothing is damaged: add the extension and start it again. On many managed databases only an administrator can create it, so if your application role is not one, run this once as an administrator before the first boot:

CREATE EXTENSION IF NOT EXISTS vector;

A deploy folder from stackbone package runs pgvector/pgvector:pg17 for you and needs none of this.

What the agent works out for itself when you run it alone: on its first boot it mints an installation id and the signing secret stackbone link asks for, then stores both in its own database so every restart reuses them. It reads the agent and workspace labels off the manifest stackbone build stamped into the bundle. A deploy folder pins HMAC_SECRET in .env instead, so nothing has to be read back out of a log; a self-host folder pins AGENT_ID too, because the dashboard beside it has to agree on the tenant. The installation id stays minted per container in both.

You can still set any of those four yourself (STACKBONE_INSTALLATION_ID, HMAC_SECRET, AGENT_ID, WORKSPACE_ID) and your value wins. Take that route when your platform injects secrets from a vault and you want them decided before the container starts. STACKBONE_INSTALLATION_ID has to be a UUID. The boot rejects anything else and the container exits.

Setting both identity values is a special case: the boot then never reads the stored identity at all, which lets a deployment that predates the identity table come up unchanged. The boot cannot tell you that your values differ from what that database already holds, so if you pin them, pin them from one source and keep them stable. Set only one and the boot does compare, seeding the half you left out and warning when the half you pinned contradicts the stored one.

Your agents also need a model provider. That one comes from the Model provider screen on the deployment, or from MODEL_PROVIDER_API_KEY and MODEL_PROVIDER_BASE_URL on the container. See Production: zero config.

A missing value stops the container before it opens a port. The boot prints one block per broken variable: its name, what it is for, and a command that produces a value. A misconfigured container crash-loops on start, so a half-wired box never serves traffic.

PORT is optional. The image serves on 8080 when nothing sets it, and binds whatever PORT you do set, so a platform that assigns the port works untouched. A PORT that is not a usable port number fails the boot with a message naming the value, rather than falling back to 8080 and leaving the platform health-checking a port nothing listens on.

BUILT WITH ❤️ FROM CANADA AND SPAIN