stackbone package
stackbone packageruns inside a project folder and targets no installation, so it takes no--agent. Its--jsonpayload 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 tooOn 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 --offlineThe 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.