Skip to main content
You describe a system in one uniac.yaml, the CLI resolves it into a deployable and ships container images to a remote project, and the platform runs them. Three nouns carry the whole model.

Service

A reusable definition of one workload’s shape: where its container comes from, which environment variables, which start command. A service is never directly deployable — targeting one is an error. Its container comes from one of two sources: a prebuilt OCI image named by image:, or a Docker build of your own tree pointed at by build:. The manifest has the source rule and the rest of the service fields.
uniac plan names the source it resolved for each instance the deployment instantiates. build: takes the build root, or an object refining root, context, dockerfile, and target; the manifest has the field table. plan checks those paths against your tree and nothing else — a missing root, context, or Dockerfile fails offline, while the Dockerfile’s contents stay unread until deploy. The build then runs on your machine, against your local Docker daemon, targeting linux/amd64. What it produces is pushed exactly like a pulled image, so nothing downstream knows which source a service used. type: stateful is the same definition capped at a single running instance, for workloads like databases where two concurrent instances would corrupt state. That cap is the precondition for durable storage: only a stateful service may declare volumes:, and a type: service that tries fails with service "db" declares volumes but is not stateful — durable storage needs the single-writer guarantee (`type: stateful`). A volume declares a name, a size, and the path it mounts at; the manifest has the keys. A service declares no port. The project’s internal network is port-transparent: other services reach yours by name, on whatever port your process binds. Liveness is observed on the container process, never by probing a port.

Deployment

An instantiation: a map of instance names to the definitions they draw from, plus the public exposure claimed for each instance. Resolving a deployment yields the deployable, the thing the platform consumes. The instance name is the identity. It is the deployed service’s name, its internal hostname, its platform identity, the name every ${{...}} reference resolves against, and the prefix on any volume it mounts — a definition’s data volume mounted by the instance db is the disk db.data. The definition’s name is only a local label for from:.
uniac deploy rejects a deployment instantiating more than one service, so a multi-service system is several deployments in one manifest, deployed one at a time and wired by cross-deployment references.

Project

The remote destination, bound to your directory by uniac link (.uniac/deploy.json). A project is a flat list of running services; the platform holds no grouping above them. Deployments are a client-side composition resolved on the way in, so the platform never sees one — what a running service is serving is its own version number. Volumes are project entities of their own, not properties of a container: the whole-project uniac status lists each with its size and its own lifecycle state — including whether a service is holding it. What is running is not the manifest: deleting a resource from uniac.yaml does not remove the service, and the CLI has no removal command — retire services from the dashboard at uniac.ai, which also covers scaling replicas and deleting projects.

The loop

plan is the verification loop. It needs no network, no credentials, and no Docker daemon; it applies the manifest’s local rules — the ones the manifest page tabulates, some against the whole file and some, the on-disk existence of a build: source among them, only against the deployment it resolves — and previews the resolved deployable, naming it by a shortened digest; --json prints the artifact itself. That same page lists what waits for deploy — the rules plan passes and deploy applies. Run it after every manifest edit — a manifest that fails plan fails deploy with the same message, before anything is sent anywhere. Identical inputs yield an identical digest. The deployable is content-addressed: reindenting YAML or reordering resources leaves the digest unchanged, so a digest that moved means the declaration actually changed. It keys the declaration only — editing the code or the Dockerfile behind a build: leaves the digest identical, and that build runs on every deploy regardless.

Ownership

The container environment is exactly the resolved declared env — nothing is injected beyond it, and nothing of Uniac is added to your image: a build: service is your Dockerfile, built by your daemon, pushed as it came out.