Problem
deploy/deploy.py renders each app's config/*.template.* files on the CI runner (render_app_configs), but the rendered output isn't written into the release directory - it's pushed straight into the target host's persistent, non-versioned apps-data/{app}/config/ tree, in place:
```python
def deploy_to_host(host, archive_path, rendered_configs, apps, networks, config):
...
push_release(connection, archive_path, release_path) # release lands in releases/{timestamp}/
push_app_configs(connection, base_path, rendered_configs) # <- overwrites apps-data/{app}/config/*, NOT part of the release tar
...
connection.run(f"ln -sfn {shlex.quote(release_path)} {shlex.quote(current_path)}", hide=True) # atomic switch
```
This runs before the current symlink switch and mutates a directory that isn't tied to any specific release. Consequences:
- It breaks the atomicity guarantee AGENTS.md documents elsewhere ("a deploy either fully lands and switches
current as its last step, or fails leaving current untouched - never a partially-applied app"): if the run dies between push_app_configs and the symlink switch, apps-data now holds config for a release that never went live.
- Rendered config has no history. Each deploy overwrites the previous render in place, so there's nothing to fall back to even for apps whose
.env (correctly stored inside the release tar) IS versioned per release.
- It only works today because the affected apps' compose files hardcode the
apps-data mount path (e.g. apps/traefik/docker-compose.yml: ../../apps-data/${APP_NAME}/config/traefik.yml:/traefik.yml:ro) - a convention inherited unreflectively from the pre-Fabric up.sh model, where apps-data was always the render target because there was no versioned release tree to put it in.
Fix
Drop the config/ subdirectory entirely and rely on naming alone: any *.template.* file sitting directly next to an app's docker-compose.yml is a template. render_app_configs renders it in place, into a sibling file in the same directory (traefik.template.yml -> traefik.yml), on the runner, before the release is archived - so the rendered file travels inside the release tarball automatically, right alongside .env, with no separate push step and no special nesting needed.
Compose files mount the rendered file directly by its plain relative path, one line per file (./traefik.yml:/traefik.yml:ro) rather than mounting a whole directory - more explicit about which container path each file lands at. apps-data/{app}/ stays reserved strictly for state that must genuinely persist across releases and is never regenerated by a deploy (acme.json, database data directories, codecov's archive/) - never templated config.
push_app_configs and the rendered_configs dict threaded through resolve_app_envs/deploy_to_host/main go away entirely as part of this.
Related
Also flagged while reviewing this: AGENTS.md had no dedicated section explaining the templating mechanism itself - only scattered one-line mentions across several sections, one of which (the Field Ordering Rules example) didn't even match how any real app in the catalog did it. Added a proper "Config Templates" section covering the naming convention, rationale, and when to prefer a plain environment: var over a template.
Problem
deploy/deploy.pyrenders each app'sconfig/*.template.*files on the CI runner (render_app_configs), but the rendered output isn't written into the release directory - it's pushed straight into the target host's persistent, non-versionedapps-data/{app}/config/tree, in place:```python
def deploy_to_host(host, archive_path, rendered_configs, apps, networks, config):
...
push_release(connection, archive_path, release_path) # release lands in releases/{timestamp}/
push_app_configs(connection, base_path, rendered_configs) # <- overwrites apps-data/{app}/config/*, NOT part of the release tar
...
connection.run(f"ln -sfn {shlex.quote(release_path)} {shlex.quote(current_path)}", hide=True) # atomic switch
```
This runs before the
currentsymlink switch and mutates a directory that isn't tied to any specific release. Consequences:currentas its last step, or fails leavingcurrentuntouched - never a partially-applied app"): if the run dies betweenpush_app_configsand the symlink switch,apps-datanow holds config for a release that never went live..env(correctly stored inside the release tar) IS versioned per release.apps-datamount path (e.g.apps/traefik/docker-compose.yml:../../apps-data/${APP_NAME}/config/traefik.yml:/traefik.yml:ro) - a convention inherited unreflectively from the pre-Fabricup.shmodel, whereapps-datawas always the render target because there was no versioned release tree to put it in.Fix
Drop the
config/subdirectory entirely and rely on naming alone: any*.template.*file sitting directly next to an app'sdocker-compose.ymlis a template.render_app_configsrenders it in place, into a sibling file in the same directory (traefik.template.yml->traefik.yml), on the runner, before the release is archived - so the rendered file travels inside the release tarball automatically, right alongside.env, with no separate push step and no special nesting needed.Compose files mount the rendered file directly by its plain relative path, one line per file (
./traefik.yml:/traefik.yml:ro) rather than mounting a whole directory - more explicit about which container path each file lands at.apps-data/{app}/stays reserved strictly for state that must genuinely persist across releases and is never regenerated by a deploy (acme.json, database data directories, codecov'sarchive/) - never templated config.push_app_configsand therendered_configsdict threaded throughresolve_app_envs/deploy_to_host/maingo away entirely as part of this.Related
Also flagged while reviewing this: AGENTS.md had no dedicated section explaining the templating mechanism itself - only scattered one-line mentions across several sections, one of which (the Field Ordering Rules example) didn't even match how any real app in the catalog did it. Added a proper "Config Templates" section covering the naming convention, rationale, and when to prefer a plain
environment:var over a template.