Saltar al contenido

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
  1. Where recipes come from
  2. A complete example
  3. Top-level fields
  4. Source
  5. Settings (inputs)
  6. References in values
  7. Services
  8. Images and builds
  9. Ports
  10. Volumes
  11. Health checks
  12. Start order and one-shot services
  13. Internet access
  14. Resources
  15. Expose
  16. Checks
  17. Outside services (requires)
  18. Limits
  19. Errors and warnings
  20. What a recipe cannot express

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.json in 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.json to run it as written. Paste a Docker Compose file (with its .env), docker run commands, 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.json in 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.

json
{
  "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

FieldTypeDefaultDescription
$schemastringnoneIgnored by Setup. Lets an editor that knows JSON Schema offer completion.
version1requiredFormat version. Always 1.
slugstringrequired1 to 63 characters: lowercase letters, digits and dashes, starting with a letter or digit. Example: uptime-kuma.
namestringrequiredDisplay name, up to 80 characters.
descriptionstringnoneUp to 500 characters.
categorystringappOne of app, ai-model, ai-app, homelab, dev-tool, database, media, productivity, analytics, security, other.
tagsstring[][]Up to 12 slugs.
iconURLnoneShown in the catalog.
homepageURLnoneShown in the catalog.
licensestringnoneUp to 64 characters, for example MIT.
sourceobjectrequiredWhere the software comes from.
inputsobject[][]Settings the person fills in or Setup generates. Up to 32.
servicesobjectrequired1 to 8 containers, keyed by service name.
exposeobjectnoneThe web address Setup opens. Leave it out for software without a web interface.
checksobject{ "startupTimeoutSeconds": 300 }How Setup decides that the software works.
requiresobject[][]Outside services and accounts the app needs. Up to 16.
notesstringnoneMarkdown shown after a successful start, such as the first login. Up to 8,000 characters.

Source

json
"source": {
  "kind": "github",
  "url": "https://github.com/acme/widget",
  "git": { "url": "https://github.com/acme/widget.git", "ref": "v1.4.2", "subdir": "server" }
}
FieldTypeDefaultDescription
kindstringrequiredgithub, gitlab, git, huggingface-model, huggingface-space, docs, image or description.
urlURLnoneThe page a person would open.
git.urlHTTPS URLrequired in gitClone URL. The builder fetches it inside its own sandbox, never on the runner host.
git.refstringremote HEADBranch, tag or full commit SHA.
git.subdirrelative pathnoneDirectory 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.

json
{
  "key": "ADMIN_EMAIL",
  "label": "Admin email",
  "type": "email",
  "required": true
}
FieldTypeDefaultDescription
keystringrequiredUPPER_SNAKE_CASE, starting with a letter, up to 64 characters. Unique within the recipe.
labelstringrequiredUp to 80 characters.
descriptionstringnoneHelp text, up to 500 characters.
typestringtexttext, secret, number, boolean, select, url or email.
requiredbooleanfalseThe app cannot start without a value.
defaultstringnoneUp to 2,000 characters. Always a string, also for number and boolean.
optionsstring[]noneThe choices of a select, up to 50. Required for select.
generatestringnonepassword, hex32, base64-32 or uuid. Used when the field is left empty.
lengthnumbernone8 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.

GeneratorProduces
password24 letters and digits, without look-alike characters such as 0, O, 1, l and I.
hex3232 random bytes as 64 hex characters.
base64-3232 random bytes, base64-encoded.
uuidA 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.

ReferenceBecomes
${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.

FieldTypeDefaultDescription
imagestringnoneRegistry reference, for example postgres:17.11-alpine.
buildobjectnoneBuild an image instead of pulling one. Each service needs exactly one of image and build.
entrypointstring[]image defaultExec form, up to 64 parts.
commandstring[]image defaultExec form, up to 64 parts.
workingDirabsolute pathimage defaultWorking directory in the container.
userstringimage defaultuid, uid:gid or a user name that exists in the image.
envobject{}Environment variables, up to 128. Values up to 8,000 characters.
portsobject[][]Up to 8 per service.
volumesobject[][]Up to 8 per service.
tmpfsabsolute path[][]Up to 8 in-memory mounts.
readOnlyRootFsbooleanfalseMount the root filesystem read-only.
shmSizeMbintegerruntime defaultSize of /dev/shm, 1 to 8,192 MB.
healthcheckobjectnoneA command that tells whether the service is ready.
dependsOnobject[][]Services to wait for before this one starts.
egressstringnonenone or public.
resourcesobject1 CPU, 1,024 MBCPU, memory and GPU.
platformstringnonelinux/amd64 or linux/arm64.
oneshotbooleanfalseRuns 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.

json
"build": {
  "context": "source",
  "dockerfile": "docker/Dockerfile",
  "target": "production",
  "args": { "NODE_ENV": "production" }
}
FieldTypeDefaultDescription
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.
dockerfilerelative pathnonePath of the Dockerfile in the context.
dockerfileInlinestringnoneA whole Dockerfile, up to 32,000 characters, for projects that have none. Use either this or dockerfile.
targetstringnoneBuild stage.
argsobjectnoneBuild arguments. Values may use references.

Builds run inside BuildKit on the runner. The control plane never clones or builds anything.

Ports

json
"ports": [{ "container": 3000 }]
FieldTypeDefaultDescription
containerintegerrequiredPort inside the container, 1 to 65535.
protocolstringhttphttp, tcp or udp.
nameslugnoneA label for the port.
publishbooleanfalsePublish a raw tcp or udp port on the runner host.
hostPortintegerrunner picksThe 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

json
"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

json
"healthcheck": {
  "command": ["curl", "-f", "http://127.0.0.1:3000/health"],
  "intervalSeconds": 10,
  "retries": 5
}
FieldTypeDefaultDescription
commandstring[]requiredExec form, 1 to 32 parts, runs inside the container.
intervalSecondsinteger101 to 300.
timeoutSecondsinteger51 to 120.
retriesinteger51 to 30.
startPeriodSecondsinteger300 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

json
"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

FieldTypeDefaultDescription
cpusnumber10.1 to 16.
memoryMbinteger102464 to 65,536.
gpubooleannoneThe 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

json
"expose": { "service": "app", "port": 3000, "path": "/login" }
FieldTypeDefaultDescription
servicestringrequiredA service that exists and is not one-shot.
portintegerrequiredMust be declared as an http port of that service.
pathstring/Opened in the browser after start.

Checks

json
"checks": {
  "http": { "path": "/health", "expectStatus": [200], "contains": "ok" },
  "startupTimeoutSeconds": 300
}
FieldTypeDefaultDescription
http.pathstring/Requested through the app's public address.
http.expectStatusinteger[]any 2xx or 3xxAccepted status codes, up to 20.
http.containsstringnoneText the response body must contain, up to 200 characters.
startupTimeoutSecondsinteger30010 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.

json
"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"
  }
]
FieldTypeDefaultDescription
idslugrequiredUnique within the recipe.
kindstringrequiredmanaged-service, api-credentials, oauth-app or deploy-target.
providerslugrequiredFor example resend, cloudflare, convex, stripe.
capabilityslugnoneWhat the app needs, such as email-sending, database, object-storage, backend, oauth-client, payments.
titlestringrequiredPlain-language name, up to 80 characters.
reasonstringrequiredWhy the app needs it, up to 500 characters.
optionalbooleanfalseThe app runs without it, minus a feature.
inputsstring[][]Input keys this requirement fills, up to 16. Each must be declared in inputs, and only one requirement may fill it.
resourcesobject[]provider defaultWhat Setup creates per app, such as { "type": "d1-database", "name": "db" }. Up to 8.
outputsobjectnoneWhich created value fills which input. Keys must be listed in inputs.
docsUrlURLnoneWhere to sign up or create the credentials.
selfHostedobjectnonecatalog (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:

json
"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.

json
{
  "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

WhatLimit
Services per recipe8
Inputs per recipe32
Requirements per recipe16
Tags12
Environment variables per service128
Ports per service8
Volumes per service8
CPUs per service16
Memory per service65,536 MB
Inline Dockerfile32,000 characters
Notes8,000 characters
Startup timeout1,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,
  • privileged mode or Linux capabilities,
  • devices, apart from the gpu flag,
  • 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.