The template file
Every block a template may contain, and the defaults for the ones it leaves out
A template is one YAML file, template.yaml inside its directory in the shared
repository — see Publishing. This is the
worked example the validator's own tests run against — Umami, one app and one
database:
version: 1
name: Umami
minCubeship: "0.6.0"
project: umami # suggested slug; the installer may change it
inputs: # what the template cannot know
- key: domain
type: domain
label: Where the dashboard answers
- key: appSecret
type: secret
label: The app's session secret
generate: 32 # the instance generates it and shows it once
databases:
- key: db # template-local; what references name
name: umami-db # suggested instance name, renamable at install
engine: postgres
version: "18"
database: umami # the password is generated, never written here
apps:
- key: web
name: web
image: ghcr.io/umami-software/umami
tag: postgresql-v2
port: 3000
health: /api/heartbeat
domains:
- host: ${input.domain}
attach:
- database: db # contributes DATABASE_URL and friends
prefix: ""
limits: { cpu: 1, memory: 1Gi }
scale: 1
env:
APP_SECRET: ${input.appSecret}
DATABASE_TYPE: postgresqlEvery key not shown below is optional and takes the default listed.
Unknown keys are refused — see schema.unknown-key.
Top level
| Key | Default | Meaning |
|---|---|---|
version | — | Always 1. This is the file format, not a template release. |
name | — | Display name in the catalog. Required in templates/. |
minCubeship | none | A semver range the installing instance must satisfy, e.g. ">=0.6.0". |
project | — | The suggested project slug. The installer may rename it. |
environment | production | The suggested environment inside that project. |
inputs | [] | What the installer is asked. See Inputs. |
databases | [] | The managed databases this template needs. |
stores | [] | The managed object stores this template needs. |
apps | — | At least one. What actually runs. |
Keys and names are two different things
Every input, database, store and app has a key — lowercase or mixed
case, letters/digits/dash/underscore, starting with a letter — which
never leaves the file. It is what a reference like ${db.db.host}
names, and what attach.database or attach.store names, and it is
how one part of the template points at another. See
References.
A database, store or app also has a name, which defaults to its
key and is what appears on the instance. The installer may change any
name, and for databases and stores it sometimes has to: their names are
unique across the whole instance, so two installs of one template would
collide on the first one otherwise.
Databases
| Key | Default | Meaning |
|---|---|---|
key | — | Required. What attach and ${db.<key>.…} name. |
name | the key | The suggested instance name. |
engine | — | Required. One of postgres, mysql, mariadb, redis, mongodb. |
version | the newest offered | Pinning it is advised but not required. |
extensions | none | Postgres only. A list of extension names the database is created with. |
username | the engine's default | Redis is always default; MySQL and MariaDB refuse root. |
database | none | The database name inside the engine. Ignored on engines with none, like Redis. |
expose | not exposed | null (the default) keeps it internal only; 0 asks for a port picked for you; a number asks for that port. |
limits | none | See Limits. |
There is no password field. A database's password is generated by
the instance and handed to an app through
an attachment — a template that could write one down
would be a template that could steal one.
Extensions
A Postgres database can be created with extensions:
databases:
- key: db
engine: postgres
version: "16"
extensions:
- pgvector
- vectorchordNames, not packages. Cubeship accepts only the ones it has reviewed, and each one picks a fixed statement — and, for two of them, a specific image pinned by digest. Nothing in a template reaches a Dockerfile, a package manager or a SQL string.
The name you write is Cubeship's, and it is not always the name the
extension has in SQL: pgvector creates the extension vector, and
vectorchord creates vchord. Your migrations use the SQL name.
The list is normalized: duplicates go, it is sorted, and what an
extension requires is added — vectorchord brings pgvector,
earthdistance brings cube. So [vectorchord] and
[pgvector, vectorchord] are the same template.
Most of them are PostgreSQL's own contrib modules, which every image
Cubeship runs already carries: pg_trgm, pgcrypto, hstore,
citext, ltree, unaccent, uuid-ossp, btree_gin, btree_gist,
bloom, cube, earthdistance, fuzzystrmatch, intarray, isn,
seg, tablefunc, tsm_system_rows, dblink, postgres_fdw,
pg_prewarm, pgstattuple, pg_buffercache and pg_stat_statements.
Two are not — pgvector and vectorchord — and those run an image of
their own. GET /datastores/engines on an instance is the list it will
actually accept, per version.
A template with extensions needs a minCubeship that excludes releases
before them. An older instance installs the template happily and creates
the database without them, which is an app that comes up and fails on
its first query.
A later release of a template that changes a database's extensions does not change a database that already exists — the preview says so, the same way it does for an engine or a version. Extensions can be installed on the database itself afterwards, from its own page.
Stores
| Key | Default | Meaning |
|---|---|---|
key | — | Required. What attach names. |
name | the key | The suggested instance name. |
version | none | The SeaweedFS version, unvalidated here — the instance decides what a missing one means. |
buckets | [] | The buckets to create inside it. |
limits | none | See Limits. |
A template cannot declare a linked S3 endpoint — that would be
somebody's credential. A template that wants one asks for an
input of type store instead, and the
installer picks a store that already exists on the instance.
Apps
Source
An app is exactly one of an image or a build — never neither, never both:
| Key | Default | Meaning |
|---|---|---|
image | none | A public image, with no tag in the string — tag is its own field. |
tag | none | The tag for image. Refused on a build: a built app is tagged by its deploy, not by you. |
repo | none | An http, https or git URL, with the branch in ref, never after a #. |
ref | the repo's default branch | The branch or tag to build. |
build | none | dockerfile or railpack. Required once repo is set. |
dockerfile | Dockerfile | Only meaningful with build: dockerfile. |
Network
| Key | Default | Meaning |
|---|---|---|
port | none | The port the app listens on. Also the default for a domain that names none. |
health | off | A path starting with /, no ? or #, at most 255 characters. Strongly advised — see advice.no-health. |
domains | [] | See below. |
Each entry under domains is { host, port }. host must come from
exactly one input of type domain —
${input.<key>} and nothing else, never a literal hostname. port
defaults to the app's own port, and beneath that to 8080.
Attach
Each entry under attach wires one database or one store into the
app's environment — exactly one of database or store per entry,
naming its key:
| Key | Default | Meaning |
|---|---|---|
database | — | A database's key, exclusive with store. |
store | — | A store's key, or the key of an input of type store, exclusive with database. |
bucket | — | Required with store: which bucket the app gets. |
prefix | "" | Upper case, ending in _, like ANALYTICS_. Distinguishes two attachments that would otherwise write the same variable names. |
A database attachment contributes <PREFIX><STEM>_URL, _HOST,
_PORT, _USER, _PASSWORD, and _NAME unless the engine has no
named databases — STEM is DATABASE for postgres, mysql and
mariadb, REDIS for redis, MONGO for mongodb. A store attachment
contributes <PREFIX>S3_ENDPOINT, _REGION, _BUCKET,
_ACCESS_KEY_ID, _SECRET_ACCESS_KEY, _PATH_STYLE.
Environment
env is a map of names to values, merged into the app on top of
whatever attach already wrote — an app's own env wins on a
collision, which is how you point at a different target without
detaching anything. A value may itself be a
reference, like ${input.appSecret}
above.
Limits and scaling
| Key | Default | Meaning |
|---|---|---|
limits.cpu | none | Cores, at least 0.01. |
limits.memory | none | A size — 512Mi, 2Gi, 1500M — at least 6Mi. |
scale | none | How many copies, at least 1. |
spread | false | Spread copies across machines rather than pack them onto one. |
autoscale.min | 1 | The floor, at least 1 and at most max. |
autoscale.max | — | Required with autoscale. At most 100. |
autoscale.cpu | — | Required with autoscale. A target above 0, where 100 means one whole core. |
limits and stores/databases' own limits share the same shape.
Volumes
apps:
- key: broker
image: rabbitmq
volumes:
- path: /var/lib/rabbitmq| Key | Default | Meaning |
|---|---|---|
volumes[].path | — | An absolute path inside the container whose contents survive deploys and restarts. Not /, not under /proc, /sys or /dev. |
A volume is for software that keeps its state in files — a queue, a
search index. An app with a volume runs as one copy on one machine,
so scale, spread and autoscale are refused beside it, and each
deploy stops the old container before starting the new one: the app is
briefly unavailable. A template with a volume needs minCubeship: "0.7.0"
or later, so an older instance refuses it instead of installing it
without its data surviving. Uninstalling keeps volume data unless
whoever uninstalls says otherwise.
TCP ports
inputs:
- key: sshPort
type: number
label: The port SSH answers on
default: 2222
min: 1024
max: 65535
apps:
- key: git
image: gitea/gitea
tcp:
- port: 22
host: ${input.sshPort}| Key | Default | Meaning |
|---|---|---|
tcp[].port | — | The port the app listens on inside its container, 1-65535. |
tcp[].host | picked | Where the instance publishes it: 1024-65535, or ${input.<key>} naming a number input. Left out, the instance picks a free one from 17000-17999. |
A TCP port is for a protocol that is not HTTP — SSH into a Git server, a
game server, a broker. It is published on the instance's own address with
no TLS and no proxy in front of it, so the app's own authentication is
what protects it. An app with a TCP port runs as one copy, so scale,
spread and autoscale are refused beside it, and each deploy stops the old
container before starting the new one.
Name the host port through an input rather than as a number: a port another
installation already holds is refused, and the person installing is the one
who knows which is free. A template with a TCP port needs
minCubeship: "0.7.2" or later.