App catalog JSON
The complete field reference for an Openship App definition — the JSON that turns a set of containers into a one-click install, with generated secrets, a curated settings form, and a connection card.
An App is a JSON file that wraps a deployment into a one-click, focused experience. Instead of creating a project, adding a service per container, hand-writing the images and the env that wire them together, generating and matching a database password across two services, and then hunting through raw env tabs to change a setting — you click the app, fill a short form, and deploy.
So an App is not a new runtime. It is metadata over the normal
multi-service deploy: a repo-less services project marked
isApp. Same engine underneath, much shorter path for the user. This page documents every field you
can author. To actually submit one, see Add an app.
Add the $schema line for editor autocomplete and inline validation:
{
"$schema": "https://openship.io/app.schema.json",
"id": "it-tools",
"name": "IT-Tools",
"description": "A handy collection of developer and sysadmin utilities.",
"kind": "template",
"logo": "it-tools",
"category": "other"
}It is JSON, not JSONC — no comments, no trailing commas.
Unknown keys are ignored, not rejected
The validator strips keys it does not recognize instead of failing. That is deliberate: it is what
lets a newer app definition stay installable on an older Openship, and it is why growth is always
additive. The trade-off is that a misspelled field name fails silently — it is dropped, not
reported, by both the published schema and the server. So keep the $schema line and let your editor
autocomplete field names rather than typing them from memory.
Two kinds
kind | What it does |
|---|---|
template | Instantiates the services defined in the entry (a backend plus its database, a CMS plus its database, …) and deploys them through the compose/services path. This is the common case. |
flow | Provisioning already has a bespoke wizard (for example the mail stack). The entry just points at that wizard via flowHref; it does not instantiate services. |
Top level
Prop
Type
Services
Each entry becomes one container in the created project.
Service name is the hostname
Services reach each other by name on the project network. A service named ghost-db is reachable
at the host ghost-db — which is why a database host in environment is just the plain service
name, never an IP or a localhost address.
Prop
Type
Building an image
Most services pull a prebuilt image. Some can't: the upstream ships a base image that still
needs extra packages and a provisioning entrypoint baked in — Neon's compute node is the canonical
case. For those, set build instead of image. It is an inline Docker build context that Openship
materializes at deploy time and builds on the deploy host through the normal compose build pipeline.
Building subsumes an entrypoint override: the Dockerfile sets its own ENTRYPOINT.
Set exactly one of image or build per service. Neither (nothing to run) and both (ambiguous)
are rejected — service "<name>" must set exactly one of image|build.
Prop
Type
COPY paths are prefixed by the service name
This is the rule authors get wrong first. Every buildable service is materialized under a subdir
named after the service, inside one shared build context, and docker build runs with that shared
root as its context. So COPY/ADD sources are relative to the root, not to the service's own subdir:
# service "compute", with files: [{ "path": "compute.sh", ... }]
COPY compute/compute.sh /shell/compute.sh # ✅ prefixed with the service name
COPY compute.sh /shell/compute.sh # ❌ COPY source not found → build failsA files[].path is relative to the service's own subdir; you COPY it as <service-name>/<path>.
Values and build args
{{config:KEY}} placeholders are resolved in dockerfile and in every files[].content at install
time, exactly as in environment values — so a generated secret or an install-step answer can flow
into the build.
There is no args map. A build ARG the Dockerfile declares is fed from the project's build env:
the runtime passes each env var as --build-arg. Declare the value as a normal environment entry (or
a configField) and reference it with ARG in the Dockerfile.
Building happens on the deploy host
A large base image builds where it deploys. On a small host a heavy build — Neon's compute node pulls a multi-hundred-MB base — can be slow or memory-bound. That cost is inherent to the app, not to Openship.
Two field systems
The single most common authoring mistake is mixing these up. There are two places to declare env, split by role:
configFields | settings with installStep: true | |
|---|---|---|
| For | Machine-generated or derived env | Human inputs the wizard renders |
| Rendered as a form input? | No — resolved server-side | Yes — text, select, number, … |
| Typical use | generate: "secret" / "jwt", generateGroup | a name, a toggle, a chosen option |
If you want the user to fill it in, it is an installStep setting — not a configField. Use
configFields only for values the operator never types.
configFields
Generated or derived env only. Each entry maps to one env key on one service.
Prop
Type
"configFields": [
{ "key": "MYSQL_ROOT_PASSWORD", "service": "ghost-db", "label": "Database password",
"generate": "secret", "generateGroup": "ghostdb", "secret": true },
{ "key": "database__connection__password", "service": "ghost", "label": "Ghost DB password",
"generate": "secret", "generateGroup": "ghostdb", "secret": true }
]Both fields share generateGroup: "ghostdb", so one generated password is written to both sides and
the two always match.
Settings
The curated settings form, grouped. A field with installStep: true is also collected in the
install wizard — this is the human-input surface. Everything else is day-2 configuration.
Validation is enforced live in the form and server-side on save.
Groups:
Prop
Type
Fields:
Prop
Type
Connection
The post-install Connection card: the URLs and keys a user needs, plus the handover into another project.
Prop
Type
Each output:
Prop
Type
Output sources
An output source (and each variant source) must match one of these three forms:
| Form | Resolves to |
|---|---|
env:<service>:<KEY> | That env value on that service |
publicUrl:<service> or publicUrl:<service>:<port> | The public URL for that service, optionally for a specific route |
template:… | A composed string, embedding {{env:svc:KEY}} placeholders |
Anything else fails validation, and the <service> part must name a declared service.
Prepare
Commands run inside a service container after it starts — never a host shell. Their stdout can be captured and persisted as an env var (this is how an app like Convex gets its admin key).
Must be re-run safe
A prepare step can run again on a later deploy. Write it idempotently, or gate it with once.
Prop
Type
phase: pre-deploy is reserved
The schema accepts "pre-deploy", but the engine does not run it yet. Do not rely on it. For
pre-run database initialization use files to write into
/docker-entrypoint-initdb.d, combined with dependsOn and a healthcheck.
Endpoints
What the install wizard asks you about — how to ship each port. Omit endpoints entirely and
Openship derives one http endpoint per exposed service.
Prop
Type
Provides and requires
The connection graph between projects. provides advertises a connectable bundle; requires
declares a connection this app needs from another project — the install wizard then offers a
same-org source picker and wires it in one shot. Same-org only, and always user-confirmed.
Prop
Type
Host requirements
minResources is what the app needs from the machine it lands on. Declare it only when the app
genuinely will not work below a floor — a nine-container analytics stack, a Postgres fork that
wants 8 GB. Most apps should declare nothing, and an app that declares nothing is never checked.
{
"minResources": { "memoryMb": 8192, "cpuCores": 4 }
}Prop
Type
The declaration is matched against what the destination's Docker daemon reports (NCPU,
MemTotal), in two places that share one comparison:
- The install wizard shows the floor next to the destination picker, and names the shortfall
before anything is created. You can query it directly — see
GET /apps/catalog/{id}/host-fit. - Deploy preflight enforces it: a first deploy onto a machine that is short is refused
with
HOST_RESOURCES_INSUFFICIENT.
Three rules keep the check from being a footgun of its own:
A shortfall never blocks a redeploy
Refusing a redeploy would strand an app already running on a box that turned out to be undersized — the operator's way out of that is a deploy, not a refusal. After the first successful deploy a shortfall is a warning only.
- An unmeasurable machine never fails. A box Openship could not probe means it did not look, not that the hardware is too small.
- Round numbers pass the box they name. A "16 GB" machine reports ~15.6 GB once firmware and the kernel take their cut, so a tenth under the declared figure still passes — declare the round number an operator recognises.
- Openship Cloud is skipped. A cloud workspace is sized from the tier table, not from host hardware.
Files
Generated config files bind-mounted into a container at deploy — for apps that need a config file,
not just env (an init .sql, for example). Supports placeholders.
Prop
Type
Self-hosted and desktop only
files are not applied on Openship Cloud.
Management
Overrides how the installed app is managed.
| Value | Effect |
|---|---|
{ "kind": "schema" } | Render the curated settings form from settings. |
{ "kind": "custom", "href": "/emails" } | Send the user to a bespoke surface instead. |
Omit it and Openship derives the behavior: schema when settings exist, otherwise the raw project
tabs.
Placeholders
Resolved at install. Usable in environment values, files[].content, and connection outputs:
| Placeholder | Resolves to |
|---|---|
{{publicUrl:<service>}} | That service public URL. Add :<port> for a specific route. |
{{config:<KEY>}} | A generated config value by key — for example a generate: "secret". |
Localized strings
Several display-only fields accept either a plain string or an inline per-locale map:
{ "title": { "en": "Creating the admin key", "ar": "إنشاء مفتاح المدير" } }This applies to prepare[].title, prepare[].description, requires[].label,
connection.firstLogin.username / password / note, connection.guide.intro / useHint,
connection.outputs[].sourceLabel, and variants[].label.
Validation
Beyond field types, these referential checks run at the gate — for both curated and custom apps — so a dangling reference fails immediately instead of at deploy time:
- Every
servicereference resolves to a declared service — inconfigFields,prepare,endpoints,files,settings[].fields, andconnection.outputs[].service. - Every
connection.outputs[].sourceandvariants[].sourcematchesenv:<service>:<KEY>,publicUrl:<service>[:<port>], ortemplate:…. - Every
provides[].outputRefsentry names a realconnection.outputs[].id. - Every
jwtSecretGroupmatches a declaredgenerateGroup. flowHrefstarts with/— it can never be an external URL.categoryis one of the seven allowed values.
Stability and versioning
The catalog JSON is a stable, versioned public API. Growth is additive and backward-compatible: new optional fields and new enum members default to prior behavior, and a field is never repurposed or removed within a schema version.
Prop
Type
One file per app — never author multiple versions. The repo always holds the single latest file.
An older instance simply keeps the copy bundled in its own build. When an instance is older than an
app minEngine:
- the app is bundled there → it keeps serving the bundled copy, and may note that an update is available;
- the app is brand new → the catalog shows a guided Requires Openship ≥ X card and install is refused, client and server. Never a silent disappearance.
See also
- Add an app — how to submit one, and how to upload a custom app
- Apps API — the REST surface
- openship.json — the per-repo deploy config
openship.json
The declarative deploy config for Openship — like vercel.json or railway.toml. Declare framework, build, runtime, env, domains, routes, resources, services and monorepo layout, and Openship deploys the same way every time.
The dashboard
A tour of the Openship dashboard — the sidebar that navigates everything, the home overview, and a map to each area.