Skip to content
Console →
Website →
Asking an AI? Paste this URL https://keelson.dev/llms.txt

keelson.yaml

keelson.yaml is the deployment configuration file at the root of your project. It defines the application runtime, startup command, environment variables, database mode, scheduled jobs, and static assets.

Keelson reads this file during deployment and uses it to determine how to build and run the application. For a task-oriented introduction, see Configure an app with keelson.yaml.

slug: my-app
runtime: python-slim
command: "python app.py"
db:
mode: none

FieldTypeRequiredDefaultDescription
slugstringYes—Application identifier; used when the application is created
workspacestringNonullDefault workspace for project-scoped CLI commands. Prefer the workspace slug. Omit this field when your account has only one workspace because the CLI selects it automatically. An explicit --workspace overrides this value; apps list remains unfiltered
descriptionstringNonullA short description of the application, up to 300 characters
iconstringNonullImage shown as the app’s icon in the console; a path relative to keelson.yaml. See icon
typestringConditionalnullApp type; only web is accepted. Required for static sites and SPAs without a start command
runtimestringYes—Execution environment; see Supported runtimes
commandstring | listConditional—Start command; optional for scheduled jobs only, or static sites with type: web and assets
envmapNo{}Non-secret environment variables
dbobjectYes—Database and local SQLite policy
cronslistConditional[]Scheduled jobs; required without command unless this is a static site with type: web and assets
taskslistNo[]Background tasks that the app enqueues with the SDK; see tasks
mcpobjectNonullTools that AI clients use over MCP; see mcp
storageobjectNo{}Compatibility block that accepts only the deprecated disk_id field
assetsobjectNonullStatic asset delivery configuration
healthobjectNonullHealth check path used during deployment
secretsobjectNo{}Secret declarations and requirements
authobjectNonullEndpoints that bypass interactive Keelson authentication
verifystring | listNo[]Extra paths checked after a deployment becomes healthy
regionstringNonullLogical placement region for a new application

The retired top-level databases and workers keys are not valid, even when their value is null or an empty list. Use db.mode: libsql for durable relational data, tasks for work that runs after a request, and crons for time-triggered work.

The former top-level tenant field remains accepted as a compatibility alias for workspace. No removal date is set. If both fields are present, their raw string values must match exactly.

This includes leading and trailing whitespace and letter case.

Unknown top-level keys such as build, port, and scaling, and unknown keys within objects, are rejected. This is not an extensible format for custom fields. null is rejected for crons, secrets, and storage; to omit one, leave out the key entirely.

When in doubt, quote string values. This ensures the CLI and API read the same string.

The following strings require quotes or a block scalar (| / >). Unquoted values are rejected with string_value_not_portable:

  • Values beginning with a digit, such as slug: "123-app".
  • Values beginning with a non-ASCII character, such as description: "Équipe commerciale".
  • Values beginning with -, including command arguments: command: [python, app.py, "--port", "8080"].
  • Words YAML interprets as booleans or null: yes, no, on, off, true, false, null, and the case variants in Environment variable key constraints.
  • Values starting with other symbols not covered by the exceptions below, such as ":memory:".

Quotes may be omitted in the following cases, except for the boolean and null words above:

Value typeUnquoted form allowedExamples
Identifiers such as slug, runtime, or secret namesStarts with an ASCII letter or _my-app, python-slim, OPENAI_API_KEY
Commands, descriptions, paths, and other stringsStarts with an ASCII letter, _, /, ./, or ../python app.py, /health, ./app

A colon alone does not require quotes: description: https://example.com is valid unquoted. YAML syntax can still require quotes, for example around strings containing : (a colon followed by a space). Quoting does not change field-specific path or length constraints.

env values are an exception: every value requires quotes or a block scalar, regardless of its first character. See env.


The app identifier. It forms part of the app URL assigned at creation: https://<workspace-slug>--<slug>.keelson.run.

slug: my-app

Rules:

  • Use lowercase letters, digits, and hyphens only: a-z, 0-9, and -.
  • The length must be between 1 and 63 characters.
  • The value must start and end with a letter or digit.
  • Consecutive hyphens are not accepted.
  • Reserved slugs are admin, api, assets, auth, console, health, static, and www.

public_slug is a deprecated compatibility field. It is ignored because the public URL is derived from the application host.


An optional one- or two-sentence explanation of who the application is for and what it does.

description: "Daily and weekly reports for the sales team, with automatic weekly summaries."

After trimming surrounding whitespace, the value must be at most 300 characters (Unicode code points). Exceeding this limit fails deployment. The description appears in the console and is applied only if the app’s description is still empty. Deployment does not overwrite a description edited in the console.


An optional image that the console shows as the app’s icon in the app list and app header. Apps without an icon show their initial letter, and apps created from a template show the template’s icon.

icon: ./assets/app-icon.png
  • The path is relative to the directory that contains keelson.yaml, not to the directory you run the command from. It must stay inside the project: absolute paths, paths that leave the project with .., backslashes, and symbolic links are rejected.
  • The image is uploaded with your source, so it must not be excluded from the archive (see Archive contents). For a static site without command, put it inside assets.dir.
  • Accepted images: PNG, JPEG, or WebP, at most 256 KB, and 32 to 2048 pixels on each side. The format is detected from the file’s contents, not its extension. SVG is not accepted.
  • keelson deploy (including --check) checks the image before it creates the app or uploads anything, and stops with the path and the reason: icon.path (missing, unreadable, symbolic link, or not uploaded), icon.format, icon.size, icon.dimensions, or icon.corrupt (the image header cannot be read).

The icon is applied when the deployment succeeds, and the CLI prints one Icon: line when it finishes (icon_outcome in --ndjson output):

icon_outcomeMeaning
appliedThe image from keelson.yaml is now the app’s icon
unchangedThe app already uses this image
kept_console_iconAn icon uploaded in the console is kept; keelson.yaml does not replace it
kept_clearedThe icon was removed in the console, so deployments do not add it back

An icon set or removed in the console always takes precedence over keelson.yaml. To use the image from keelson.yaml after that, upload it in the app’s Settings in the console. Removing icon from keelson.yaml does not remove the app’s icon.

App icons are being rolled out. If a deployment is rejected with icon is not available yet, remove icon from keelson.yaml and deploy again.


The app type. Required for static sites and SPAs without a start command; optional otherwise.

type: web

The only accepted value is web. It enables static asset delivery through assets and permits a deployment without a top-level command when static assets are present.

Constraints:

  • type: web cannot be combined with crons.
  • Omit type for apps that combine a web app and scheduled jobs.

The execution environment. Required even for static-only deployments, but not used to build them. Static sites serve assets.dir exactly as built locally or in CI.

RuntimeLanguageIntended use
python-slimPythonLightweight applications, APIs, and text processing
python-mediaPythonImage and video processing, including Pillow, opencv, and ffmpeg-related libraries
node-slimNode.jsLightweight Node.js applications
node-mediaNode.jsImage processing, including sharp
go-slimGoLightweight Go applications
go-mediaGoMedia-processing Go applications

Start with a -slim runtime unless the application needs the extra media libraries included in a -media runtime.

Keelson installs dependencies during the build for container and hybrid deployments. Static sites (type: web and assets, without command) skip this step. By default, only files under assets.dir and keelson.yaml are uploaded for static sites; see Archive contents. Use command only to start the app. Including pip install, npm install, or go build causes the CLI preflight check to stop deployment with command_installs_dependencies.

RuntimeDetected filesBuild step
python-*requirements.txtpython -m pip install --user -r requirements.txt
python-*pyproject.toml with [project] or [build-system], only if requirements.txt is absentpython -m pip install --user .
node-*package-lock.json / package.jsonnpm ci when a lockfile is present, or npm install, followed by npm run build --if-present
go-*go.mod / go.sumgo mod download, then go build -o /workspace/app . (start it with command: "./app")

Build constraints:

  • If both requirements.txt and pyproject.toml exist, only requirements.txt is used. Dependencies and local packages declared only in pyproject.toml are not installed; include everything required in requirements.txt.

  • An installable Python pyproject.toml must be accompanied by a recognized lockfile: requirements.txt, poetry.lock, uv.lock, or Pipfile.lock.

  • Node.js builds use npm only. Apps declaring external packages, including development, optional, or peer dependencies, require package-lock.json. Apps without external dependency declarations may omit it. If package.json exists without a lockfile, the build runs npm install. If only pnpm-lock.yaml or yarn.lock exists, preflight stops deployment with lockfile_unsupported; these files may remain alongside package-lock.json.

  • A Go module that declares third-party dependencies must include go.sum. A standard-library-only module does not require it.

  • Dependencies must be available from public registries. Private registries, git+ssh dependencies, and authenticated installs fail.

  • Go builds use CGO_ENABLED=0.

  • The build target is linux/amd64 only.

  • Secrets are not available during the build.

  • The build execution time limit is 600 seconds.


The command executed when the application starts. Use either a shell string or an argument list.

# String form: executed with /bin/sh -c
command: "python app.py"
# List form: executed directly
command:
- python
- app.py

Rules:

  • Supply command, crons, or a static site configured with both type: web and assets.
  • A cron-only application does not need a top-level command.
  • A static-only type: web application with assets does not need a top-level command.
  • Do not include dependency installation or build steps. Keelson runs them automatically during the image build.
  • Empty strings and empty lists mean no start command. An ordinary web app requires a command. crons[].command cannot be empty.

Defines non-secret environment variables as key-value pairs. All values are strings.

Keelson sets PORT at runtime. A value declared in env is ignored.

Use quotes or a block scalar (| / >) for every value. An unquoted value is rejected with env_value_not_string, and deployment does not start. Numbers and booleans must also be quoted.

env:
NODE_ENV: "production"
DEBUG: "false"
LOG_LEVEL: "info"

Quoting is required because the CLI and API parse the file using YAML 1.2 and YAML 1.1 implementations, respectively. Those versions can assign different types to the same unquoted scalar. For example, K: 0o123 can become either "83" or "0o123". Quoting guarantees that both paths deploy the same string.

An unquoted key must start with a letter or underscore and contain only letters, digits, and underscores. Ordinary names such as NODE_ENV and DB_POOL can therefore be written without quotes.

The following words cannot be used as unquoted keys because YAML 1.1 may interpret them as booleans or null:

yes Yes YES no No NO true True TRUE false False FALSE on On ON off Off OFF null Null NULL

Quote a key to use a hyphen, a reserved YAML word, or another otherwise unsupported spelling.

env:
NODE_ENV: "production"
"MY-VAR": "x"
"yes": "x"

Keys beginning with KEELSON_, case-insensitively, are reserved for the platform and cannot be set in env. Duplicate env declarations, duplicate keys inside env, and YAML merge keys (<<) are also rejected.

Declare sensitive settings in secrets and set their values through the console or CLI. When a secret has the same name as an env entry, the secret takes precedence; changing that env value has no effect.


Selects how the application handles relational data. The db block and its mode field are required.

db:
mode: libsql
FieldTypeRequiredDefaultDescription
modestringYes—libsql or none; legacy turso is normalized to libsql
migratestringNonullMigration command run during deployment; valid only with libsql
auto_adoptbooleanNofalseAccepted for compatibility but currently does nothing; it does not adopt an existing database
local_sqliteobjectNonullExplicit declaration of regenerable, ephemeral local SQLite files
modeMeaning
libsqlKeelson provisions Managed SQLite dedicated to the app and injects KEELSON_DB_URL / KEELSON_DB_AUTH_TOKEN (aliases: TURSO_DATABASE_URL / TURSO_AUTH_TOKEN)
noneKeelson does not manage a database; use this for database-free apps or an external database

Managed SQLite with mode: libsql requires no separate account or connection configuration. It supports scale-to-zero and is the recommended option for durable relational data.

For PostgreSQL, MySQL, or an independently managed libSQL service, select mode: none and configure connection details as secrets. Keelson does not inject credentials for an external database.

No mode makes file SQLite persistent (/data does not exist and cannot be created by the app). Code using file SQLite clients such as sqlite3 or better-sqlite3 is detected during deployment and rejected unless local_sqlite is declared.

db.migrate defines an application-owned migration command. Keelson runs it once against the new image before switching rollout traffic.

db:
mode: libsql
migrate: "python migrate.py"
  • Valid only with mode: libsql; use a string.
  • Runs only for container and hybrid deployments. Static sites have no container, so a declared migration does not run.
  • Runs in the deployment image after the candidate revision passes its health check and before traffic switches. App startup code must work with the schema before migration.
  • A nonzero exit code fails the deployment with deploy.runtime.migration_failed, and the previous revision remains active.
  • Runs on every deployment, so make it idempotent, for example with CREATE TABLE IF NOT EXISTS and ON CONFLICT DO NOTHING.

Use db.local_sqlite only for a disposable file that can be regenerated. It documents the application policy; it does not make the file persistent.

db:
mode: none
local_sqlite:
policy: ephemeral
paths:
- /tmp/cache.db
reason: "A derived cache rebuilt from the source API"
FieldTypeRequiredConstraint
policystringYesMust be ephemeral
pathslist of stringsYesNon-empty; each entry must be under /tmp/ or be :memory:
reasonstringYesNon-empty explanation of why losing the data is safe

The retired databases block is always rejected. It cannot be used to persist a file SQLite database.


Defines scheduled jobs. Keelson starts each job according to its schedule in a separate instance from the web service.

crons:
- name: cleanup
description: "Delete expired sessions"
schedule: "0 3 * * *"
command: "python cleanup.py"
timeout: 60
enabled: true
FieldTypeRequiredDefaultDescription
namestringYes—Unique job name, 1–63 lowercase letters, digits, or hyphens
schedulestringYes—Valid five-field cron expression
commandstring | listYes—Command to execute
timeoutintegerNo300Timeout in seconds, from 1 through 600
enabledbooleanNotrueWhether Keelson schedules the job
descriptionstringNo—One-line explanation of the job, shown in the console’s Scheduled jobs tab; see crons[].description below

Rules:

  • A configuration may contain at most 10 jobs. The plan may impose a lower limit.
  • Job names must be unique.
  • crons[].description is display only. Leading and trailing whitespace is removed, and an empty value counts as unset. A value longer than 100 characters after trimming is rejected with cron_description_too_long; it is not truncated. Changing it does not change what runs.
  • type: web and crons cannot be combined.
  • A job runs in a separate instance; local files, including /tmp, are not shared with the web app. Store shared state in db.mode: libsql.
  • If the previous run is still active, Keelson skips the overlapping run.
  • Schedules are evaluated in the workspace time zone. This setting is detected automatically from the browser when the workspace is created.
  • The absolute timeout limit is 600 seconds. The default is 300 seconds, reduced to the plan limit if that is lower.

See Plans and limits for job counts, minimum intervals, and timeout limits by plan.

ExpressionMeaning
0 * * * *At minute 0 of every hour
*/15 * * * *Every 15 minutes
0 3 * * *Every day at 03:00
0 9 * * 1-5Weekdays at 09:00
0 0 1 * *The first day of each month at 00:00

* * * * * (every minute) is rejected because it is shorter than every plan’s minimum interval. See Scheduled jobs for execution behavior and design.


Declares background tasks. When the app’s code enqueues a task by name with the SDK, Keelson runs the declared command once in a separate instance from the web service and retries a failed attempt automatically. Unlike crons, a task is never started by the clock. See Background tasks for enqueueing, writing the command, and making it safe to run twice.

command: "python app.py"
tasks:
- name: generate-pdf
description: "Generate the invoice PDF for an order"
command: "python generate_pdf.py"
timeout: 180
max_attempts: 5
FieldTypeRequiredDefaultDescription
namestringYes—Task name, 1–63 lowercase letters, digits, or hyphens (same rule as crons[].name). Leading and trailing whitespace is removed and the name is lowercased, so Foo is accepted as foo
commandstring | listYes—Command to execute. A string runs with /bin/sh -c; a list runs directly
timeoutintegerNo300Time limit for one attempt in seconds, from 1 through 600
max_attemptsintegerNo3Maximum number of attempts, including the first, from 1 through 5
descriptionstringNo—One-line explanation of the task, shown in the console’s Background tasks tab; see tasks[].description below

Rules:

  • A configuration may contain at most 10 tasks on every plan.
  • Task names must be unique, and a task cannot have the same name as a job in crons (task_name_conflicts_with_cron). Names are compared after removing surrounding whitespace and lowercasing.
  • A configuration with tasks but neither command nor crons is rejected with tasks_require_command_or_crons, because no code could enqueue the tasks. This includes a static site with type: web and assets but no command.
  • type: web and tasks can be combined.
  • tasks[].description is display only. Leading and trailing whitespace is removed, and an empty value counts as unset. A value longer than 100 characters after trimming is rejected with task_description_too_long; it is not truncated. Changing it does not change what runs.
  • Unknown keys are rejected. Concurrency cannot be declared; Keelson applies the plan’s value.
  • The absolute timeout limit is 600 seconds. An explicit timeout above the plan limit is rejected with plan_task_timeout; the default of 300 seconds is reduced to the plan limit if that is lower.
  • Each attempt receives one line of JSON on standard input with attempt_no, payload, schema_version, task_id, and task_name. Exit code 0 means success. A task runs in a separate instance; local files are not shared with the web app.

See Plans and limits for timeout limits by plan.


Declares the app’s tools so that AI clients such as Claude and ChatGPT can use the app over MCP. When a tool is called, Keelson calls the app’s POST /api/mcp/<name>. Keelson handles login, the consent screen, and the exchange with the AI client. See Use your app from AI clients (MCP) for writing handlers, connecting, and limits.

command: "python app.py"
mcp:
enabled: true
instructions: "Look up stock levels and record incoming deliveries."
tools:
- name: search_items
description: "Search stock by item name."
access: read
input:
type: object
properties:
query:
type: string
required: [query]
- name: create_arrival
description: "Record one incoming delivery."
access: write
permission: manage
input:
type: object
properties:
item_id:
type: string
quantity:
type: integer
minimum: 1
required: [item_id, quantity]
FieldTypeRequiredDefaultDescription
enabledbooleanYes—true publishes the tools. false keeps the declaration but stops calls (existing connections are not revoked)
instructionsstringNonullDescription of the tools as a whole, passed to the AI client. Up to 4096 characters
toolslistConditional—The tools. Required when enabled: true. When written, 1–40 entries regardless of enabled
filestringNo—Separate file holding instructions and tools; see mcp.file. Requires a recent Keelson CLI
tools[].namestringYes—Tool name matching ^[a-z][a-z0-9_]{0,63}$ (starts with a lowercase letter; lowercase letters, digits, and _ only; not normalized). Must be unique. The handler path is POST /api/mcp/<name>
tools[].descriptionstringYes—Description shown to the AI client. 1–1024 characters, not only whitespace
tools[].accessstringYes—read or write. write tools are shown separately on the consent screen, and users can approve read-only access
tools[].permissionstringNoviewview or manage. A manage tool can be used only by people with the app’s manage permission
tools[].inputobjectYes—JSON Schema (2020-12) for the arguments. The top level must be type: object

Rules:

  • Only apps that run server-side code (with command and without assets) can declare mcp. Static sites, SPAs, and hybrid apps are rejected with mcp is only supported for container apps.
  • Unknown keys under mcp and tools[] are rejected. The handler path is fixed, so tools[].endpoint cannot be written.
  • Write input with JSON-compatible values only: at most 32 levels deep and 64 KB as JSON. Quote dates and any key that would otherwise not be read as a string (yes, on, numbers, and so on).
  • Do not use $ref, $dynamicRef, $recursiveRef, or x-mcp-header in input. $ref and x-mcp-header are rejected at deploy; $dynamicRef and $recursiveRef pass deployment but every call fails.
  • YAML anchors, aliases, and merge keys (<<) cannot be used under mcp.
  • Even without mcp:, /api/mcp and paths beginning with /api/mcp/ are reserved for MCP in apps that run server-side code (see Reserved URL paths).

When there are many tools, instructions and tools can live in a separate YAML file. keelson.yaml then contains only enabled and file.

command: "python app.py"
mcp:
enabled: true
file: mcp.yaml
# mcp.yaml (in the same folder as keelson.yaml)
instructions: "Look up stock levels and record incoming deliveries."
tools:
- name: search_items
description: "Search stock by item name."
access: read
input:
type: object
properties:
query:
type: string
required: [query]
  • At deploy time, the Keelson CLI inserts the separate file’s contents into mcp in keelson.yaml before validating and uploading. Your local keelson.yaml is not rewritten.
  • Only instructions and tools may appear at the top level of the separate file (both optional). mcp.file cannot be combined with mcp.instructions or mcp.tools.
  • The path is relative to the folder containing keelson.yaml and cannot point outside it (including through .. or symbolic links). The extension must be .yaml or .yml, and the file must be 4 MiB or smaller.
  • Errors inside the separate file are reported with the file name and location, such as mcp.yaml: tools[3].input ....
  • Only a recent Keelson CLI reads mcp.file. Older CLIs reject the deployment with mcp contains unsupported field(s): file; run keelson upgrade to get the latest CLI. It cannot be used through paths that bypass the CLI.

workers for background workers or periodic drains has been retired. Any top-level workers key is rejected with workers_not_supported, including an empty list or null value.

For work that runs in response to a request, declare it in tasks and enqueue it with the SDK (Background tasks). For a periodic drain of pending work, define a scheduled job in crons. Use an interval allowed by the plan; process pending items and exit on each run.

# Invalid: deployment is rejected
workers:
- name: drain
command: "python worker.py"
every: 10m
# Valid: use crons
crons:
- name: drain
schedule: "*/5 * * * *"
command: "python drain.py"
timeout: 120

storage accepts only the deprecated disk_id field. The value is ignored; it does not enable persistent storage or automatic synchronization of /data. Remove this block if it remains in your configuration.

FieldTypeRequiredDefaultDescription
disk_idstringNonullDeprecated and ignored identifier; 1–32 lowercase letters, digits, or hyphens

Configures static asset delivery for static sites, SPAs, and hybrid applications that combine static files with a backend API.

assets:
dir: dist
fallback: index.html
api: /api
FieldTypeRequiredDefaultDescription
dirstringYes—Asset directory relative to the project root
static_dirstringNonullDeprecated alias of dir; if both are present, they must match
fallbackstringConditionalnullSPA fallback, such as index.html, relative to dir. Required for hybrid apps with type: web
apistringNonull (effective value /api in hybrid apps)Path prefix forwarded to the backend. Defaults to /api when both command and assets are present

In hybrid apps, requests under api go to the backend; other requests serve static assets. The default prefix is /api; set this field only to use a different prefix.

Rules:

  • dir cannot contain ...
  • api must begin with /, cannot be / itself, and cannot be under /__keelson.
  • api requires a top-level command.
  • A hybrid app with type: web, assets, and command requires fallback. If type is omitted, configuration validation allows fallback to be omitted. Set a fallback such as index.html when serving an SPA.

Static sites (type: web and assets, without command) upload only files under assets.dir and keelson.yaml by default. Container and hybrid apps also include source needed for the build. The exclusions below apply to both.

Avoid assets.dir: . and equivalent paths such as ./. They make the project root the public asset directory, potentially exposing every file in the archive. The CLI warns before deployment but does not reject this setting.

When the CLI creates the deploy archive, it normally excludes paths containing any of these names: .git, .venv, __pycache__, .pytest_cache, node_modules, dist, build, .idea, .vscode, and .DS_Store. The declared assets.dir and its ancestor directories are exempt, so build output such as dist is included. Built-in exclusions still apply below assets.dir: for example, dist/index.html is included but dist/node_modules/** is not. .git is always excluded, as are symlinks, other non-regular files, and the file selected by --secrets-from-env-file.

Env-style files and directories are also excluded, including inside assets.dir. A final path element is treated as an env name when it starts with .env or ends with .env, case-insensitively. Examples include .env, .env.production, .envrc, and .secrets.env. This rule applies whether or not --secrets-from-env-file is used.

Customizing exclusions with .keelsonignore

Section titled “Customizing exclusions with .keelsonignore”

Create .keelsonignore in the project root for project-specific exclusions. .gitignore is not read.

SyntaxMeaning
One pattern per lineExclude matching files or directories; the last matching line wins
A line beginning with #Comment
/, trailing /, *, ?, **, [abc], [a-z]Gitignore-like path patterns
Leading !Re-include matching files or directories

Negation can deliberately re-include an env file such as !.env.production, but cannot override built-in exclusions such as .git, include .keelsonignore itself, or include the file selected by --secrets-from-env-file.

For a static app with assets and no string- or array-form command, the archive includes only files under assets.dir and keelson.yaml by default; ancestor directories remain traversable so the CLI can reach the asset directory. Files outside that tree are not uploaded. A leading ! rule can deliberately restore an outside file. To restore a directory and its contents, use both !keep/ and !keep/**. Container and hybrid apps still archive the source needed for their builds.

Run keelson deploy --check --json to inspect exclusions and files that may contain secrets.

  • Detected preview credentials or app tokens block upload.
  • If only webhook signing keys are detected, the CLI warns and continues.
  • Files whose names suggest sensitive content also produce warnings. Warnings do not exclude files; add exclusions to .keelsonignore as needed.

--check also validates major settings locally. However, the API validates unknown keys inside assets, crons, auth, and secrets, cron expression validity, and verify path formats. A deployment can still fail with deploy.config.invalid after --check passes.

JSON fieldWhat it reports
archive.excludedExcluded files
archive.excluded_env_filesExcluded env files
archive.secret_like_filesFiles suspected of containing secrets based on their names or other indicators
archive.embedded_credentialsPaths and types of credentials detected inside files; values are never shown
archive.unscanned_filesFiles whose contents were not scanned, for example because of size limits
archive.unscanned_binary_countNumber of files classified as binary and not content-scanned

Files over 16 MiB, binary files, and files excluded from the archive are not content-scanned.

secret_like_files includes names such as credentials.json and *.pem, plus existing files selected by an earlier --secrets-from-env-file deployment that are not excluded this time. This includes names such as prod-secrets.txt that do not follow env-file naming rules.

To identify previously selected files, the CLI records their project-relative paths in .keelson-config/previous-secrets-files.json. The record contains no secret values and is automatically excluded from uploads.

JavaScript and CSS files with a clear content hash in the filename, such as index-B7hK2mQ1.js, are cached for one year because the content at that URL is expected never to change. Keelson recognizes a final 8–64 character segment made of ASCII letters, digits, and underscores, containing both letters and digits, as a hash. HTML, unhashed files such as style.css and app.js, images, and all other assets are checked for updates on every visit. When a file has not changed, its ETag makes this a lightweight validation request.

After redeploying a static site (edge-static / edge-spa), new files can take up to about 60 seconds to reach the edge (measured 1–57 seconds, median about 30). Wait 60 seconds before concluding a file is missing.


Sets the deployment health check path. Use a lightweight endpoint if / is expensive or depends on the database.

health:
path: /health
  • Defaults to /.
  • Must start with /, contain no .., not start with /__keelson, and be at most 2,048 characters.
  • HTTP 2xx, 3xx, and 404 pass; other 4xx and all 5xx fail. Because 404 passes, success proves the process started, not that the route exists.
  • The probe goes directly to the candidate revision’s internal URL, bypassing the public host and authentication. Use verify to check public paths.

Declares the app’s secrets and required combinations. Set values separately through the console, CLI, or an env file during deployment; do not put values here.

secrets:
items:
- name: OPENAI_API_KEY
description: "OpenAI API key"
- name: ANTHROPIC_API_KEY
description: "Anthropic API key"
required:
- any_of: [OPENAI_API_KEY, ANTHROPIC_API_KEY]
message: "Set at least one AI provider key"
FieldTypeRequiredDefaultDescription
itemslistNo[]Secret definitions
items[].namestringYes—Environment variable name: starts with an uppercase letter or _, contains only A-Z0-9_, and is at most 255 characters. KEELSON_ names are reserved
items[].descriptionstringNo""Human-readable purpose of the secret
requiredlistNo[]Rules checked against configured secrets
required[].any_oflistConditionalnullNonempty list of names; at least one must be set
required[].all_oflistConditionalnullNonempty list of names; all must be set
required[].messagestringNo""Guidance displayed when requirements are unmet

Each required entry must contain exactly one of any_of or all_of. The selected list must be non-empty and may reference only names declared in items. An optional message supplies remediation text.

Names are normalized to uppercase when values are saved, while required matching is case-sensitive. If you declare name: api_key, its value is saved as API_KEY, so the requirement does not match. Use uppercase names from the start. Names with hyphens, such as MY-KEY, can be declared but cannot have values set.

For a new app, keelson deploy --new --secrets-from-env-file <path> creates the app, registers secrets, and starts its first deployment in one command. Changes to an existing app’s secrets take effect on the next deployment.


Configures application endpoints that are intended for external systems, such as webhook senders, and therefore do not use the normal interactive Keelson login flow.

auth:
endpoints:
- /api/webhooks/stripe
- path: /api/external/status
methods:
- GET

auth.endpoints is required when auth is present and must be a non-empty list. Each entry is either a path string or an object with path and optional methods.

Endpoint paths must be unique and start with /api/external/ or /api/webhooks/. Paths under /__keelson are reserved. If methods is provided, it must be a non-empty list.

Paths use prefix matching: /api/external/status also matches /api/external/status-extra. Methods are case-insensitive; omitting them allows every method. methods only applies to /api/external/, not /api/webhooks/. Restrict webhook methods in app code.

Bypassing interactive login does not bypass Keelson authentication. Declaring an endpoint alone is insufficient; callers must supply Keelson-issued credentials.

PathRequired credentials
/api/external/...App token with the api scope in Authorization: Bearer
/api/webhooks/...App token with the webhook scope in X-Webhook-Secret, or a URL registered with the external service in the form /api/webhooks/<secret>/...

Issue tokens with keelson apps tokens create. See External integrations for URL construction. A standard Stripe webhook carrying only a Stripe signature, or a client carrying only an app-defined API key, is rejected before reaching the app.

Requests to these paths carry X-Keelson-User-Id as an empty string (the header is always set, empty when there is no interactive user). Treat an empty value as “no user”. In addition to Keelson credentials, verify webhook signatures or API keys in the app.


Adds paths to deployment verification. By default, only / is checked.

verify:
- /dashboard
- /settings

Use this to check entry points that cannot be reached from /, such as pages accessible only through client-side routing.

  • A single path may be a string or a one-item list.
  • At most 20 paths are allowed after trimming whitespace and removing duplicates. Each must start with /, must not start with //, and must not contain .. path components or control characters.
  • Container and hybrid deployments fetch paths from the candidate revision through the public host. Static sites and SPAs resolve paths against the release manifest before publication.
  • A declared path must return 2xx or 3xx. A 404, 401, 403, 405, or any other response outside those ranges fails verification with verification_declared_path_unreachable, for both container/hybrid apps and static/SPA apps.
  • Write only paths that do not require the app’s own user login. Verification requests identify the candidate revision but do not carry an app user identity, so an app-level 401 or 403 fails the deploy.
  • / remains a tolerant default even if it also appears in verify; an API-only app may legitimately return 404 there.

This differs from health.path: the health probe goes directly to the candidate revision and accepts 404 as proof that the process started. verify checks whether a declared public path is actually servable, so 404 does not pass.


Optionally selects the placement region when creating a new application.

region: jp-tokyo

Use a Keelson logical region key, not a cloud-provider region identifier:

KeyDisplay nameNew apps
jp-tokyoJapanAvailable (default)
us-oregonUS WestNot yet offered (an explicit selection is rejected)

For a new application, selection priority is the CLI --region option, then region in keelson.yaml, then the workspace default. An unknown or not-yet-available explicit selection is rejected instead of falling back to another region.

An application’s region is fixed when the application is created. A later deploy with a different region is rejected and cannot move the application; create a new application to use another region.


Deployment mode is derived from the presence of command, assets, and fallback; it is not selected directly. CLI and API output such as keelson status --json returns the raw deploy_mode label. Static sites and SPAs also require type: web.

NameRaw deploy_modecommandassetsfallbackDescription
Web applicationcontainerPresentAbsent—Ordinary application deployment
Static siteedge-staticAbsentPresentAbsentStatic files only
SPAedge-spaAbsentPresentPresentStatic files with a fallback
HybridhybridPresentPresentRequired with type: webStatic files plus a backend API

Some fields are valid only in specific combinations.

RuleRequirement
A start command, scheduled jobs, or a static site is requiredSupply command, crons, or both type: web and assets
Tasks need code that enqueues themWhen tasks is present, supply command or crons (tasks_require_command_or_crons)
Task and job names are separateA tasks[].name cannot match a crons[].name (task_name_conflicts_with_cron)
Static sites cannot use scheduled jobsCombining type: web and crons fails configuration validation. Omit type when using scheduled jobs
Static sites have no containerdb.migrate does not run for static sites or SPAs. Use a container or hybrid deployment for migrations
workers is retiredReplace every workers declaration with tasks (work that runs after a request) or crons (time-triggered work)
Managed migration requires managed datadb.migrate requires db.mode: libsql
Local SQLite is disposabledb.local_sqlite.paths accepts only /tmp/ paths and :memory:
mcp requires an app that runs server-side codemcp can be written only with command and without assets (mcp is only supported for container apps)
assets.api requires a backendSet a top-level command before using an API prefix
Hybrid deployments with type: web require a fallbackSet assets.fallback when type: web, assets, and command are present. This check does not apply if type is omitted

slug: crud-app
description: "An internal CRM for managing customers and deals"
runtime: python-slim
command: "python app.py"
db:
mode: libsql
migrate: "python migrate.py"
env:
PYTHONUNBUFFERED: "1"

Run a local build such as npm run build to create dist before deployment. Keelson does not build a static site.

slug: marketing-site
type: web
runtime: node-slim
db:
mode: none
assets:
dir: dist
fallback: index.html
slug: daily-report
runtime: python-slim
db:
mode: libsql
env:
PYTHONUNBUFFERED: "1"
crons:
- name: generate
schedule: "0 9 * * *"
command: "python report.py"
timeout: 120

The web application enqueues generate-pdf with the SDK when it receives a request. Keelson runs the task’s command in an instance separate from the web application. See Background tasks.

slug: pdf-reports
runtime: python-slim
command: "python app.py"
db:
mode: libsql
env:
PYTHONUNBUFFERED: "1"
tasks:
- name: generate-pdf
command: "python generate_pdf.py"
timeout: 180
max_attempts: 3

To run work after a request without background tasks, store it in the database and process it with a scheduled job. Work waits until the next run. To start work right after a request, use background tasks instead.

The web application writes pending rows to Managed SQLite. The cron processes those rows from a separate instance, so both processes share state through db.mode: libsql, not through the local filesystem.

slug: task-drainer
runtime: python-slim
command: "python app.py"
db:
mode: libsql
env:
PYTHONUNBUFFERED: "1"
crons:
- name: drain
schedule: "*/5 * * * *"
command: "python drain.py"
timeout: 120
slug: photo-galleries
runtime: node-slim
command: "node server.js"
db:
mode: libsql
env:
NODE_ENV: "production"
assets:
dir: dist
fallback: index.html
api: /api
slug: my-go-app
runtime: go-slim
command: "./app"
db:
mode: none

Keelson builds the Go source for Linux and places the binary at ./app during deployment.

slug: payment-hooks
runtime: node-slim
command: "npm start"
db:
mode: libsql
env:
NODE_ENV: "production"
secrets:
items:
- name: STRIPE_WEBHOOK_SECRET
description: "Stripe webhook signing secret"
required:
- all_of: [STRIPE_WEBHOOK_SECRET]
auth:
endpoints:
- path: /api/webhooks/stripe
methods: [POST]

Include an app token with the webhook scope in the URL registered with Stripe: https://<host>/api/webhooks/<token>/stripe. methods does not apply to webhooks; reject methods other than POST in app code if needed.


CauseResolution
db or db.mode is missingDeclare db.mode as libsql or none
The retired databases key is presentRemove it; use Managed SQLite or declare disposable local SQLite explicitly
A local SQLite path points outside /tmp/Use /tmp/, :memory:, or Managed SQLite for durable data
assets.api does not start with /Use an absolute path prefix such as /api
No start command, scheduled jobs, or static site configurationAdd command, crons, or both type: web and assets
workers is declaredRemove it; workers are rejected with workers_not_supported. Use tasks for work that runs after a request and crons for time-triggered work
crons[].timeout is greater than 600Reduce the timeout to the platform ceiling or lower plan limit
tasks is present without command or crons (tasks_require_command_or_crons)Add the command of the web app that enqueues the tasks, or a job in crons
A task has the same name as a scheduled job (task_name_conflicts_with_cron)Rename the task or the job
tasks[].timeout is greater than 600 (TASK_TIMEOUT_EXCEEDS_STANDARD_LIMIT)Reduce it to 600 or less, and to the plan limit (plan_task_timeout), or split the work into smaller tasks
mcp.enabled is true but mcp.tools is missingDeclare at least one tool, or move the tools into a separate file with mcp.file
A static site, SPA, or hybrid app has mcp (mcp is only supported for container apps)Remove mcp, or make the app a command-only app
mcp.tools[].input contains $ref or x-mcp-headerWrite the schema out in full. Do not use $dynamicRef or $recursiveRef either
mcp contains unsupported field(s): fileThe Keelson CLI is too old. Run keelson upgrade to get the latest version
An env value is unquotedQuote every value, including booleans and numbers
An environment variable starts with KEELSON_Rename it; that namespace is platform-owned
A hybrid deployment with type: web omits assets.fallbackSet a fallback file such as index.html
command installs dependencies (command_installs_dependencies)Remove the install or build step; dependencies are installed automatically during the image build
A dependency manifest is missing (missing_dependency_manifest)For Go, put go.mod at the project root. For Python or Node.js, add the appropriate manifest if the app uses external libraries; a standard-library-only app does not need one
runtime and the language in command differ (runtime_command_mismatch)Choose the matching runtime or fix the start command
Build fails with reserved_path_conflictRename the __keelson directory at the root of the build output