Appearance
keel.yml Reference
Every field of the config file, from internal/config/config.go and its validators. Validation reports all problems at once. Optional sections (database, cache, domain, waf, resources, environments) can be omitted entirely.
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
| Key | Type | Required | Default | Validation |
|---|---|---|---|---|
name | string | yes | — | RFC 1123 hostname |
region | string | yes | us-east-1 | non-empty |
mode | enum | no | advanced | basic | advanced | custom |
services | map | yes | — | at least one; names match ^[a-z][a-z0-9_-]*$, no collisions after -→_ normalization |
resources | map | no | — | see below |
database | section | no | — | see below |
cache | section | no | — | see below |
vpc | section | no | see below | |
networking.public_tasks | bool? | no | from mode | pointer: unset ≠ false |
load_balancer.enabled | bool? | no | from mode | pointer: unset ≠ false |
domain | section | no | — | see below |
waf | section | no | — | see below |
pipeline | section | yes | — | see below |
release | section | no | — | see below |
environment | map[string]string | no | — | merged into every service; service-level wins |
secrets | list of string | no | — | added to every service (union by name) |
logs.retention_days | int | no | 14 | a 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 |
tags | map[string]string | no | — | 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 |
settings | section | no | both false | verbose, cautious |
environments | map | no | — | see below |
default_environment | string | no | — | required (or resolvable) when multiple environments are declared |
Mode defaults
| Mode | load_balancer.enabled | networking.public_tasks | vpc.nat_gateway |
|---|---|---|---|
basic | false | true | none |
advanced (default) | true | false | single |
custom | true | false | single |
Explicit settings always win over the mode default.
services.<name>
| Key | Type | Required | Default | Validation |
|---|---|---|---|---|
type | enum | yes | — | web | worker | scheduled |
port | int | web only | — | web services must specify a port |
health_check | string or block | web only | — | web services must specify a path; timeout < interval |
dockerfile | string | no | Dockerfile | — |
command | string or list | no | image CMD | argv, not shell; scalar split on whitespace honoring quotes; shell metacharacters (| & ; < > ( ) $ \``) rejected — use ["sh", "-c", "..."]` |
cpu | int | no | 256 | 256 | 512 | 1024 | 2048 | 4096 |
memory | int | no | 512 | required |
desired_count | int | no | 1 | — |
autoscaling | block | no | — | min ≥ 1, max > min, target_cpu 1–100 |
environment | map | no | — | wins over app-level |
secrets | list | no | — | union with app-level |
bindings | list | no | — | see below |
permissions | list | no | — | each needs actions and resources; effect defaults to Allow |
Only one type: web service may exist — path-based routing is not implemented, so a second one is rejected at validation.
scheduled is not fully implemented
Validation accepts type: scheduled, but no schedule is provisioned yet — the service runs continuously like a worker.
health_check block
| Key | Default | Range |
|---|---|---|
path | / | required for web |
grace_period | 60 | 0+ seconds before ECS may replace a new task |
interval | 30 | 5–300 |
timeout | 5 | 2–120; must be < interval |
healthy_threshold | 3 | 2–10 |
unhealthy_threshold | 3 | 2–10 |
matcher | "200-299" | HTTP code range |
deregistration_delay | 30 | 0–3600 |
vpc
| Key | Default | Validation |
|---|---|---|
cidr | 10.0.0.0/16 | — |
availability_zones | 2 | 2 or 3 |
nat_gateway | single (none in basic mode) | single | per_az | none |
private_aws_endpoints | false | nat_gateway: none with private tasks requires this to be true |
database
| Key | Required | Default | Validation |
|---|---|---|---|
engine | yes | — | postgres | mysql | aurora-postgres | aurora-mysql |
version | yes | — | non-empty |
instance | yes | — | non-empty |
name | no | keeldb | ^[A-Za-z][A-Za-z0-9_]*$ |
username | no | keeluser | same pattern; not a reserved name (admin, root, postgres, …) |
storage | yes | 20 | ≥ 20 |
multi_az | no | false | pointer: an environment override that omits it inherits the base |
retain | no | true | deletion protection + guaranteed final snapshot |
backup_retention_days | no | 7 | 0–35; 0 requires retain: false |
backup_window | no | AWS chooses | e.g. "04:00-05:00" |
cache
| Key | Required | Default | Validation |
|---|---|---|---|
engine | yes | — | valkey | redis |
version | no | — | — |
node_type | yes | — | non-empty |
num_nodes | no | 1 | — |
The endpoint is TLS-only; use the injected CACHE_URL / REDIS_URL (rediss://).
domain
| Key | Required | Validation |
|---|---|---|
name | yes | FQDN |
zone | yes | FQDN |
Requires pipeline.source to be set and a load balancer (the DNS alias record points to the ALB).
waf
| Key | Default |
|---|---|
enabled | false |
managed_rules | — (CLI default when enabling: AWSManagedRulesCommonRuleSet) |
Requires a load balancer.
pipeline
| Key | Required | Default | Validation |
|---|---|---|---|
source | yes | — | github | codecommit |
repo | github: yes; codecommit: no | — | see accepted forms below |
branch | no | main | — |
connection_arn | no | resolved from SSM | github only; a CodeConnections connection ARN |
build_image | no | aws/codebuild/amazonlinux-x86_64-standard:5.0 | an 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| Key | Required | Default | Validation |
|---|---|---|---|
command | yes (when block present) | — | same argv rules as service command |
service | no | the sole web service, or the sole service | must name a service |
timeout | no | 15m | positive Go duration |
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 baseCapabilities 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, services, resources, database, cache, domain, release, environment, secrets, tags. Declaring account opts the environment into account isolation.
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
| Concept | Value |
|---|---|
| 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.