Skip to content

fix: [#382] guard all service networks blocks in docker-compose template#384

Merged
josecelano merged 5 commits intomainfrom
382-fix-docker-compose-template-empty-networks-key
Feb 24, 2026
Merged

fix: [#382] guard all service networks blocks in docker-compose template#384
josecelano merged 5 commits intomainfrom
382-fix-docker-compose-template-empty-networks-key

Conversation

@josecelano
Copy link
Member

Summary

Fixes #382 — the docker-compose.yml.tera template rendered an invalid empty networks: key for the tracker service (and other services) when no optional services were enabled, causing docker compose up to fail with:

services.tracker.networks must be a list

This failure only surfaced at run time on the remote VM — after the full create → provision → configure → release workflow had already completed.

Changes

Fix (Phase 1)

Guard all service networks: blocks with {%- if <service>.networks | length > 0 %}, matching the pre-existing backup service pattern. Affected services: tracker, caddy, prometheus, grafana, mysql.

Tests (Phase 2)

Added unit tests in template.rs and local_validator.rs:

  • it_should_not_render_empty_networks_key_for_tracker_when_no_optional_services_are_configured
  • it_should_render_networks_key_for_tracker_when_prometheus_is_enabled
  • 4 tests for validate_docker_compose_file() (valid file passes, empty networks: key fails)

Local validation (Phase 3)

New module src/infrastructure/templating/docker_compose/local_validator.rs — runs docker compose config --quiet on the build directory immediately after both docker-compose.yml and .env are rendered. Any structural error in the generated file now fails fast at configure time with a clear, actionable error message.

The validator is called inside DockerComposeProjectGenerator::render() — the earliest point where the complete artifact exists, and still within the infrastructure layer (keeping the application layer free of process-spawning logic). This decision is documented in a new ADR.

Documentation (Phase 4)

  • Issue spec fully updated with progress and E2E verification results
  • New ADR: docs/decisions/docker-compose-local-validation-placement.md

Verification

Full E2E workflow run with a minimal config (SQLite, no domains, no Prometheus — the exact reproduction case from the issue):

create → provision → configure → release → run → test

All steps passed. The run command no longer fails with the previously broken configuration.

Pre-commit checks pass: ./scripts/pre-commit.sh
All linters pass: cargo run --bin linter all
2353 unit tests pass ✅

Fixes #382.

Any service with an empty networks list would render an invalid empty
`networks:` key, which Docker Compose rejects with:

    services.<name>.networks must be a list

The backup service already had the correct guard. This commit applies
the same defensive pattern to every remaining service block.

Changes in templates/docker-compose/docker-compose.yml.tera:
- tracker:    add `{%- if tracker.networks | length > 0 %}` guard
- caddy:      add `{%- if caddy.networks | length > 0 %}` guard
- prometheus: add `{%- if prometheus.networks | length > 0 %}` guard
- grafana:    add `{%- if grafana.networks | length > 0 %}` guard
- mysql:      add `{%- if mysql.networks | length > 0 %}` guard

All five service blocks now follow the same pattern as the pre-existing
backup service guard. The top-level `networks:` section was already
wrapped in `{%- if required_networks | length > 0 %}` and is unchanged.

Changes in template.rs:
- it_should_not_render_empty_networks_key_for_tracker_when_no_optional_services_are_configured
  Verifies a minimal config (SQLite, no Caddy/Prometheus/MySQL) produces
  no empty `networks:` key for the tracker service.
- it_should_render_networks_key_for_tracker_when_prometheus_is_enabled
  Verifies `networks:` is present and contains `metrics_network` when
  Prometheus is enabled.
Mark completed tasks:
- Phase 1: all service networks blocks guarded (tracker, caddy,
  prometheus, grafana, mysql)
- Phase 2: two unit tests added for minimal-config and Prometheus-enabled
  rendering scenarios
- Phase 4: no template doc changes needed

Update the affected-services table to show all services are now guarded.

Phase 3 (docker compose config --quiet validation at configure time)
remains open.
…ring

Add local_validator.rs with validate_docker_compose_file() which runs
`docker compose config --quiet` on the build directory immediately after
the docker-compose.yml template is rendered.

- New module: src/infrastructure/templating/docker_compose/local_validator.rs
  - validate_docker_compose_file(compose_dir: &Path) -> Result<()>
  - DockerComposeLocalValidationError with CommandExecutionFailed and
    InvalidDockerComposeFile variants, each with a help() message
  - 4 unit tests: valid file passes, empty networks key fails,
    help messages are non-empty
- DockerComposeProjectGeneratorError gains DockerComposeValidationFailed
  variant that wraps DockerComposeLocalValidationError
- validate_docker_compose_file() called in DockerComposeProjectGenerator::render()
  after both env and docker-compose.yml are written, before returning the build path

This provides fast-fail detection at configure time rather than waiting for
Docker to reject the file at run time on the remote VM.
Document the decision to call validate_docker_compose_file() inside
DockerComposeProjectGenerator::render() (infrastructure layer) rather
than in the application service or step layers.

Key rationale:
- The generator is the earliest point where the complete artifact
  (both docker-compose.yml and .env) exists, making validation accurate
- Keeps process-spawning (docker CLI) in the infrastructure layer,
  preserving DDD boundaries
- DockerComposeRenderer::render() alone is not sufficient because .env
  is not yet written at that point
…tion

Ran the full workflow (create → provision → configure → release → run → test)
with a minimal config (SQLite, no domains, no Prometheus) — the exact
configuration that previously failed with:

  services.tracker.networks must be a list

All steps completed successfully. The 'run' command now works with this
configuration. All goals and acceptance criteria are now complete.

Also added:
- Reference to the new ADR (docker-compose-local-validation-placement.md)
  in the Related Documentation section
- Reference to local_validator.rs in the Related Documentation section
@josecelano josecelano self-assigned this Feb 24, 2026
@josecelano
Copy link
Member Author

ACK d75b7cd

@josecelano josecelano merged commit 3bb2bf3 into main Feb 24, 2026
42 checks passed
josecelano added a commit that referenced this pull request Feb 24, 2026
PR #384 has been merged to main. The issue tracking file is no longer needed.
josecelano added a commit that referenced this pull request Mar 4, 2026
… not in PATH)

The local docker-compose.yml validator added in PR #384 runs
'docker compose config --quiet' on the host. When the deployer is invoked via
its Docker container (the standard usage), the docker binary is not installed
inside the container and the command fails with ENOENT, aborting the release.

Documents:
- Root cause: local validator assumes docker is in PATH, but the deployer
  container has no docker binary installed
- Fix applied: handle ErrorKind::NotFound gracefully (skip with warning)
- State recovery: how environment.json was manually reset from ReleaseFailed
  to Configured so release could be retried

Also adds 'ENOENT' to project-words.txt.

Refs #405
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Docker Compose template renders invalid empty networks: key for tracker service

1 participant