Development documentation may include features that are not in the latest release. Read the Lawn 0.8.1 guide.
Merging
Merging & Overrides
When a container starts, Lawn merges the template compose file with the instance override to produce the final configuration. The template defines the baseline. The instance override stores the user's customizations.
Within each service, Lawn applies these rules when both layers define a property:
| Property | Merge rule |
|---|---|
environment mappings | Merge by variable name; the later value wins. A null value removes the variable. |
ports | Merge by container port, published host port, and protocol. A matching entry is replaced; other entries remain. |
volumes | Merge by container path. A matching mount is replaced; other mounts remain. |
depends_on lists | Combine service names without duplicates. |
deploy | Merge nested mappings recursively; the later value wins on conflicts. |
| Other service properties | Use the later value, including whole mappings and lists. |
Merge example
The template defines this baseline:
services:
app:
image: nginx:1.28
environment: { KEEP: inherited, MODE: normal, REMOVE: legacy }
ports: ["8080:80", "8443:443"]
volumes: ["./data:/data", "./config:/config:ro"]
depends_on: [db]
deploy:
resources:
limits: { cpus: "1", memory: 256M }
healthcheck: { test: ["CMD", "nginx", "-t"], interval: 1m }
db:
image: postgres:17
cache:
image: redis:8
The instance override changes some entries and adds others:
services:
app:
environment: { MODE: fast, REMOVE: null }
ports: ["127.0.0.1:8080:80", "9090:80"]
volumes: ["./custom-config:/config:ro"]
depends_on: [cache]
deploy:
resources:
limits: { memory: 512M }
healthcheck: { test: ["CMD", "nginx", "-t"], timeout: 5s }
The merged result retains the inherited environment variable, 8443:443 port mapping, data mount, dependency, and CPU limit. The port's host IP does not form part of its identity, so 127.0.0.1:8080:80 replaces 8080:80. A different published port adds another entry. The healthcheck mapping is replaced entirely, so the inherited interval is absent:
services:
app:
image: nginx:1.28
environment: { KEEP: inherited, MODE: fast }
ports: ["127.0.0.1:8080:80", "8443:443", "9090:80"]
volumes: ["./data:/data", "./custom-config:/config:ro"]
depends_on: [db, cache]
deploy:
resources:
limits: { cpus: "1", memory: 512M }
healthcheck: { test: ["CMD", "nginx", "-t"], timeout: 5s }
db:
image: postgres:17
cache:
image: redis:8
Removing a Variable
Set a variable to null in the override to remove it from the merged result:
services:
app:
environment:
UNWANTED_VAR: null
Replacing Entire Values
Use the !override YAML tag to replace a value entirely instead of merging. Applied to the template above, this override leaves only "9090:8080" in ports:
services:
app:
ports: !override
- "9090:8080"
Use the !reset tag to remove a value entirely (the property becomes absent, rather than empty):
services:
app:
volumes: !reset
Variable Resolution
Template variables declared in x-lawn.config are resolved when needed. Stored values and defaults are substituted at container start using variable substitution syntax.
Config Option Layers
When a config variable uses selectable options, Lawn merges the selected option's compose overlay as an additional layer. The layers are applied bottom to top:
| Layer | Source |
|---|---|
| 3. User override | Instance customizations (always wins) |
| 2. Config option overlays | One layer per active selection |
| 1. Template | Base compose file from lawn-compose/ |
Lawn resolves config options recursively — if one option depends on another, the system iterates until the layer set stabilizes.
What Users Can Override
Through the Lawn UI or CLI, users can customize:
| Setting | How it's stored |
|---|---|
| Environment variables | services.<name>.environment entries |
| Port mappings | services.<name>.ports entries |
| Memory limits | services.<name>.deploy.resources.limits.memory |
| Dismissed notices | x-lawn.instance.dismissedNoticeIds |