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 that also brings up a Postgres, a Redis and an object store, an .env carrying freshly generated secrets, an .env.example you can commit, and a README.md written for whoever receives it.

stackbone package                          # writes dist/deploy
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 image in the folder too

On the receiving end the folder is two commands:

docker compose up -d
curl http://localhost:8080/health
Flag Type Description
--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 build the image and save it into the folder (around 150 MB). Default: false.

Once the container is up, register it with stackbone link so Stackbone knows its URL, its HMAC secret and the image tag it is running.

What the folder contains

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

What lands in the output directory:

dist/deploy/
  workspace/          # the compiled bundle, exactly what `stackbone build` writes
  Dockerfile          # FROM the published runtime base, COPY workspace/
  docker-compose.yml  # the agent, 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     # 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 bucket is created for you. The agent never creates a bucket, so the compose 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 takes one flag. By default the generated Dockerfile pulls the runtime base image, which is public, so whoever runs the folder has to reach a registry once. --offline builds the image on your machine and saves it into the folder as agent-image.tar, and the receiving side runs docker load -i agent-image.tar 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: building is the thing the flag exists to avoid.

Three things need saying to 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 the dashboard'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: the dashboard signs it and hands it 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. Point their 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 only the bundled MinIO gets one made for it, 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.

Hand the folder over intact. The generated .env holds the only copy of STACKBONE_SECRET_KEY, the key everything the agent stores is encrypted with. It also holds MINIO_ROOT_PASSWORD for the bundled object store, which the Compose file refuses to start without. 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"],
  "imageTag": "acme-agent:latest",
  "baseImage": "ghcr.io/stackbone/stackbone-workspace-runtime:node24-base",
  "workflows": ["onboarding", "rag-ingest", "mapping-suggest"],
  "deepAgents": ["support"],
  "degradedWorkflows": [], // shipped, but they will not run
  "port": 8080,
  "offline": false,
}

Exit codes: 0 ok, 1 a compile failed, or --offline could not run Docker (the message carries Docker's own output).

What you set on the deployed container

The image boots on three values. It 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.

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 container works out for itself: 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.

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.

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 then 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