keelson.yaml
Overview
Section titled “Overview”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.
Minimal example
Section titled “Minimal example”slug: my-appruntime: python-slimcommand: "python app.py"db: mode: noneTop-level fields
Section titled “Top-level fields”| Field | Type | Required | Default | Description |
|---|---|---|---|---|
slug | string | Yes | — | Application identifier; used when the application is created |
workspace | string | No | null | Default 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 |
description | string | No | null | A short description of the application, up to 300 characters |
type | string | No | null | Application type; only "web" is accepted |
runtime | string | Yes | — | Execution environment; see Supported runtimes |
command | string | list | Conditional | — | Startup command; optional for cron-only and static-only deployments |
env | map | No | {} | Non-secret environment variables |
db | object | Yes | — | Database and local SQLite policy |
crons | list | Conditional | [] | Scheduled jobs; required if there is no command or static asset deployment |
storage | object | No | {} | Compatibility block that accepts only the deprecated disk_id field |
assets | object | No | null | Static asset delivery configuration |
secrets | object | No | {} | Secret declarations and requirements |
auth | object | No | null | Endpoints that bypass interactive Keelson authentication |
email | object | No | {} | Inbound email configuration |
verify | string | list | No | [] | Extra paths checked after a deployment becomes healthy |
region | string | No | null | Logical 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-appRules:
- 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, andwww.
public_slug is a deprecated compatibility field. It is ignored because the
public URL is derived from the application host.
description
Section titled “description”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: webThe 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: webcannot be combined withcrons.- Omitting
typeis appropriate for ordinary application and cron-only deployments.
runtime
Section titled “runtime”The execution environment. A runtime is required even for a static-only deployment because Keelson still uses it during the build.
Supported runtimes
Section titled “Supported runtimes”| Runtime | Language | Intended use |
|---|---|---|
python-slim | Python | Lightweight applications, APIs, and text processing |
python-media | Python | Image and video processing workloads |
node-slim | Node.js | Lightweight Node.js applications |
node-media | Node.js | Media-processing Node.js applications |
go-slim | Go | Lightweight Go applications |
go-media | Go | Media-processing Go applications |
Start with a -slim runtime unless the application needs the extra media
libraries included in a -media runtime.
Installing dependencies
Section titled “Installing dependencies”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.
| Runtime | Detected files | Build step |
|---|---|---|
python-* | requirements.txt | python -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.json | npm ci when a lockfile is present, or npm install, followed by npm run build --if-present |
go-* | go.mod / go.sum | go mod download, then go build -o /workspace/app . (start it with command: "./app") |
Build constraints:
- An installable Python
pyproject.tomlmust be accompanied by a recognized lockfile:requirements.txt,poetry.lock,uv.lock, orPipfile.lock. - Node builds use npm and require
package-lock.json. If onlypnpm-lock.yamloryarn.lockis present, the pre-deploy check stops withlockfile_unsupported. Those files may remain alongsidepackage-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+sshdependencies, and authenticated installs fail. - Go builds use
CGO_ENABLED=0. - The build target is
linux/amd64only. - Secrets are not available during the build.
- The build execution time limit is 600 seconds.
command
Section titled “command”The command executed when the application starts. Use either a shell string or an argument list.
# String form: executed through a shellcommand: "python app.py"
# List form: executed directlycommand: - python - app.pyRules:
- At least one of
command,crons, or a staticassetsdeployment must be present. - A cron-only application does not need a top-level
command. - A static-only
type: webapplication withassetsdoes not need a top-levelcommand. - 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.
Environment variable key constraints
Section titled “Environment variable key constraints”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| Field | Type | Required | Default | Description |
|---|---|---|---|---|
mode | string | Yes | — | libsql or none; legacy turso is normalized to libsql |
migrate | string | No | null | Migration command run during deployment; valid only with libsql |
auto_adopt | boolean | No | false | Compatibility option for adopting an existing managed database |
local_sqlite | object | No | null | Explicit declaration of regenerable, ephemeral local SQLite files |
Database modes
Section titled “Database modes”mode | Meaning |
|---|---|
libsql | Keelson provisions an isolated managed database and injects KEELSON_DB_URL and KEELSON_DB_AUTH_TOKEN |
none | Keelson 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.
Database migrations
Section titled “Database migrations”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.
Ephemeral local SQLite
Section titled “Ephemeral local SQLite”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"| Field | Type | Required | Constraint |
|---|---|---|---|
policy | string | Yes | Must be ephemeral |
paths | list of strings | Yes | Non-empty; each entry must be under /tmp/ or be :memory: |
reason | string | Yes | Non-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| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | — | Unique job name, 1–63 lowercase letters, digits, or hyphens |
schedule | string | Yes | — | Valid five-field cron expression |
command | string | list | Yes | — | Command to execute |
timeout | integer | No | 300 | Timeout in seconds, from 1 through 600 |
enabled | boolean | No | true | Whether 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: webandcronscannot be combined.- A job runs in a different instance from the web service, so local files such
as
/dataare 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.
Cron expression examples
Section titled “Cron expression examples”| Expression | Meaning |
|---|---|
* * * * * | Every minute |
0 * * * * | At minute 0 of every hour |
0 3 * * * | Every day at 03:00 |
0 0 * * 1 | Every Monday at 00:00 |
See Scheduled jobs for execution behavior, examples, and troubleshooting.
workers (not supported)
Section titled “workers (not supported)”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 rejectedworkers: - name: drain command: "python worker.py" every: 10m
# Valid: use cronscrons: - name: drain schedule: "*/5 * * * *" command: "python drain.py" timeout: 120storage
Section titled “storage”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| Field | Type | Required | Default | Description |
|---|---|---|---|---|
disk_id | string | No | null | Deprecated and ignored identifier; 1–32 lowercase letters, digits, or hyphens |
assets
Section titled “assets”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| Field | Type | Required | Default | Description |
|---|---|---|---|---|
dir | string | Yes | — | Asset directory relative to the project root |
static_dir | string | No | null | Deprecated alias of dir; if both are present, they must match |
fallback | string | Conditional | null | Relative fallback file for an SPA, such as index.html |
api | string | No | null | Path 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.
Browser caching
Section titled “Browser caching”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:
dircannot contain a..path component.fallbackmust be a relative path underassets.dir.apimust begin with/, cannot be/, and cannot be under/__keelson.apiis valid only when a top-levelcommandprovides a backend.- A hybrid deployment with both
assetsandcommandrequiresfallback.
secrets
Section titled “secrets”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."Secret definitions
Section titled “Secret definitions”| Field | Type | Required | Default | Description |
|---|---|---|---|---|
items | list | No | [] | Secret definitions |
items[].name | string | Yes | — | Non-empty secret name; KEELSON_ is reserved |
items[].description | string | No | "" | Human-readable purpose of the secret |
required | list | No | [] | 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: - GETauth.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: trueemail.inbound.enabled is a boolean and defaults to false.
verify
Section titled “verify”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 - /settingsUse 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 inverify; 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.
region
Section titled “region”Optionally selects the placement region when creating a new application.
region: us-oregonUse a Keelson logical region key, not a cloud-provider region identifier:
| Key | Display name |
|---|---|
jp-tokyo | Japan |
us-oregon | US 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 modes
Section titled “Deployment modes”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.
| Name | Raw deploy_mode | command | assets | fallback | Description |
|---|---|---|---|---|---|
| Web application | container | Present | Absent | — | Ordinary application deployment |
| Static site | edge-static | Absent | Present | Absent | Static files only |
| SPA | edge-spa | Absent | Present | Present | Static files with a fallback |
| Hybrid | hybrid | Present | Present | Required | Static files plus a backend API |
Cross-field validation
Section titled “Cross-field validation”Some fields are valid only in specific combinations.
| Rule | Requirement |
|---|---|
| An executable surface is required | Supply command, crons, or static assets |
type: web excludes scheduled jobs | Do not combine type: web with crons |
workers is retired | Replace every workers declaration with an appropriate crons design |
| Managed migration requires managed data | db.migrate requires db.mode: libsql |
| Local SQLite is disposable | db.local_sqlite.paths accepts only /tmp/ paths and :memory: |
assets.api requires a backend | Set a top-level command before using an API prefix |
| Hybrid deployments require a fallback | Set assets.fallback when both assets and command are present |
Complete examples
Section titled “Complete examples”Web application with Managed SQLite
Section titled “Web application with Managed SQLite”slug: flask-cruddescription: "A CRUD application backed by Managed SQLite."runtime: python-slimcommand: "python app.py"db: mode: libsql migrate: "python -m alembic upgrade head"env: PYTHONUNBUFFERED: "1"Static SPA
Section titled “Static SPA”slug: marketing-sitetype: webruntime: node-slimdb: mode: noneassets: dir: dist fallback: index.htmlScheduled jobs only
Section titled “Scheduled jobs only”slug: cron-loggerruntime: python-slimdb: mode: noneenv: PYTHONUNBUFFERED: "1"crons: - name: heartbeat schedule: "* * * * *" command: "python heartbeat.py" timeout: 30Web application with a scheduled drain
Section titled “Web application with a scheduled drain”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-drainerruntime: python-slimcommand: "python app.py"db: mode: libsqlenv: PYTHONUNBUFFERED: "1"crons: - name: drain schedule: "*/5 * * * *" command: "python drain.py" timeout: 120Hybrid static files and backend API
Section titled “Hybrid static files and backend API”slug: photo-galleriesruntime: python-slimcommand: "python app.py"db: mode: libsqlenv: PYTHONUNBUFFERED: "1"assets: dir: static fallback: index.html api: /apiGo application
Section titled “Go application”slug: my-go-appruntime: go-slimcommand: "./app"db: mode: noneKeelson builds the Go source for Linux and places the binary at ./app during
deployment.
Node.js application
Section titled “Node.js application”slug: node-crudruntime: node-slimcommand: "npm start"db: mode: libsqlenv: NODE_ENV: "production"Common validation errors
Section titled “Common validation errors”| Cause | Resolution |
|---|---|
db or db.mode is missing | Declare db.mode as libsql or none |
The retired databases key is present | Remove 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 present | Add at least one executable surface |
workers is declared | Replace it with crons; workers are rejected with workers_not_supported |
crons[].timeout is greater than 600 | Reduce the timeout to the platform ceiling or lower plan limit |
An env value is unquoted | Quote every value, including booleans and numbers |
An environment variable starts with KEELSON_ | Rename it; that namespace is platform-owned |
A hybrid deployment omits assets.fallback | Set 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 |