Docker
Run ObjectOS with Docker — pin the image, set the three required variables, add PostgreSQL with Compose, wire the probes and the proxy, upgrade and roll back. The Enterprise image adds a private registry, verification and a license.
Docker is the standard way to run ObjectOS on your own servers. The steps
are the same for both self-managed editions: one image, pinned exactly, with
the configuration injected at runtime and the database outside the
container. This page walks them with the open Community image,
ghcr.io/objectstack-ai/objectstack; the Enterprise adds
section at the end covers what changes on the licensed image.
Pin the image
The Community image is published on every ObjectStack release. Its tags
mirror the version of the ObjectStack CLI inside it: a release X.Y.Z is
pushed as X.Y.Z, and moves latest, X.Y and X onto it. The rolling
tags move with every publish, so a routine docker compose pull on latest
can replace the version you run with no deployment event.
The exact tag fixes the version, not the bytes. The maintainers can rebuild
a version that is already out, for example to pick up security patches in
the node:22-slim base image, and the rebuild pushes the same X.Y.Z tag
again, onto a new image. The ObjectStack CLI inside stays the same version;
the base image, and the database drivers it installs by version range, are
resolved afresh. The
publish workflow
declares that rebuild. Only a digest names the same bytes for good.
Run the exact version in production, the same version as the CLI that built your app, and pin its digest wherever you need the same bytes on every host and after every restart. Read the digest from the registry:
docker buildx imagetools inspect ghcr.io/objectstack-ai/objectstack:X.Y.ZPin the first Digest: line, the one under Name:. It names the whole
image, both architectures; the digests listed under Manifests: each name
one platform. Reference the image by that digest, and keep the version
beside it, since a digest does not say which release it is:
# ObjectStack X.Y.Z
ghcr.io/objectstack-ai/objectstack@sha256:<64 hex characters>Keep the previous version and its digest written down: that is what a
rollback is. The image is published for linux/amd64 and linux/arm64,
runs as a non-root user, listens on port 8080, and carries its own
health check. It includes the PostgreSQL (pg) and MySQL (mysql2)
drivers.
The three values you set
Everything else has a workable default. These three do not:
| Variable | What happens without it |
|---|---|
OS_DATABASE_URL | Data lands in a SQLite file under the runtime's home directory — fine on one durable host, lost with a container's filesystem. Use postgres://… in production. |
OS_AUTH_SECRET | The sign-in endpoints under /api/v1/auth/* are skipped, and the server runs with no authentication. |
OS_SECRET_KEY | The runtime mints its own key for stored secrets and keeps it on its filesystem. In a container that key dies with the container, and every secret encrypted under it becomes unreadable. |
Generate the two secrets once and keep them in your secret manager:
OS_AUTH_SECRET=$(openssl rand -hex 32)
OS_SECRET_KEY=$(openssl rand -hex 32)Never bake either secret into an image. Pass them at runtime from your
orchestrator's secret store. Back up OS_SECRET_KEY with the database: a
database restored without its key cannot decrypt its own secrets — see
Backup. The full list is
Environment Variables.
Run it
The image carries no app. Hand it the objectstack.json that os build
compiles, in one of three ways.
Mount the artifact. No image build at all — the image looks for the app
at /srv/app/objectstack.json:
docker run -p 8080:8080 \
-v "$PWD/dist/objectstack.json:/srv/app/objectstack.json:ro" \
-e OS_DATABASE_URL="postgres://objectos:secret@db-host:5432/objectos" \
-e OS_AUTH_SECRET \
-e OS_SECRET_KEY \
ghcr.io/objectstack-ai/objectstack:X.Y.ZName the artifact by URL. OS_ARTIFACT_URL points at a published
artifact, so a fixed image plus one variable is a running app, and upgrading
the app is a change of that variable and a restart:
docker run -p 8080:8080 \
-e OS_ARTIFACT_URL="https://releases.example.com/crm-2.2.2.json#sha256=<64 hex characters>" \
-e OS_DATABASE_URL -e OS_AUTH_SECRET -e OS_SECRET_KEY \
ghcr.io/objectstack-ai/objectstack:X.Y.ZThe optional #sha256= fragment pins the artifact's bytes, and the runtime
holds the boot to it:
| Situation | What the runtime does |
|---|---|
| No pin | Boots without verification. If the artifact cannot be fetched, the boot fails so your orchestrator retries — there is no cached copy to fall back on. |
| Pin present, bytes match | Boots, and keeps the verified copy. |
| Pin present, bytes differ | Refuses to boot, naming the expected and the actual digest. |
| Pin present, host unreachable | Boots from the cached copy only if it still matches the pin, and says so with a warning. |
file:///… works too, for a mounted file. Publish artifacts as immutable,
version-named objects and give only your release pipeline write access to
them — that is what makes rolling the app back the same operation as rolling
it forward.
Copy the artifact into your own image. One self-contained image per
release. The project scaffold (npm create objectstack) writes a Dockerfile
of this shape for you:
FROM node:22-slim AS build
WORKDIR /app
COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml* package-lock.json* ./
RUN if [ -f pnpm-lock.yaml ]; then corepack pnpm@10 install --frozen-lockfile; \
else npm ci; fi
COPY . .
RUN npx os build # → dist/objectstack.json
FROM ghcr.io/objectstack-ai/objectstack:X.Y.Z
COPY --from=build --chown=node:node /app/dist/objectstack.json /srv/app/objectstack.jsonCompose with PostgreSQL
A single-host production stack: the image above with its database beside it.
services:
app:
build: .
ports:
- "8080:8080"
environment:
OS_DATABASE_URL: postgres://objectos:${POSTGRES_PASSWORD}@db:5432/objectos
OS_AUTH_SECRET: ${OS_AUTH_SECRET}
OS_SECRET_KEY: ${OS_SECRET_KEY}
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:17
environment:
POSTGRES_USER: objectos
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: objectos
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U objectos -d objectos"]
interval: 5s
timeout: 3s
retries: 10
restart: unless-stopped
volumes:
pgdata:# .env beside docker-compose.yml — never committed
POSTGRES_PASSWORD=…
OS_AUTH_SECRET=…
OS_SECRET_KEY=…
docker compose up -d
curl -fsS http://localhost:8080/api/v1/healthWhen you move to a managed PostgreSQL, point OS_DATABASE_URL at it and
remove the db service. The schema is brought up to date when the runtime
boots: safe changes run on their own, and a destructive one refuses the
boot and names each change, until you apply it deliberately with
os migrate apply --allow-destructive
(CLI).
Health checks
Two endpoints, and they are not interchangeable:
| Endpoint | Answers | Wire it to |
|---|---|---|
/api/v1/health | The process is up and serving HTTP. It never touches the database. | Liveness |
/api/v1/ready | The runtime has booted and its data drivers answer. 503, naming the failing driver, while one does not. | Readiness, and your load balancer |
Crossed over, they fail badly. A liveness probe on /api/v1/ready restarts
every replica during a database outage — which a restart cannot fix — and
turns a configuration fault into a restart loop that hides it. Readiness is
the one that should react: it takes a replica out of rotation while its
database is away and puts it back when the database returns, with no
restart. A datasource the app binds to must also connect at startup, or the
boot fails outright: an unreachable database is a failed deploy, not a
container that comes up and then errors on every request.
Reverse proxy and TLS
Terminate TLS in front of the runtime — Caddy, nginx, Traefik or your cloud load balancer — and keep three values in step with the public address:
OS_AUTH_URL=https://app.example.com # sign-in callbacks and cookie origin
OS_TRUSTED_ORIGINS=https://app.example.com # sign-in callback and CSRF allow-list
OS_PORT=8080 # the proxy's upstream portapp.example.com {
reverse_proxy localhost:8080
}A drifted port or origin is the classic self-hosting failure: the app runs,
but sign-ins bounce and browsers block API calls. CORS needs no entry while
the Console and the API share the public origin. A front end served from
another origin is listed in OS_CORS_ORIGIN, explicitly — * is
refused at boot while credentials are on. Turn on HSTS only once TLS is
confirmed; Production Readiness has the rest.
First boot: the first administrator
On a fresh database, open the deployment's address and register — the
first account becomes the administrator. Do it straight after the first
deploy, before you share the address. The production server seeds no
default credentials: the admin@objectos.ai account you may know from
os dev exists only on development databases.
To name the administrators in configuration instead, set
OS_PLATFORM_OWNER_EMAIL to one address or a comma-separated list; each
listed account holds the standing once its email address is verified. A
multi-organization posture requires it — see
Environment Variables.
An app that seeds people records without sign-in accounts closes this door: the first-account rule counts people, not logins. The boot log says so — see Troubleshooting.
Multi-node
ObjectOS Community runs one replica. Coordination — locks, queues,
schedules — is in-process, so a second replica would not share it.
Declaring several replicas (OS_CLUSTER_REPLICAS above 1) without a
license refuses the boot rather than run them on per-process locks.
Multi-node operation is an Enterprise license capability, and today the
Enterprise kit runs on a single node too — see
Multi-node below.
Upgrade and rollback
- Read the release notes of every ObjectStack version you cross, and the version-specific steps they ask of the operator.
- Back up the database. Schema migrations go forward: rolling the image back does not roll the schema back.
- Change the image to the new exact version, or to its digest if you pin one, and restart. The schema is brought up to date at boot, before the runtime reports ready.
- Confirm
/api/v1/readyanswers 200. - To roll back, restore the previous tag or digest and repeat step 3 — valid as long as the version you are leaving made no incompatible schema change.
The app versions separately: changing OS_ARTIFACT_URL, or the artifact you
mount, upgrades it without touching the image.
Enterprise adds
The licensed Enterprise image runs the same way, and adds a private registry, supply-chain verification, a license, and multi-node; its release notes name the digest to pin. Your release ships a deploy bundle containing a Compose stack, an environment template per deployment shape, and a README with the commands for your exact version. This section is the decision map for what that bundle configures.
What you are given
Your license comes with registry credentials for the private runtime
image. Signing in requires a token with package read access; there is no
public Enterprise image and no source build: the image is produced by our
release pipeline from sources that are not distributed, so docker build is
not a path that exists for it.
Supported architecture: linux/amd64 only. That is a declaration, not an
omission: the build refuses to produce an image for an architecture we have
not declared. Apple Silicon and Graviton hosts would run it under emulation,
which is fine for a look and wrong for production.
Pin the image by digest
The image reference you run in production is a digest, not a tag:
OS_EE_IMAGE=<the registry reference from your release>@sha256:<64 hex characters>A tag is a human-readable name and can be re-pointed at different bytes. A digest is content-addressed: the same digest is always the same image, on every host, forever. With a tag, there is no way to prove afterwards what was actually running when something went wrong. Your release notes give the digest; paste it verbatim. Keep the previous digest too.
The image can tell you what it is
Two different version numbers travel with the image and should not be conflated: the product version of ObjectOS, and the compatibility range of the ObjectStack runtime embedded in it, which moves on its own schedule. Both — along with the source revisions the image was built from — are recorded inside the image itself, as OCI labels and as a machine-readable release manifest. You never have to ask us which build you have.
That manifest is also the single most useful thing to attach to a support request: it is enough to reproduce your exact image.
Verify the image before production
The image is signed (keyless, through our release pipeline's OIDC identity) and ships with an SBOM and build provenance. Verify once, before a production rollout — the three checks answer three different questions:
| Check | The question it answers |
|---|---|
| Signature | Was this digest produced by the ObjectOS release pipeline, and not by someone else? |
| SBOM | What is inside this image? |
| Provenance | How was this image built, from which sources? |
The bundle README carries the verification commands with the identity and issuer to check against, matched to your release.
License
A license is part of the happy path. How the Enterprise image behaves without one depends on what you asked it to be:
- Single-organization, no license — the deployment runs as ObjectOS Community: the open MCP server, bring your own AI. Nothing bricks.
- Single-organization, licensed — the in-product AI is unlocked on your own infrastructure: the AI Builder and "ask your data".
- Multi-organization (a walled tenancy posture), no license — the runtime exits at startup, naming the license. Isolation between organizations is a licensed capability, and a deployment that asked for isolation must never serve traffic pretending to have it.
There is a second constraint on the same line, and it is the one that surprises people: a license has a mode, and the mode is coupled to your cloud posture. An ordinary license is validated online against a control plane; an air-gap license is verified locally and needs no network at all. Pairing an ordinary license with "no control plane" is not a degraded state, it is an impossible one — and it is refused at startup, naming both settings, rather than failing later with a misleading complaint about the license. Air-gapped has the full matrix of supported combinations.
A walled deployment must declare two more decisions
On a multi-organization posture, two settings have no safe default, so the runtime refuses to start until you decide them — declaring either available value is accepted; not deciding is not:
| Decision | Why it cannot default |
|---|---|
| What a new sign-up joins | Where a membership is the tenant boundary, it may only be granted by an explicit act — an invitation, an admin, SSO provisioning. Automatic joining would put a stranger inside an existing organization. |
| Which AI agents are mounted | The "ask" agent reads; the "build" agent authors metadata — and metadata is scoped to the deployment, not to an organization. On a shared database, one customer's build turn rewrites the schema every other customer runs on. |
A single-organization deployment sees neither check.
The shipped stack
The Compose stack in the bundle sequences a deployment the way it has to be sequenced, and it is worth knowing the order because any other orchestrator has to reproduce it:
- The database becomes healthy.
- A one-shot migration runs the schema to completion — before any application replica starts, so concurrent replicas never run schema changes against each other.
- Application replicas start. The image carries its own health check.
- The reverse proxy begins forwarding only once a replica is genuinely answering — not merely spawned.
In production, point the deployment at your managed PostgreSQL (and Redis, if you cluster) and remove the bundled database services. Give the proxy your domain to get automatic HTTPS. The runtime container runs as a non-root user and keeps its runtime state on a declared volume.
Multi-node on Enterprise
Run the Enterprise kit on a single node today. The deploy bundle wires
no cluster driver, so keep OS_CLUSTER_REPLICAS at 1 and do not scale the
app service with docker compose up --scale.
Setting OS_CLUSTER_REPLICAS above 1 refuses the boot, by design. The
built-in in-process coordinator keeps locks, counters and pub/sub inside each
process, so a second replica would silently diverge from the first. Do not
silence the refusal with OS_ALLOW_MEMORY_CLUSTER_MULTINODE, and do not
bypass it with --scale: the compose template ties the replica count and the
OS_CLUSTER_REPLICAS the runtime reads to one variable so they cannot
disagree.
Multi-node topology is unlocked by the Enterprise license (the node cap is the
license's maxNodes). It is activated by contract, and a cluster driver is
connected to the bundle when a contract calls for it; no date is committed
here. High availability is not part of what the kit includes today. If you
need more than one node, contact sales.
Whatever the node count, two rules hold:
- Set one shared secret key (
OS_SECRET_KEY). Without it the runtime mints and persists its own key, which would diverge between replicas. - The auth secret must be identical wherever the runtime runs. Changing it invalidates every existing session and orphans the signing key; the environment template documents the one-time cleanup that goes with a rotation.
Upgrade by digest
The upgrade steps above hold, with the digest in place of the tag:
- Take the new digest from the release notes and read the image's release manifest before rolling it out: confirm the product major line is the one you are on, and check whether the embedded runtime's compatibility range moved. A runtime major-version move means breaking changes underneath — read that release's notes first.
- Back up the database.
- Change the image digest and restart the stack. The migration runs to completion before new replicas take traffic.
- Confirm readiness returns 200 and the replicas report healthy.
- To roll back, restore the previous digest and repeat step 3 — valid as long as the version you are leaving made no incompatible schema change (the release notes say when it did).
Next
- Kubernetes — the same image, on an orchestrator.
- Production Readiness — the checklist before go-live.
- Observability — logs, metrics, audit.
- Air-gapped — the Enterprise license mode for disconnected sites.
Deployment
Run ObjectOS on your own infrastructure — the open Community image or the licensed Enterprise image, one runtime, one configuration contract, and what the license adds.
Kubernetes
Run the ObjectOS image on Kubernetes — a reference Deployment, the right probe on the right endpoint, secrets from a Secret, and what must stay identical across replicas.