dwe render env
Generate .env content from the merged config. Output goes to stdout by default; pass --out <path> to write to a file (parent directories are created).
Contents
Section titled “Contents”- Pipeline
- System variables
- Export rules
- Value resolution
- Truthiness
- Output format
- Worked example
- Common pitfalls
- Related references
Pipeline
Section titled “Pipeline”render env does not iterate services or read template files from disk. It walks the ordered list of export rules in the merged config and emits one line per rule.
flowchart TD
M["Merged config"] --> SYS["Emit system vars<br/>PROJECT, UID, GID"]
SYS --> R{"For each rule in<br/>exports.env"}
R --> W["Evaluate when"]
W -- falsy --> R
W -- "truthy/absent" --> V["Resolve from<br/>via dot-path"]
V --> F["Format value<br/>by format hint"]
F --> O["Write line<br/>NAME=value"]
O --> R
R -- "end" --> OUT["stdout / file"]
The export rule list lives under exports.env in workspace/defaults.yml (see exports.env reference). The rule order in the YAML file determines the line order in the output.
System variables
Section titled “System variables”Three variables are always emitted before any rule, regardless of exports.env:
| Variable | Source | Notes |
|---|---|---|
PROJECT | project.name from workspace.yml | Used by Docker labels, Compose project name, and Make targets |
UID | host UID — 1000 on macOS, the real host UID on Linux/WSL | Hard-coded 1000 on macOS because Docker Desktop runs containers in a Linux VM where host UIDs do not map directly |
GID | host GID — same platform logic as UID | Same rationale as UID |
The names PROJECT, UID, and GID are reserved: any export rule that tries to use one of them as name is rejected at config-load time with a clear error. This applies to every command that loads the project config, not only dwe render env — so a typo is caught the first time you run any command after editing defaults.yml.
Export rules
Section titled “Export rules”Each rule maps a dot-path in the merged config to an env variable name. All per-service values — enabled, container, ports.<port-name>, hosts.<host-name> — are reachable under services.<name>.* regardless of type (app / tool / infra).
exports: env: - name: APP_PORT from: services.main.ports.http format: int
- name: APP_HOST from: services.main.hosts.web
- name: TOOL_ADMINER_ENABLED from: services.adminer.enabled format: bool
- name: ADMINER_PORT from: services.adminer.ports.http format: int when: services.adminer.enabled
- name: ADMINER_HOST from: services.adminer.hosts.web when: services.adminer.enabled
- name: APP_URL from: runtime.urls.app default: http://localhost comment: Public application URLRule fields
Section titled “Rule fields”| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Env variable name written to .env |
from | string | yes | Dot-path navigated against the merged config (e.g. services.main.ports.http, services.main.container) |
default | string | no | Fallback when from resolves to nothing or to a falsy string (see Value resolution) |
required | bool | no | If true and from is absent and default is empty, rendering fails with an error |
format | string | no | One of string (default), bool, int — controls how the resolved value is rendered |
when | string | no | Dot-path; the rule is skipped entirely when this path resolves to a falsy value |
comment | string | no | Written as # comment on the line above the variable |
Evaluation order
Section titled “Evaluation order”For each rule, in source order:
whengate — ifwhenis set, resolve its dot-path against the merged config. If the value is falsy, skip the rule entirely (no line emitted, no comment).- Resolve
from— fetch the value at the dot-path. - Pick value — see Value resolution.
- Required check — if the path was absent and no
defaultis set andrequired: true, fail with an error naming the missing path. - Comment — if
commentis set, emit# <comment>on its own line. - Emit — write
<name>=<value>.
Value resolution
Section titled “Value resolution”The picked value depends on three things: whether from resolved, the format hint, and the truthiness of the resolved value.
flowchart TD
A{"from resolves?"} -- no --> R{"required AND<br/>default empty?"}
R -- yes --> ERR["error"]
R -- no --> D["use default"]
A -- yes --> F{"format is bool<br/>or int?"}
F -- yes --> USE["use resolved value<br/>format applied"]
F -- no --> T{"resolved value truthy?"}
T -- yes --> USE
T -- no --> D
The asymmetry matters:
format: boolandformat: intalways use the resolved value, even when it isfalseor0. This guarantees thatTOOL_ADMINER=falseandPORT=0survive through to.envinstead of silently falling back to a default.format: string(the default) treats falsy resolved values as “not really set” and falls back todefault. So a YAML""atruntime.urls.appfalls through todefault: http://localhost.
format shapes the output:
| Format | Behavior |
|---|---|
string (default) | Stringify the resolved value as-is. |
bool | A boolean value is rendered as the literal true or false. Other types fall back to a plain stringification. |
int | The resolved number is stringified directly (YAML numbers are already int-like). |
Truthiness
Section titled “Truthiness”The same truthiness rule applies to both when and the string-format fallback:
| Value | Truthy? |
|---|---|
| absent path | no |
false | no |
0 (any numeric type) | no |
"" | no |
"false" | no |
"0" | no |
| anything else | yes |
Example: when: services.adminer.enabled skips the rule whenever the service is unset, explicitly false, or a string "false" / "0".
Dot-path syntax note: Export rule from: / when: fields use bare dot-paths into the merged config, not the {{ ... }} template syntax. Per-service values live under services.<name>.* for every type — e.g. from: services.adminer.ports.http, from: services.mailpit.hosts.web, from: services.main.container, when: services.adminer.enabled.
Output format
Section titled “Output format”# Generated by dwe — do not edit manually
PROJECT=<project.name>UID=<host UID or 1000>GID=<host GID or 1000># <comment from rule, if any><NAME>=<value>...A blank line follows the header banner, then system variables, then rules.
When --out <path> is supplied:
- The path is interpreted relative to the current working directory, not the project root. Pass an absolute path if you want a deterministic location regardless of where the command is invoked from (for example, when running with
-c /path/to/workspace.ymlfrom a different directory). - Missing parent directories are created.
- The full content replaces any existing file (no merging, no comments preserved).
Worked example
Section titled “Worked example”workspace/services/main/service.yml:
type: appcontainer: app-mainrequired: truedir: ./services/mainports: http: 8080workspace/services/adminer/service.yml:
type: toolcontainer: adminerports: http: 8027workspace/defaults.yml:
services: adminer: enabled: trueruntime: urls: app: ""exports: env: - name: APP_PORT from: services.main.ports.http format: int - name: APP_URL from: runtime.urls.app default: http://localhost - name: TOOL_ADMINER from: services.adminer.enabled format: bool when: services.adminer.enabled - name: TOOL_REDIS from: services.redis_insight.enabled format: bool when: services.redis_insight.enabledworkspace.yml:
project: name: demodwe render env on macOS produces:
# Generated by dwe — do not edit manually
PROJECT=demoUID=1000GID=1000APP_PORT=8080APP_URL=http://localhostTOOL_ADMINER=trueWalk-through:
APP_PORT—format: int, value8080, emitted directly.APP_URL—fromresolves to empty string (falsy underformat: string), sodefaultis used.TOOL_ADMINER—whenresolves truthy, valuetruerendered as literaltrue.TOOL_REDIS—whenresolves to absent (noredis_insightentry), rule skipped, no line emitted.
Common pitfalls
Section titled “Common pitfalls”format: stringswallowsfalse/0/""— if you need a literalfalseor0in the output, pickformat: boolorformat: int. Otherwise the value silently falls through todefault(or to an empty string).whenandfromare independent dot-paths —whendoes not have to point at the same key asfrom. Use it to gate one variable on another setting (e.g.from: services.second.container,when: services.second.enabled).required: truewithoutdefault— produces a hard error if the path is absent. Use it for variables your runtime cannot start without; otherwise rely ondefaultto keep the file complete.- Editing
.envby hand — the file is regenerated bydwe render env --out .envand by lifecycle hooks. Editworkspace/defaults.ymlexports orworkspace/local.ymloverrides instead. --outhas no short form. The flag is spelled--out; there is no-oalias. Usedwe render env --out .env.- Redeclaring
PROJECT/UID/GID— these names are reserved and validated at config load. An export rule that uses one of them produces a hard error when any command loads the project config; remove the rule and reach the same value via the system variable instead.
Related references
Section titled “Related references”exports.envrule schema — full field reference, formats- Dot-path resolution — how
fromandwhenpaths navigate the merged config - Run
dwe render env --helpfor the live CLI surface