Skip to content

keel.yml Reference ​

Reference for every keel.yml field, type, default, and validation rule. Keel reports all validation errors in one run. Optional sections such as database, cache, domain, waf, resources, and environments may be omitted.

keel.yml.example

keel init writes an annotated keel.yml.example alongside your config — every option Keel understands, with notes, kept as a sidecar because Keel's own edits to keel.yml round-trip through the YAML parser. A test suite loads and validates the example, and a reflection test fails if any config field is missing from it, so it cannot drift from this reference.

Top level ​

KeyTypeRequiredDefaultValidation
namestringyes—RFC 1123 hostname
regionstringyesus-east-1non-empty
platformstringno—shared-platform name; an app owns its VPC and cluster when omitted
servicesmapat least one of services/sites—names match ^[a-z][a-z0-9_-]*$, no collisions after -→_ normalization
sitesmapat least one of services/sites—see below
monitoringsectionno—see below
capacitysectionnoFargate onlysee below
resourcesmapno—see below
databasesectionno—see below
cachesectionno—see below
vpcsectionnosee below
networkingsectionnosee below
load_balancersectionnoenabledenabled, idle_timeout
domainsectionno—see below
wafsectionno—see below
pipelinesectionyes—see below
releasesectionno—see below
customsectionno—directory of user-authored OpenTofu files plus outputs injected as environment variables
alertssectionno—CloudWatch application alarms (Pro)
auditsectionno—see below (Pro)
idpsectionno—see below (Pro)
previewsectionnodisabledsee below (Pro)
environmentmap[string]stringno—merged into every service; service-level wins
secretslist of stringno—added to every service (union by name)
logs.retention_daysintno14a CloudWatch-accepted value (1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288, 3653) or -1 for never
tagsmap[string]stringno—applied to every managed AWS resource (ManagedBy=keel is never overridable). Keys that differ only in case — from Keel's App/Environment/ManagedBy tags or from each other — are rejected: IAM treats tag keys case-insensitively
settingssectionnoboth falseverbose, cautious
environmentsmapno—see below
default_environmentstringno—required (or resolvable) when multiple environments are declared

Networking defaults ​

The former mode preset has been removed. Its three decisions are explicit and independently overridable: load_balancer.enabled defaults to true, networking.public_tasks to false, and networking.nat_gateway to single. The inexpensive arrangement is:

yaml
load_balancer:
  enabled: false
networking:
  public_tasks: true
  nat_gateway: none

services.<name> ​

KeyTypeRequiredDefaultValidation
typeenumyes—web | worker | scheduled
portintweb only—web services must specify a port; rejected on scheduled
health_checkstring or blockweb only—web services must specify a path; timeout < interval; rejected on scheduled
dockerfilestringnoDockerfile—
commandstring or listnoimage CMDargv, not shell; scalar split on whitespace honoring quotes; shell metacharacters (| & ; < > ( ) $ \``) rejected — use ["sh", "-c", "..."]. With buildpacks, names a Procfile process type (["web"]`)
cpuintno256≥ 128; Fargate services must use a supported Fargate CPU/memory pairing, while EC2 services must fit the selected instances
memoryintno512> 0; must satisfy the selected fleet's constraints
desired_countintno1rejected on scheduled; ignored when autoscaling is set
autoscalingblockno—see below; rejected on scheduled
visibilityenumnopublicpublic | internal; web services only
authblockno—idp: true or an oidc block, plus optional unauthenticated_paths; internal web services only
capacityblocknoFargate on demandfleet: fargate | ec2, spot 0–100, on_demand ≥ 0
networking.public_tasksboolnoenvironment defaultper-service subnet placement; must agree with an EC2 fleet's placement
gpuintno—≥ 1; requires an EC2 fleet of GPU instances
stop_timeoutintnoECS default, or 120 with Spot0–120 seconds
scheduleblockscheduled only—see below; rejected on any other type
environmentmapno—wins over app-level
secretslistno—union with app-level
bindingslistno—see below
permissionslistno—each needs actions and resources; effect defaults to Allow

Only one public web service may use the root route. Internal services use service-specific hostnames and can share the internal load balancer.

autoscaling block ​

KeyRequiredDefaultValidation
minyes—≥ 0; 0 requires target_queue so the service can wake up
maxyes—> min
target_cpuno—1–100 (% average CPU)
target_memoryno—1–100 (% average memory)
target_requestsno—≥ 1; ALB requests per target per minute — web services behind an enabled load balancer only
target_queue.resourcewith queue target—a declared sqs resource
target_queue.backlog_per_taskwith queue target—≥ 1 visible messages per running task
scale_in_cooldownno3000–3600 seconds; explicit 0 is valid
scale_out_cooldownno600–3600 seconds

At least one target is required. When multiple targets are configured, each calculates a desired count and ECS uses the largest. Queue depth is the only signal available while no task is running, so it is also the only target that supports min: 0. Manage settings with keel autoscale show/set/off; set accepts --queue and --backlog-per-task.

schedule block ​

For type: scheduled services — a task definition run by EventBridge Scheduler, with no ECS service behind it. See the Scheduled Tasks guide.

KeyRequiredDefaultValidation
expressionyes—EventBridge syntax: cron(...) (six fields; ? required for the unused day field; day-of-week 1–7 with 1 = Sunday), rate(n unit) (n ≥ 1), or at(yyyy-mm-ddThh:mm:ss)
timezonenoUTCan IANA zone name, validated against embedded tzdata
enablednotruefalse creates the schedule DISABLED (PAUSED in the dashboard)
retriesno30–185; retries a failed invocation, not a non-zero exit
dead_letterno—must name a resources entry of type: sqs

health_check block ​

KeyDefaultRange
path/required for web
grace_period600+ seconds before ECS may replace a new task
interval305–300
timeout52–120; must be < interval
healthy_threshold32–10
unhealthy_threshold32–10
matcher"200-299"HTTP code range
deregistration_delay300–3600

sites.<name> ​

Static sites: a private S3 bucket served through CloudFront. See the Static Sites guide. Names match ^[a-z][a-z0-9-]*$ (no _ — the name becomes part of a bucket name) and must not collide with a service name.

sites and services are independent: declare either, or both. An app that declares no services gets no VPC, cluster, or ALB, and may not then declare a database, cache, or waf (each needs the VPC's subnets). It may still declare a pipeline.source, which buys a CodeBuild site build without buying any of the networking.

KeyRequiredDefaultValidation
rootyes—the directory to upload; no default on purpose
buildno—argv command that produces root; a site of committed files needs none
build_innocodebuild when pipeline.source is set, else localcodebuild | local. codebuild requires pipeline.source, and root must then be inside the repository
spanofalsemap 403 and 404 to the index document with a 200 — required for client-side routing
index_documentnoindex.html—
error_documentnoindex.html—
cdnyes—the block must be present: the bucket is private and served only through CloudFront
cdn.enablednotruefalse takes the site down (the bucket survives)
cdn.domainno*.cloudfront.net namemust sit inside the top-level domain zone; certificate issued in us-east-1
cdn.certificate_issuednofalsedomain.dns: external only; written by keel sites verify
cdn.price_classnoPriceClass_100PriceClass_100 | PriceClass_200 | PriceClass_All
cdn.compressnotrue—
cdn.default_ttlno864000–31536000 seconds

monitoring ​

KeyDefaultValues
container_insightsenableddisabled | enabled | enhanced

enabled gives cluster- and service-level metrics; per-task figures are read from the performance logs via a Logs Insights query (billed per GB scanned). enhanced publishes real per-task metrics (billed per metric per month — grows with task count). disabled means no live utilization anywhere; gauges report unknown.

vpc ​

KeyDefaultValidation
cidr10.0.0.0/16—
availability_zones22 or 3

networking ​

KeyDefaultValidation
public_tasksfalseenvironment default; individual services may override it
nat_gatewaysinglesingle | per_az | none
private_aws_endpointsfalsenat_gateway: none with private tasks requires this to be true
endpoint_azsallall | single

database ​

KeyRequiredDefaultValidation
engineyes—postgres | mysql | aurora-postgres | aurora-mysql
versionyes—non-empty
instanceyes—non-empty
namenokeeldb^[A-Za-z][A-Za-z0-9_]*$
usernamenokeelusersame pattern; not a reserved name (admin, root, postgres, …)
storageyes20≥ 20
multi_aznofalsepointer: an environment override that omits it inherits the base
retainnotruedeletion protection + guaranteed final snapshot
backup_retention_daysno70–35; 0 requires retain: false
backup_windownoAWS choosese.g. "04:00-05:00"
log_exportsno[] (none)postgres engines: postgresql, upgrade; mysql engines: error, general, slowquery, audit. Nothing is exported by default — CloudWatch ingestion is billed per GB. Required for keel db logs
iam_authnofalseenables per-person database roles managed by keel db grant/revoke (Pro)

cache ​

KeyRequiredDefaultValidation
engineyes—valkey | redis
versionno——
node_typeyes—non-empty
num_nodesno1—

The endpoint is TLS-only; use the injected CACHE_URL / REDIS_URL (rediss://).

domain ​

See the Custom Domains guide for the three DNS modes.

KeyRequiredDefaultValidation
nameyes—FQDN
dnsnoroute53route53 | route53-managed | external
zoneroute53 modesfor route53-managed, the domain itselfFQDN; ignored for external
zone_idno—pins the zone when a private zone shares its name; not valid with route53-managed
aliasesno—extra names on the certificate; FQDNs or wildcards (*.example.com); every name must sit inside the zone. Wildcards get no alias record — covered by the cert, deliberately not routed
certificate_arnno—use an existing ACM certificate (must be in the ALB's region); Keel then issues nothing
certificate_issuednofalseexternal only, and only when Keel issues the certificate; written by keel domains verify
force_httpsnotrue once TLS is readynever redirects to a listener that doesn't exist yet — a pending certificate can't take the app offline
validation_timeoutno45m (10m for a Keel-created zone)positive Go duration, e.g. 20m

With services, a domain requires a load balancer (the alias record points at the ALB). A sites-only app may declare a domain too — it names the zone that a site's CloudFront alias goes into.

waf ​

KeyDefault
enabledfalse
managed_rules— (CLI default when enabling: AWSManagedRulesCommonRuleSet)

Requires a load balancer.

capacity ​

Fargate and Fargate Spot are available on every cluster and need no top-level declaration. capacity.ec2 adds an Auto Scaling group behind an ECS capacity provider; services.<name>.capacity.fleet: ec2 selects it.

KeyRequiredDefaultValidation
ec2.instance_typesyes—one or more compatible instance types; all x86_64 and either all GPU or all non-GPU
ec2.spotno00–100 percent above the on-demand base
ec2.on_demandno0≥ 0 instances always bought on demand
ec2.minno0≥ 0; zero lets ECS managed scaling take an idle fleet down completely
ec2.maxyes—≥ 1
ec2.publicnofollows networking.public_tasksplaces instances, not task ENIs, in public subnets
ec2.network_modenoawsvpcawsvpc | bridge

bridge gives tasks the instance's network address and avoids one ENI per task, but every task on the host shares the instance security group. It is refused for a shared-platform tenant because that would collapse network identity across applications. A shared platform may own the EC2 fleet itself; tenants then select fleet: ec2 without declaring top-level capacity.

alerts ​

The block creates CloudWatch alarms and requires email, sns_topic_arn, or both. Threshold keys are optional: absent uses the listed default and 0 disables that alarm. response_time is off unless set.

KeyDefault
http_5xx1 percent
unhealthy_hosts1
response_timeoff; positive duration such as 2s
service_cpu, service_memory90 percent
tasks_runningtrue
database_cpu90 percent
database_storage_free10 percent
cache_cpu90 percent

audit ​

KeyDefaultValidation
exec.recordfalseenables cluster-level ECS Exec transcripts
exec.require_reasonfalsemakes keel exec --reason mandatory; may be used without recording
exec.retention_dayslogs.retention_daysCloudWatch retention value or -1; requires recording
exec.kms_key_idAWS-managed encryptionKMS key ARN or alias/…; requires recording

Recording belongs to the cluster and captures sessions opened outside Keel too. Transcripts live in /keel/exec/<app>-<env> and survive keel destroy. Use keel audit sessions and keel audit transcript to read them.

idp ​

This app-scoped IdP is for people signing in to an internal web service. It is separate from the account-level operator IdP described in Operators.

KeyDefaultValidation
enabledfalserequires at least one service with auth.idp: true
domain<app>-<environment>globally unique Cognito hosted-UI prefix
users[]email addresses; Cognito sends temporary passwords
groups[]created and included in the token; Keel does not enforce them

preview ​

KeyDefaultValidation
enabledfalse—
basedefault_environmentdeclared environment
prefixpv-marks names that may be synthesized
ttl72hpositive duration
load_balancerdisabled for previewssame fields as the top-level block
networkingpublic tasks, no NATsame fields as the top-level block
databasenonenone | shared | own
desired_count1≥ 1

A preview is named for its branch; a pull-request number is optional metadata. The older preview.mode preset is gone with top-level mode.

pipeline ​

KeyRequiredDefaultValidation
sourcewhen services are declared, or a site sets build_in: codebuild—github | codecommit
repogithub: yes; codecommit: no—see accepted forms below
branchnomain—
connection_arnnoresolved from SSMgithub only; a CodeConnections connection ARN
buildnoautoauto | docker | buildpack — auto probes for a Dockerfile; the chosen strategy is always printed (Build: <strategy> — <reason>)
buildernoheroku/builder:24buildpack path only; must be x86_64 (Fargate does no emulation); rejected alongside build: docker
pack_versionno0.36.4buildpack path only; pinned so a build is a function of the commit, not the day
build_imagenoaws/codebuild/amazonlinux-x86_64-standard:5.0an image whose name marks the wrong architecture for the CodeBuild environment (aarch64/arm64 vs x86_64/amd64) is rejected at validation; unmarked custom images are accepted

Accepted repo forms: owner/name, https://github.com/owner/name[.git], [email protected]:owner/name.git (github); a bare name or the full git-codecommit URL (codecommit). source: codecommit with repo unset means Keel creates and owns a repository named <app>-<env>.

release ​

Runs once per deploy, before any service takes traffic. Shorthand forms:

yaml
release: bundle exec rails db:migrate
release: ["sh", "-c", "rails db:migrate && rails db:seed"]
release:
  command: bundle exec rails db:migrate
  service: web
  timeout: 20m
  environment:
    MIGRATION_MODE: online
  secrets:
    - MIGRATOR_DATABASE_URL
KeyRequiredDefaultValidation
commandyes (when block present)—same argv rules as service command
servicenothe sole web service, or the sole servicemust name a service
timeoutno15mpositive Go duration
environmentno{}variables added only to the release task
secretsno[]secret names injected only into the release task

resources ​

Each entry declares exactly one of create (Keel owns it) or existing (Keel binds it). type: s3 | sns | sqs | rds | cache | iam-role. Only s3/sns/sqs can be created. retain defaults to true for managed resources.

create options: versioning (s3), encryption (s3, aws-managed), fifo (sns/sqs), visibility_timeout (sqs, 0–43200). A managed s3 resource name cannot contain _.

existing required fields by type: arn (s3/sns/iam-role); arn + url (sqs); endpoint + port + security_group_id (rds/cache). Optional: identifier, secret_arn, secret_env (must match ^[A-Z_][A-Z0-9_]*$), kms_key_arn.

Bindings ​

yaml
services:
  web:
    bindings:
      - resource: uploads
        access: [list, read, write]
        prefix: user-content   # S3 only
        env: UPLOADS           # override the injected variable base

Capabilities per type: s3 list, read, write, delete; sns publish; sqs send, consume; rds/cache connect; iam-role assume.

environments ​

Each entry overrides the base config; every field optional. Overridable: region, account, platform, load_balancer, networking, capacity, services, sites, resources, database, cache, domain, release, audit, environment, secrets, and tags. Declaring account opts the environment into account isolation. A site's cdn block merges field by field; certificate_issued is cleared when an environment overrides cdn.domain to a different hostname.

Merge rules: database/cache/domain and services.<name> merge field by field; environment and tags merge per key (override wins); secrets union by name; bindings merge by resource; permissions append; resources merge per name; release is replaced whole.

Selection precedence: --env → KEEL_ENV → keel env use → default_environment. With no environments: block, a single synthetic default environment is used.

Derived names ​

ConceptValue
Name prefix<app>-<env>
ECS cluster<app>-<env>-cluster
ECS service<app>-<env>-<service>
Log group/ecs/<app>-<env>
Config vars/keel/<app>/<env>/<key>
Database identifier<app>-<env>-db
Cache replication group<app>-<env>-cache
ECR repository<app>-<env> (shared; images tagged <service>-<sha>)
CodeBuild project<app>-<env>-build (per Dockerfile)
OpenTofu state key<app>/<env>/terraform.tfstate

Worked example ​

The Rails web+worker config that docs/deploying-rails.md walks through lives at internal/config/testdata/rails-web-worker.yml and is loaded, validated, and HCL-generated by the test suite — the docs cannot drift from it. See Deploying Rails for the annotated version.

Keel — the missing platform layer for AWS.