--- title: 'package' description: 'Write a deploy folder someone else can run with Docker, and the variables that container boots on.' position: 11 --- # 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](/docs/cli/reference/conventions#json-output). The folder it writes > carries real secrets in its `.env`; the CLI itself prints none of them, see > [secrets are never printed](/docs/cli/reference/conventions#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. ```sh 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: ```sh 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: `: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`](/docs/cli/reference/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`](/docs/cli/reference/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: ```text 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](#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](/docs/sdk/data/storage#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** ```jsonc { "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: ```sql 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`](/docs/cli/reference/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`](/docs/cli/reference/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](/docs/sdk/reference/cli-integration#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.