Recetas y apps
Recipes
What a recipe is, and the setup.json reference with every field, its type and default, and what a recipe deliberately cannot do.
Aún sin traducir. Por ahora esta página solo está en inglés.
En esta página
A recipe is a short file that tells Setup how to run a piece of software: which programs to start, which settings you fill in, where the data lives and how Setup can tell that the software works. When you paste a link, Setup writes the recipe for you and shows it to you before anything starts. You never have to write one yourself.
The file is called setup.json. It is plain JSON, so you can read it, change it and share it. It can only describe things that are safe to run in a sandbox. There is no field that gives the software access to the computer it runs on, and that is on purpose.
The rest of this page is the reference for developers and maintainers.
Where recipes come from
- From an analysis. When you paste a link, Setup looks for a
setup.jsonin the repository first, then for a curated recipe in its catalog, then reads Compose files, Dockerfiles and common project layouts. If none of that gives a confident answer and a model is configured, the model drafts a recipe. Setup reads the project as data and runs none of its code during this step. - Pasted by you. The box on Home takes a link, a file or a description. Paste a whole
setup.jsonto run it as written. Paste a Docker Compose file (with its.env),docker runcommands, an image name, a Dockerfile, Kubernetes manifests, or a CasaOS, Umbrel, Runtipi, Cosmos or Portainer definition, and Setup translates it into a recipe. Getting started lists every kind with an example. - Edited by you. Before you deploy, the Recipe JSON tab shows the recipe and lets you change it. Catalog recipes are read-only there. Paste an edited copy on Home to make it yours.
- From a maintainer. Projects can keep a
setup.jsonin their repository. See Maintainers.
A complete example
This is the recipe for Umami, a web analytics app with a PostgreSQL database. It passes the same schema and policy checks Setup runs on every recipe.
{
"version": 1,
"slug": "umami",
"name": "Umami",
"description": "Cookie-free website analytics with a simple dashboard.",
"category": "analytics",
"tags": ["analytics", "privacy", "web"],
"homepage": "https://umami.is",
"license": "MIT",
"source": {
"kind": "github",
"url": "https://github.com/umami-software/umami"
},
"inputs": [
{
"key": "DB_PASSWORD",
"label": "Database password",
"type": "secret",
"generate": "password"
},
{
"key": "APP_SECRET",
"label": "App secret",
"type": "secret",
"generate": "hex32"
}
],
"services": {
"db": {
"image": "postgres:17.11-alpine",
"env": {
"POSTGRES_DB": "umami",
"POSTGRES_USER": "umami",
"POSTGRES_PASSWORD": "${inputs.DB_PASSWORD}"
},
"volumes": [{ "name": "postgres", "path": "/var/lib/postgresql/data" }],
"healthcheck": {
"command": [
"pg_isready",
"-h",
"127.0.0.1",
"-U",
"umami",
"-d",
"umami"
]
},
"resources": { "cpus": 0.5, "memoryMb": 512 }
},
"app": {
"image": "ghcr.io/umami-software/umami:3.4.0",
"env": {
"DATABASE_URL": "postgresql://umami:${inputs.DB_PASSWORD}@${services.db.host}:5432/umami",
"APP_SECRET": "${inputs.APP_SECRET}"
},
"ports": [{ "container": 3000 }],
"dependsOn": [{ "service": "db", "condition": "healthy" }],
"resources": { "memoryMb": 512 }
}
},
"expose": { "service": "app", "port": 3000 },
"checks": {
"http": { "path": "/api/heartbeat" },
"startupTimeoutSeconds": 180
},
"notes": "Sign in with username `admin` and password `umami`, then change the password under **Settings → Profile**."
}Both passwords are generated when the person leaves them empty. The database keeps its files in a named volume, and the app waits until the database answers its health check. Only port 3000 of app is reachable from outside.
Top-level fields
| Field | Type | Default | Description |
|---|---|---|---|
$schema | string | none | Ignored by Setup. Lets an editor that knows JSON Schema offer completion. |
version | 1 | required | Format version. Always 1. |
slug | string | required | 1 to 63 characters: lowercase letters, digits and dashes, starting with a letter or digit. Example: uptime-kuma. |
name | string | required | Display name, up to 80 characters. |
description | string | none | Up to 500 characters. |
category | string | app | One of app, ai-model, ai-app, homelab, dev-tool, database, media, productivity, analytics, security, other. |
tags | string[] | [] | Up to 12 slugs. |
icon | URL | none | Shown in the catalog. |
homepage | URL | none | Shown in the catalog. |
license | string | none | Up to 64 characters, for example MIT. |
source | object | required | Where the software comes from. |
inputs | object[] | [] | Settings the person fills in or Setup generates. Up to 32. |
services | object | required | 1 to 8 containers, keyed by service name. |
expose | object | none | The web address Setup opens. Leave it out for software without a web interface. |
checks | object | { "startupTimeoutSeconds": 300 } | How Setup decides that the software works. |
requires | object[] | [] | Outside services and accounts the app needs. Up to 16. |
notes | string | none | Markdown shown after a successful start, such as the first login. Up to 8,000 characters. |
Source
"source": {
"kind": "github",
"url": "https://github.com/acme/widget",
"git": { "url": "https://github.com/acme/widget.git", "ref": "v1.4.2", "subdir": "server" }
}| Field | Type | Default | Description |
|---|---|---|---|
kind | string | required | github, gitlab, git, huggingface-model, huggingface-space, docs, image or description. |
url | URL | none | The page a person would open. |
git.url | HTTPS URL | required in git | Clone URL. The builder fetches it inside its own sandbox, never on the runner host. |
git.ref | string | remote HEAD | Branch, tag or full commit SHA. |
git.subdir | relative path | none | Directory inside the repository. |
git is required when a service builds from source ("build": { "context": "source" }).
Settings (inputs)
Inputs are the values a person provides when they start the app, such as an admin email address or an API key. The dashboard shows them under Settings.
{
"key": "ADMIN_EMAIL",
"label": "Admin email",
"type": "email",
"required": true
}| Field | Type | Default | Description |
|---|---|---|---|
key | string | required | UPPER_SNAKE_CASE, starting with a letter, up to 64 characters. Unique within the recipe. |
label | string | required | Up to 80 characters. |
description | string | none | Help text, up to 500 characters. |
type | string | text | text, secret, number, boolean, select, url or email. |
required | boolean | false | The app cannot start without a value. |
default | string | none | Up to 2,000 characters. Always a string, also for number and boolean. |
options | string[] | none | The choices of a select, up to 50. Required for select. |
generate | string | none | password, hex32, base64-32 or uuid. Used when the field is left empty. |
length | number | none | 8 to 128: the size of a generated value, for apps that check it (below). |
Setup picks each value in this order: what the person entered, then default, then the generator. A required input that is still empty stops the deploy. An optional one becomes an empty string.
Then it checks the value against its type. A number must parse as a number, a boolean must be true or false, a select must be one of its options, a url must parse as a URL and an email must look like name@example.com.
| Generator | Produces |
|---|---|
password | 24 letters and digits, without look-alike characters such as 0, O, 1, l and I. |
hex32 | 32 random bytes as 64 hex characters. |
base64-32 | 32 random bytes, base64-encoded. |
uuid | A random UUID. |
length changes the size: characters for password, random bytes for hex32 and base64-32, the number openssl rand -hex or -base64 takes. An app that wants a 32-character hex key (openssl rand -hex 16) gets "generate": "hex32", "length": 16. uuid takes no length.
Generated values are stored with the app, so they stay the same across restarts and updates. Values of secret inputs are encrypted at rest.
References in values
Values can point to inputs, sibling services and the app's own address.
| Reference | Becomes |
|---|---|
${inputs.KEY} | The value of input KEY. |
${services.NAME.host} | The host name of service NAME on the app's private network. |
${deployment.url} | The public origin of the exposed service (scheme://host:port), without a path. |
${deployment.host} | The public host, without scheme or port. |
${deployment.port} | The public port. |
${deployment.id} | The deployment's id. |
References work in env values, command, entrypoint and build.args. Health check commands are passed on as written. $${ writes a literal ${. A reference to an input or service that does not exist is an error.
Services
services maps a service name to one container. Names start with a lowercase letter and use a-z, 0-9 and dashes, up to 31 characters.
| Field | Type | Default | Description |
|---|---|---|---|
image | string | none | Registry reference, for example postgres:17.11-alpine. |
build | object | none | Build an image instead of pulling one. Each service needs exactly one of image and build. |
entrypoint | string[] | image default | Exec form, up to 64 parts. |
command | string[] | image default | Exec form, up to 64 parts. |
workingDir | absolute path | image default | Working directory in the container. |
user | string | image default | uid, uid:gid or a user name that exists in the image. |
env | object | {} | Environment variables, up to 128. Values up to 8,000 characters. |
ports | object[] | [] | Up to 8 per service. |
volumes | object[] | [] | Up to 8 per service. |
tmpfs | absolute path[] | [] | Up to 8 in-memory mounts. |
readOnlyRootFs | boolean | false | Mount the root filesystem read-only. |
shmSizeMb | integer | runtime default | Size of /dev/shm, 1 to 8,192 MB. |
healthcheck | object | none | A command that tells whether the service is ready. |
dependsOn | object[] | [] | Services to wait for before this one starts. |
egress | string | none | none or public. |
resources | object | 1 CPU, 1,024 MB | CPU, memory and GPU. |
platform | string | none | linux/amd64 or linux/arm64. |
oneshot | boolean | false | Runs to completion instead of staying up. |
Images and builds
Pin images to a version or a digest. A tag of latest, main, master, nightly, edge or dev, or no tag at all, produces a warning, because an update could change what runs. Image references must follow the registry grammar: no spaces, quotes or shell characters.
"build": {
"context": "source",
"dockerfile": "docker/Dockerfile",
"target": "production",
"args": { "NODE_ENV": "production" }
}| Field | Type | Default | Description |
|---|---|---|---|
context | "source", "empty" or HTTPS git URL | "source" | source builds from source.git. empty builds a dockerfileInline that copies no files, such as a pasted Dockerfile. |
dockerfile | relative path | none | Path of the Dockerfile in the context. |
dockerfileInline | string | none | A whole Dockerfile, up to 32,000 characters, for projects that have none. Use either this or dockerfile. |
target | string | none | Build stage. |
args | object | none | Build arguments. Values may use references. |
Builds run inside BuildKit on the runner. The control plane never clones or builds anything.
Ports
"ports": [{ "container": 3000 }]| Field | Type | Default | Description |
|---|---|---|---|
container | integer | required | Port inside the container, 1 to 65535. |
protocol | string | http | http, tcp or udp. |
name | slug | none | A label for the port. |
publish | boolean | false | Publish a raw tcp or udp port on the runner host. |
hostPort | integer | runner picks | The host side of a published port, such as 53. Only together with publish. |
Only the HTTP port named in expose is reachable from outside, through Setup's ingress proxy. Other ports are reachable by sibling services on the app's private network.
Publishing is for home-lab software that speaks something other than HTTP, such as DNS or game servers. A runner refuses it unless its operator turned on --allow-published-ports (see Runner). HTTP ports cannot be published. They always go through the ingress.
Volumes
"volumes": [{ "name": "data", "path": "/app/data" }]name is a slug and path is an absolute path in the container without ... Volumes are named, belong to one app, survive restarts and updates and are included in backups. A folder on the runner host cannot be mounted.
Health checks
"healthcheck": {
"command": ["curl", "-f", "http://127.0.0.1:3000/health"],
"intervalSeconds": 10,
"retries": 5
}| Field | Type | Default | Description |
|---|---|---|---|
command | string[] | required | Exec form, 1 to 32 parts, runs inside the container. |
intervalSeconds | integer | 10 | 1 to 300. |
timeoutSeconds | integer | 5 | 1 to 120. |
retries | integer | 5 | 1 to 30. |
startPeriodSeconds | integer | 30 | 0 to 1,800. Failures during this time don't count. |
The command must exist in the image. Catalog recipes call 127.0.0.1 rather than localhost. When an image has its own HEALTHCHECK, the runner waits for that too.
Start order and one-shot services
"dependsOn": [{ "service": "db", "condition": "healthy" }]condition is started (the default), healthy or completed. healthy needs a healthcheck on the other service. completed waits for a one-shot service to finish, which suits database migrations or downloads that must happen first. Cycles are an error.
A service with "oneshot": true runs to completion. It cannot be exposed and does not count toward the app's CPU and memory total.
Internet access
By default ("egress": "none") a service has no route to the internet or to the local network. It reaches only its sibling services.
With "egress": "public" the service reaches the internet through the app's egress proxy, which refuses private, loopback, link-local and cloud metadata addresses. The runner sets HTTP_PROXY and HTTPS_PROXY for it. Programs that ignore those variables cannot download anything. The recipe check warns about every service with public egress.
Resources
| Field | Type | Default | Description |
|---|---|---|---|
cpus | number | 1 | 0.1 to 16. |
memoryMb | integer | 1024 | 64 to 65,536. |
gpu | boolean | none | The service needs a GPU. Only runners that report one can run it. |
Setup adds up the long-running services and compares the total with the limits of your plan or runner before it deploys. If a service runs out of memory, the runner says so and suggests raising memoryMb.
Note. Setup does not host language models. An app that needs a model should take the address and key of an OpenAI-compatible API as settings.
Expose
"expose": { "service": "app", "port": 3000, "path": "/login" }| Field | Type | Default | Description |
|---|---|---|---|
service | string | required | A service that exists and is not one-shot. |
port | integer | required | Must be declared as an http port of that service. |
path | string | / | Opened in the browser after start. |
Checks
"checks": {
"http": { "path": "/health", "expectStatus": [200], "contains": "ok" },
"startupTimeoutSeconds": 300
}| Field | Type | Default | Description |
|---|---|---|---|
http.path | string | / | Requested through the app's public address. |
http.expectStatus | integer[] | any 2xx or 3xx | Accepted status codes, up to 20. |
http.contains | string | none | Text the response body must contain, up to 200 characters. |
startupTimeoutSeconds | integer | 300 | 10 to 1,800. |
A run passes when every long-running service is up, every declared health check is healthy and the HTTP check succeeds before the timeout. Without checks.http, the runner requests expose.path with the default status rule. Redirects are not followed, so a redirect to a login page counts as a pass unless you set expectStatus. checks.http needs expose.
A passing run verifies the recipe. See Maintainers for what that changes.
Outside services (requires)
Some apps need something that cannot run in the sandbox: an email provider, a hosted database, a payment account, their own OAuth client. requires names these, so the person starting the app sees them before anything runs.
"requires": [
{
"id": "email",
"kind": "api-credentials",
"provider": "resend",
"capability": "email-sending",
"title": "Resend for email",
"reason": "The app sends sign-in links and notifications by email.",
"inputs": ["RESEND_API_KEY"],
"docsUrl": "https://resend.com/docs"
}
]| Field | Type | Default | Description |
|---|---|---|---|
id | slug | required | Unique within the recipe. |
kind | string | required | managed-service, api-credentials, oauth-app or deploy-target. |
provider | slug | required | For example resend, cloudflare, convex, stripe. |
capability | slug | none | What the app needs, such as email-sending, database, object-storage, backend, oauth-client, payments. |
title | string | required | Plain-language name, up to 80 characters. |
reason | string | required | Why the app needs it, up to 500 characters. |
optional | boolean | false | The app runs without it, minus a feature. |
inputs | string[] | [] | Input keys this requirement fills, up to 16. Each must be declared in inputs, and only one requirement may fill it. |
resources | object[] | provider default | What Setup creates per app, such as { "type": "d1-database", "name": "db" }. Up to 8. |
outputs | object | none | Which created value fills which input. Keys must be listed in inputs. |
docsUrl | URL | none | Where to sign up or create the credentials. |
selfHosted | object | none | catalog (slug of a replacement that runs in the sandbox) and note (up to 300 characters). |
The input itself is an ordinary input, and a service uses it through a reference:
"inputs": [
{
"key": "RESEND_API_KEY",
"label": "Resend API key",
"description": "Filled when you connect Resend, or enter it yourself.",
"type": "secret"
}
]When someone deploys, the dashboard lists each requirement under Outside services and offers Create it for me, Use my own values, Set it up by hand and, for optional ones, Run without it. With Create it for me, Setup creates resources for this one app in a connected account and removes them when the app is removed. The code that creates them comes from Setup's own reviewed registry, never from the analyzed project. See Integrations and connected accounts for which providers are supported.
resources and outputs matter only when Setup creates something. Output names follow the provider's module, with dashes. Inputs not listed in outputs are matched by their well-known names, such as RESEND_API_KEY.
{
"id": "database",
"kind": "managed-service",
"provider": "cloudflare",
"capability": "database",
"title": "Cloudflare D1 database",
"reason": "The app keeps its data in a D1 database.",
"inputs": ["D1_DATABASE_ID", "CLOUDFLARE_API_TOKEN"],
"resources": [{ "type": "d1-database", "name": "db" }],
"outputs": {
"D1_DATABASE_ID": "db.database-id",
"CLOUDFLARE_API_TOKEN": "api-token"
}
}Limits
| What | Limit |
|---|---|
| Services per recipe | 8 |
| Inputs per recipe | 32 |
| Requirements per recipe | 16 |
| Tags | 12 |
| Environment variables per service | 128 |
| Ports per service | 8 |
| Volumes per service | 8 |
| CPUs per service | 16 |
| Memory per service | 65,536 MB |
| Inline Dockerfile | 32,000 characters |
| Notes | 8,000 characters |
| Startup timeout | 1,800 seconds |
Your plan or runner may set lower totals for a whole app.
Errors and warnings
Setup checks every recipe twice: against the schema, then against rules the schema cannot express. Errors block the deploy. Warnings are shown and don't block.
Errors include duplicate input keys, a select without options, unknown references, a service that depends on itself, a healthy dependency on a service without a health check, dependency cycles, building from source without source.git, duplicate ports or volume paths, hostPort without publish, a published HTTP port, an expose that points to a missing service or undeclared port, and checks.http without expose.
Warnings cover unpinned image tags, published ports, public egress and every requirement that is not optional.
What a recipe cannot express
The format has no field for:
- folders on the runner host or bind mounts,
privilegedmode or Linux capabilities,- devices, apart from the
gpuflag, - host networking or the host's PID and IPC namespaces,
- sysctls, kernel tuning or security options,
- the Docker socket.
Unknown keys are dropped when Setup reads a recipe. Writing "privileged": true into a service doesn't fail, it simply has no effect. Container paths must be absolute and build paths relative, and neither may contain ...
The reason is that Setup runs software that strangers wrote, often on a computer shared with other apps. Each of these options would let a container reach the host or its neighbors, and a recipe that cannot say them cannot be talked into them. Runners add their own hardening on top: dropped capabilities, a private network per app, CPU, memory and process limits. The only opt-in is publish for raw ports, and only on runners whose operator allows it. Read Security for the whole model.