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
slugstringYesApplication 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
typestringNonullApplication type; only "web" is accepted
runtimestringYesExecution environment; see Supported runtimes
commandstring | listConditionalStartup command; optional for cron-only and static-only deployments
envmapNo{}Non-secret environment variables
dbobjectYesDatabase and local SQLite policy
cronslistConditional[]Scheduled jobs; required if there is no command or static asset deployment
storageobjectNo{}Compatibility block that accepts only the deprecated disk_id field
assetsobjectNonullStatic asset delivery configuration
secretsobjectNo{}Secret declarations and requirements
authobjectNonullEndpoints that bypass interactive Keelson authentication
emailobjectNo{}Inbound email configuration
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 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.


The application identifier. It is used as part of the application identity and, when creating an application, contributes to its default public address.

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: "An internal dashboard for reviewing support requests."

The value is trimmed and may contain at most 300 characters. An empty value is treated as unset. During deployment, the description is used only when the application does not already have a description, so a description edited in the Console is not silently overwritten.


The optional application type.

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.
  • Omitting type is appropriate for ordinary application and cron-only deployments.

The execution environment. A runtime is required even for a static-only deployment because Keelson still uses it during the build.

RuntimeLanguageIntended use
python-slimPythonLightweight applications, APIs, and text processing
python-mediaPythonImage and video processing workloads
node-slimNode.jsLightweight Node.js applications
node-mediaNode.jsMedia-processing Node.js applications
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 automatically during the image build. If the application uses external libraries, put the appropriate dependency manifest at the project root. Keep command limited to starting the application. If command includes pip install, npm install, or go build, the CLI pre-deploy check stops the 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]python -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:

  • An installable Python pyproject.toml must be accompanied by a recognized lockfile: requirements.txt, poetry.lock, uv.lock, or Pipfile.lock.
  • Node builds use npm and require package-lock.json. If only pnpm-lock.yaml or yarn.lock is present, the pre-deploy check stops with lockfile_unsupported. Those 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 through a shell
command: "python app.py"
# List form: executed directly
command:
- python
- app.py

Rules:

  • At least one of command, crons, or a static assets deployment must be present.
  • 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.
  • An empty string or empty list is not a command.

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

PORT is set by Keelson at runtime; do not declare it in env.

Always quote environment variable values. 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. 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.

Use secrets to declare sensitive settings. Do not commit secret values to keelson.yaml.


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

db:
mode: libsql
FieldTypeRequiredDefaultDescription
modestringYeslibsql or none; legacy turso is normalized to libsql
migratestringNonullMigration command run during deployment; valid only with libsql
auto_adoptbooleanNofalseCompatibility option for adopting an existing managed database
local_sqliteobjectNonullExplicit declaration of regenerable, ephemeral local SQLite files
modeMeaning
libsqlKeelson provisions an isolated managed database and injects KEELSON_DB_URL and KEELSON_DB_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.

There is no mode that makes a SQLite file under /data durable. Applications that need durable SQLite semantics must use a libSQL client with mode: libsql.

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 -m alembic upgrade head"

The command must be a string and is only valid with mode: libsql. An empty value is treated as no migration command.

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
schedule: "0 3 * * *"
command: "python cleanup.py"
timeout: 60
enabled: true
FieldTypeRequiredDefaultDescription
namestringYesUnique job name, 1–63 lowercase letters, digits, or hyphens
schedulestringYesValid five-field cron expression
commandstring | listYesCommand to execute
timeoutintegerNo300Timeout in seconds, from 1 through 600
enabledbooleanNotrueWhether Keelson schedules the job

Rules:

  • A configuration may contain at most 10 jobs. The plan may impose a lower limit.
  • Job names must be unique.
  • type: web and crons cannot be combined.
  • A job runs in a different instance from the web service, so local files such as /data are not shared.
  • 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 platform-wide timeout ceiling is 600 seconds. A plan-specific ceiling may be lower.

Plan limits for Starter, Plus, and Team are, respectively: 3, 5, and 10 jobs; minimum intervals of 60, 15, and 5 minutes; and timeout limits of 3, 5, and 10 minutes.

ExpressionMeaning
* * * * *Every minute
0 * * * *At minute 0 of every hour
0 3 * * *Every day at 03:00
0 0 * * 1Every Monday at 00:00

See Scheduled jobs for execution behavior, examples, and troubleshooting.


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.

Represent time-triggered background work with crons. A drain-style process can run on a short schedule, process the pending items, and exit successfully.

# 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.

General-purpose persistent file I/O cannot be configured in keelson.yaml.

storage:
disk_id: my-disk
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
dirstringYesAsset directory relative to the project root
static_dirstringNonullDeprecated alias of dir; if both are present, they must match
fallbackstringConditionalnullRelative fallback file for an SPA, such as index.html
apistringNonullPath prefix forwarded to the backend, such as /api

When api is set, requests under that path go to the backend application and all other requests are served as static assets.

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 a file selected by --secrets-from-env-file, regardless of its name. The CLI stores only that project-relative path, never secret values, in the automatically excluded .keelson-config/previous-secrets-files.json 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.

To exclude project-specific content, create .keelsonignore in the project root. It accepts one gitignore-like pattern per line, ignores blank lines and # comments, and supports /, trailing /, *, ?, **, [abc], [a-z], and leading ! negation. Matching is case-sensitive and the last matching rule wins. Negation can deliberately re-include an env file such as !.env.production, but cannot re-include .git, built-in exclusions, runtime build exclusions, .keelsonignore itself, or a file selected by --secrets-from-env-file. Invalid patterns produce a warning and are ignored.

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.

Avoid assets.dir: . and equivalent paths such as ./: the project root is then the public asset directory, so every otherwise archived file can be served. Keelson warns before deployment but does not reject this configuration.

Run keelson deploy --check --json and review its archive.excluded, archive.excluded_env_files, archive.secret_like_files, archive.embedded_credentials, archive.unscanned_files, and archive.unscanned_binary_count values before upload. Embedded credential entries expose only the path and kind, never the value. Preview and app-token matches block upload; webhook-signing-only matches warn and continue. Files over 16 MiB and files classified as binary are not content-scanned, and excluded files are outside the scan. secret_like_files warns about secret-like names and about an existing file selected by an earlier --secrets-from-env-file deploy when it is not selected for exclusion this time. Such files remain included, including non-env names such as prod-secrets.txt. The assets.dir exemption requires Keelson CLI v0.1.1 or later; check with keelson version and upgrade an older release before deploying assets from dist or build.

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.

You can redeploy an unhashed file under the same name. For hashed files, make sure your build tool generates a new filename whenever the content changes.

Rules:

  • dir cannot contain a .. path component.
  • fallback must be a relative path under assets.dir.
  • api must begin with /, cannot be /, and cannot be under /__keelson.
  • api is valid only when a top-level command provides a backend.
  • A hybrid deployment with both assets and command requires fallback.

Declares which secrets an application understands and which combinations are required. Values are set separately and never belong in this file.

secrets:
items:
- name: API_TOKEN
description: "Token used to call the upstream API"
- name: OAUTH_CLIENT_SECRET
required:
- all_of:
- API_TOKEN
message: "Set API_TOKEN before deploying."
FieldTypeRequiredDefaultDescription
itemslistNo[]Secret definitions
items[].namestringYesNon-empty secret name; KEELSON_ is reserved
items[].descriptionstringNo""Human-readable purpose of the secret
requiredlistNo[]Rules checked against configured secrets

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.


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/payments
- 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.


Controls inbound email support.

email:
inbound:
enabled: true

email.inbound.enabled is a boolean and defaults to false.


Adds explicit paths to the post-deployment verification probe. The default probe always checks /; these paths are additional entry points that Keelson should fetch after the deployment becomes healthy.

verify:
- /dashboard
- /settings

Use this for routes or assets that cannot be discovered from the default entry page, such as a route loaded only through a dynamic import. Omit it when the default probe is sufficient.

  • 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: us-oregon

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

KeyDisplay name
jp-tokyoJapan
us-oregonUS West

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.

NameRaw deploy_modecommandassetsfallbackDescription
Web applicationcontainerPresentAbsentOrdinary application deployment
Static siteedge-staticAbsentPresentAbsentStatic files only
SPAedge-spaAbsentPresentPresentStatic files with a fallback
HybridhybridPresentPresentRequiredStatic files plus a backend API

Some fields are valid only in specific combinations.

RuleRequirement
An executable surface is requiredSupply command, crons, or static assets
type: web excludes scheduled jobsDo not combine type: web with crons
workers is retiredReplace every workers declaration with an appropriate crons design
Managed migration requires managed datadb.migrate requires db.mode: libsql
Local SQLite is disposabledb.local_sqlite.paths accepts only /tmp/ paths and :memory:
assets.api requires a backendSet a top-level command before using an API prefix
Hybrid deployments require a fallbackSet assets.fallback when both assets and command are present

slug: flask-crud
description: "A CRUD application backed by Managed SQLite."
runtime: python-slim
command: "python app.py"
db:
mode: libsql
migrate: "python -m alembic upgrade head"
env:
PYTHONUNBUFFERED: "1"
slug: marketing-site
type: web
runtime: node-slim
db:
mode: none
assets:
dir: dist
fallback: index.html
slug: cron-logger
runtime: python-slim
db:
mode: none
env:
PYTHONUNBUFFERED: "1"
crons:
- name: heartbeat
schedule: "* * * * *"
command: "python heartbeat.py"
timeout: 30

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: python-slim
command: "python app.py"
db:
mode: libsql
env:
PYTHONUNBUFFERED: "1"
assets:
dir: static
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: node-crud
runtime: node-slim
command: "npm start"
db:
mode: libsql
env:
NODE_ENV: "production"

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
Neither command, crons, nor static assets is presentAdd at least one executable surface
workers is declaredReplace it with crons; workers are rejected with workers_not_supported
crons[].timeout is greater than 600Reduce the timeout to the platform ceiling or lower plan limit
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 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