compose-farm 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. compose_farm-0.1.0/.envrc +1 -0
  2. compose_farm-0.1.0/.github/release-drafter.yml +4 -0
  3. compose_farm-0.1.0/.github/renovate.json +35 -0
  4. compose_farm-0.1.0/.github/workflows/ci.yml +60 -0
  5. compose_farm-0.1.0/.github/workflows/release-drafter.yml +14 -0
  6. compose_farm-0.1.0/.github/workflows/release.yml +22 -0
  7. compose_farm-0.1.0/.github/workflows/renovate.json +35 -0
  8. compose_farm-0.1.0/.github/workflows/toc.yaml +10 -0
  9. compose_farm-0.1.0/.github/workflows/update-readme.yml +53 -0
  10. compose_farm-0.1.0/.gitignore +44 -0
  11. compose_farm-0.1.0/.pre-commit-config.yaml +27 -0
  12. compose_farm-0.1.0/.python-version +1 -0
  13. compose_farm-0.1.0/AGENTS.md +1 -0
  14. compose_farm-0.1.0/CLAUDE.md +47 -0
  15. compose_farm-0.1.0/GEMINI.md +1 -0
  16. compose_farm-0.1.0/PKG-INFO +196 -0
  17. compose_farm-0.1.0/PLAN.md +35 -0
  18. compose_farm-0.1.0/README.md +184 -0
  19. compose_farm-0.1.0/compose-farm.example.yaml +25 -0
  20. compose_farm-0.1.0/examples/README.md +42 -0
  21. compose_farm-0.1.0/examples/compose-farm.yaml +11 -0
  22. compose_farm-0.1.0/examples/hello/docker-compose.yml +4 -0
  23. compose_farm-0.1.0/examples/nginx/docker-compose.yml +6 -0
  24. compose_farm-0.1.0/pyproject.toml +114 -0
  25. compose_farm-0.1.0/src/compose_farm/__init__.py +9 -0
  26. compose_farm-0.1.0/src/compose_farm/_version.py +34 -0
  27. compose_farm-0.1.0/src/compose_farm/cli.py +247 -0
  28. compose_farm-0.1.0/src/compose_farm/config.py +93 -0
  29. compose_farm-0.1.0/src/compose_farm/logs.py +231 -0
  30. compose_farm-0.1.0/src/compose_farm/py.typed +0 -0
  31. compose_farm-0.1.0/src/compose_farm/ssh.py +208 -0
  32. compose_farm-0.1.0/src/compose_farm/traefik.py +479 -0
  33. compose_farm-0.1.0/tests/__init__.py +1 -0
  34. compose_farm-0.1.0/tests/test_config.py +143 -0
  35. compose_farm-0.1.0/tests/test_logs.py +82 -0
  36. compose_farm-0.1.0/tests/test_ssh.py +118 -0
  37. compose_farm-0.1.0/tests/test_traefik.py +195 -0
  38. compose_farm-0.1.0/uv.lock +943 -0
@@ -0,0 +1 @@
1
+ source .venv/bin/activate
@@ -0,0 +1,4 @@
1
+ template: |
2
+ ## What’s Changed
3
+
4
+ $CHANGES
@@ -0,0 +1,35 @@
1
+ {
2
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
+ "rebaseWhen": "behind-base-branch",
4
+ "dependencyDashboard": true,
5
+ "labels": [
6
+ "dependencies",
7
+ "no-stale"
8
+ ],
9
+ "commitMessagePrefix": "⬆️",
10
+ "commitMessageTopic": "{{depName}}",
11
+ "prBodyDefinitions": {
12
+ "Release": "yes"
13
+ },
14
+ "packageRules": [
15
+ {
16
+ "matchManagers": [
17
+ "github-actions"
18
+ ],
19
+ "addLabels": [
20
+ "github_actions"
21
+ ],
22
+ "rangeStrategy": "pin"
23
+ },
24
+ {
25
+ "matchManagers": [
26
+ "github-actions"
27
+ ],
28
+ "matchUpdateTypes": [
29
+ "minor",
30
+ "patch"
31
+ ],
32
+ "automerge": true
33
+ }
34
+ ]
35
+ }
@@ -0,0 +1,60 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ${{ matrix.os }}
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ os: [ubuntu-latest, macos-latest, windows-latest]
16
+ python-version: ["3.11", "3.12", "3.13"]
17
+
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+
21
+ - name: Install uv
22
+ uses: astral-sh/setup-uv@v6
23
+
24
+ - name: Set up Python ${{ matrix.python-version }}
25
+ run: uv python install ${{ matrix.python-version }}
26
+
27
+ - name: Install dependencies
28
+ run: uv sync --all-extras --dev
29
+
30
+ - name: Run tests
31
+ run: uv run pytest
32
+
33
+ - name: Upload coverage reports to Codecov
34
+ if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'
35
+ uses: codecov/codecov-action@v5
36
+ env:
37
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
38
+
39
+ lint:
40
+ runs-on: ubuntu-latest
41
+ steps:
42
+ - uses: actions/checkout@v4
43
+
44
+ - name: Install uv
45
+ uses: astral-sh/setup-uv@v6
46
+
47
+ - name: Set up Python
48
+ run: uv python install 3.12
49
+
50
+ - name: Install dependencies
51
+ run: uv sync --all-extras --dev
52
+
53
+ - name: Run ruff check
54
+ run: uv run ruff check .
55
+
56
+ - name: Run ruff format check
57
+ run: uv run ruff format --check .
58
+
59
+ - name: Run mypy
60
+ run: uv run mypy src/compose_farm
@@ -0,0 +1,14 @@
1
+ name: Release Drafter
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ update_release_draft:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: release-drafter/release-drafter@v6
13
+ env:
14
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,22 @@
1
+ name: Upload Python Package
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ deploy:
9
+ runs-on: ubuntu-latest
10
+ environment:
11
+ name: pypi
12
+ url: https://pypi.org/p/${{ github.repository }}
13
+ permissions:
14
+ id-token: write
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - name: Install uv
18
+ uses: astral-sh/setup-uv@v6
19
+ - name: Build
20
+ run: uv build
21
+ - name: Publish package distributions to PyPI
22
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,35 @@
1
+ {
2
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
+ "rebaseWhen": "behind-base-branch",
4
+ "dependencyDashboard": true,
5
+ "labels": [
6
+ "dependencies",
7
+ "no-stale"
8
+ ],
9
+ "commitMessagePrefix": "⬆️",
10
+ "commitMessageTopic": "{{depName}}",
11
+ "prBodyDefinitions": {
12
+ "Release": "yes"
13
+ },
14
+ "packageRules": [
15
+ {
16
+ "matchManagers": [
17
+ "github-actions"
18
+ ],
19
+ "addLabels": [
20
+ "github_actions"
21
+ ],
22
+ "rangeStrategy": "pin"
23
+ },
24
+ {
25
+ "matchManagers": [
26
+ "github-actions"
27
+ ],
28
+ "matchUpdateTypes": [
29
+ "minor",
30
+ "patch"
31
+ ],
32
+ "automerge": true
33
+ }
34
+ ]
35
+ }
@@ -0,0 +1,10 @@
1
+ on: push
2
+ name: TOC Generator
3
+ jobs:
4
+ generateTOC:
5
+ name: TOC Generator
6
+ runs-on: ubuntu-latest
7
+ steps:
8
+ - uses: technote-space/toc-generator@v4
9
+ with:
10
+ TOC_TITLE: ""
@@ -0,0 +1,53 @@
1
+ name: Update README.md
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+
9
+ jobs:
10
+ update_readme:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - name: Check out repository
14
+ uses: actions/checkout@v4
15
+ with:
16
+ persist-credentials: false
17
+ fetch-depth: 0
18
+
19
+ - name: Set up Python
20
+ uses: actions/setup-python@v5
21
+
22
+ - name: Install uv
23
+ uses: astral-sh/setup-uv@v6
24
+
25
+ - name: Run markdown-code-runner
26
+ env:
27
+ TERM: dumb
28
+ NO_COLOR: 1
29
+ TERMINAL_WIDTH: 90
30
+ run: |
31
+ uvx --with . markdown-code-runner README.md
32
+ sed -i 's/[[:space:]]*$//' README.md
33
+
34
+ - name: Commit updated README.md
35
+ id: commit
36
+ run: |
37
+ git add README.md
38
+ git config --local user.email "github-actions[bot]@users.noreply.github.com"
39
+ git config --local user.name "github-actions[bot]"
40
+ if git diff --quiet && git diff --staged --quiet; then
41
+ echo "No changes in README.md, skipping commit."
42
+ echo "commit_status=skipped" >> $GITHUB_ENV
43
+ else
44
+ git commit -m "Update README.md"
45
+ echo "commit_status=committed" >> $GITHUB_ENV
46
+ fi
47
+
48
+ - name: Push changes
49
+ if: env.commit_status == 'committed'
50
+ uses: ad-m/github-push-action@master
51
+ with:
52
+ github_token: ${{ secrets.GITHUB_TOKEN }}
53
+ branch: ${{ github.head_ref || github.ref_name }}
@@ -0,0 +1,44 @@
1
+ # Python
2
+ __pycache__/
3
+ src/compose_farm/_version.py
4
+ *.py[cod]
5
+ *$py.class
6
+ *.so
7
+ .Python
8
+ build/
9
+ develop-eggs/
10
+ dist/
11
+ downloads/
12
+ eggs/
13
+ .eggs/
14
+ lib/
15
+ lib64/
16
+ parts/
17
+ sdist/
18
+ var/
19
+ wheels/
20
+ *.egg-info/
21
+ .installed.cfg
22
+ *.egg
23
+
24
+ # Virtual environments
25
+ .venv/
26
+ venv/
27
+ ENV/
28
+
29
+ # IDE
30
+ .idea/
31
+ .vscode/
32
+ *.swp
33
+ *.swo
34
+ .DS_Store
35
+
36
+ # Testing
37
+ .coverage
38
+ .pytest_cache/
39
+ htmlcov/
40
+
41
+ # Local config (don't commit real configs)
42
+ compose-farm.yaml
43
+ !examples/compose-farm.yaml
44
+ coverage.xml
@@ -0,0 +1,27 @@
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v5.0.0
4
+ hooks:
5
+ - id: trailing-whitespace
6
+ - id: end-of-file-fixer
7
+ - id: check-yaml
8
+ - id: check-added-large-files
9
+ - id: check-merge-conflict
10
+ - id: debug-statements
11
+
12
+ - repo: https://github.com/astral-sh/ruff-pre-commit
13
+ rev: v0.8.4
14
+ hooks:
15
+ - id: ruff
16
+ args: [--fix]
17
+ - id: ruff-format
18
+
19
+ - repo: https://github.com/pre-commit/mirrors-mypy
20
+ rev: v1.14.0
21
+ hooks:
22
+ - id: mypy
23
+ additional_dependencies:
24
+ - pydantic>=2.0.0
25
+ - typer>=0.9.0
26
+ - asyncssh>=2.14.0
27
+ - types-PyYAML
@@ -0,0 +1 @@
1
+ 3.14
@@ -0,0 +1 @@
1
+ CLAUDE.md
@@ -0,0 +1,47 @@
1
+ # Compose Farm Development Guidelines
2
+
3
+ ## Core Principles
4
+
5
+ - **KISS**: Keep it simple. This is a thin wrapper around `docker compose` over SSH.
6
+ - **YAGNI**: Don't add features until they're needed. No orchestration, no service discovery, no health checks.
7
+ - **DRY**: Reuse patterns. Common CLI options are defined once, SSH logic is centralized.
8
+
9
+ ## Architecture
10
+
11
+ ```
12
+ compose_farm/
13
+ ├── config.py # Pydantic models, YAML loading
14
+ ├── ssh.py # asyncssh execution, streaming
15
+ └── cli.py # Typer commands
16
+ ```
17
+
18
+ ## Key Design Decisions
19
+
20
+ 1. **asyncssh over Paramiko/Fabric**: Native async support, built-in streaming
21
+ 2. **Parallel by default**: Multiple services run concurrently via `asyncio.gather`
22
+ 3. **Streaming output**: Real-time stdout/stderr with `[service]` prefix
23
+ 4. **SSH key auth only**: Uses ssh-agent, no password handling (YAGNI)
24
+ 5. **NFS assumption**: Compose files at same path on all hosts
25
+ 6. **Local execution**: When host is `localhost`/`local`, skip SSH and run locally
26
+
27
+ ## Communication Notes
28
+
29
+ - Clarify ambiguous wording (e.g., homophones like "right"/"write", "their"/"there").
30
+
31
+ ## Git Safety
32
+
33
+ - Never amend commits.
34
+ - Never merge into a branch; prefer fast-forward or rebase as directed.
35
+ - Never force push.
36
+
37
+ ## Commands Quick Reference
38
+
39
+ | Command | Docker Compose Equivalent |
40
+ |---------|--------------------------|
41
+ | `up` | `docker compose up -d` |
42
+ | `down` | `docker compose down` |
43
+ | `pull` | `docker compose pull` |
44
+ | `restart` | `down` + `up -d` |
45
+ | `update` | `pull` + `down` + `up -d` |
46
+ | `logs` | `docker compose logs` |
47
+ | `ps` | `docker compose ps` |
@@ -0,0 +1 @@
1
+ CLAUDE.md
@@ -0,0 +1,196 @@
1
+ Metadata-Version: 2.4
2
+ Name: compose-farm
3
+ Version: 0.1.0
4
+ Summary: Compose Farm - run docker compose commands across multiple hosts
5
+ Author-email: Bas Nijholt <bas@nijho.lt>
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: asyncssh>=2.14.0
8
+ Requires-Dist: pydantic>=2.0.0
9
+ Requires-Dist: pyyaml>=6.0
10
+ Requires-Dist: typer>=0.9.0
11
+ Description-Content-Type: text/markdown
12
+
13
+ <!-- START doctoc generated TOC please keep comment here to allow auto update -->
14
+ <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
15
+
16
+ - [Compose Farm](#compose-farm)
17
+ - [Why Compose Farm?](#why-compose-farm)
18
+ - [Key Assumption: Shared Storage](#key-assumption-shared-storage)
19
+ - [Installation](#installation)
20
+ - [Configuration](#configuration)
21
+ - [Usage](#usage)
22
+ - [Traefik Multihost Ingress (File Provider)](#traefik-multihost-ingress-file-provider)
23
+ - [Requirements](#requirements)
24
+ - [How It Works](#how-it-works)
25
+ - [License](#license)
26
+
27
+ <!-- END doctoc generated TOC please keep comment here to allow auto update -->
28
+
29
+ # Compose Farm
30
+
31
+ A minimal CLI tool to run Docker Compose commands across multiple hosts via SSH.
32
+
33
+ ## Why Compose Farm?
34
+
35
+ I run 100+ Docker Compose stacks on an LXC container that frequently runs out of memory. I needed a way to distribute services across multiple machines without the complexity of:
36
+
37
+ - **Kubernetes**: Overkill for my use case. I don't need pods, services, ingress controllers, or YAML manifests 10x the size of my compose files.
38
+ - **Docker Swarm**: Effectively in maintenance mode—no longer being invested in by Docker.
39
+
40
+ **Compose Farm is intentionally simple**: one YAML config mapping services to hosts, and a CLI that runs `docker compose` commands over SSH. That's it.
41
+
42
+ ## Key Assumption: Shared Storage
43
+
44
+ Compose Farm assumes **all your compose files are accessible at the same path on all hosts**. This is typically achieved via:
45
+
46
+ - **NFS mount** (e.g., `/opt/compose` mounted from a NAS)
47
+ - **Synced folders** (e.g., Syncthing, rsync)
48
+ - **Shared filesystem** (e.g., GlusterFS, Ceph)
49
+
50
+ ```
51
+ # Example: NFS mount on all hosts
52
+ nas:/volume1/compose → /opt/compose (on nas01)
53
+ nas:/volume1/compose → /opt/compose (on nas02)
54
+ nas:/volume1/compose → /opt/compose (on nas03)
55
+ ```
56
+
57
+ Compose Farm simply runs `docker compose -f /opt/compose/{service}/docker-compose.yml` on the appropriate host—it doesn't copy or sync files.
58
+
59
+ ## Installation
60
+
61
+ ```bash
62
+ pip install compose-farm
63
+ # or
64
+ uv pip install compose-farm
65
+ ```
66
+
67
+ ## Configuration
68
+
69
+ Create `~/.config/compose-farm/compose-farm.yaml` (or `./compose-farm.yaml` in your working directory):
70
+
71
+ ```yaml
72
+ compose_dir: /opt/compose # Must be the same path on all hosts
73
+
74
+ hosts:
75
+ nas01:
76
+ address: 192.168.1.10
77
+ user: docker
78
+ nas02:
79
+ address: 192.168.1.11
80
+ # user defaults to current user
81
+ local: localhost # Run locally without SSH
82
+
83
+ services:
84
+ plex: nas01
85
+ jellyfin: nas02
86
+ sonarr: nas01
87
+ radarr: local # Runs on the machine where you invoke compose-farm
88
+ ```
89
+
90
+ Compose files are expected at `{compose_dir}/{service}/docker-compose.yml`.
91
+
92
+ ## Usage
93
+
94
+ ```bash
95
+ # Start services
96
+ compose-farm up plex jellyfin
97
+ compose-farm up --all
98
+
99
+ # Stop services
100
+ compose-farm down plex
101
+
102
+ # Pull latest images
103
+ compose-farm pull --all
104
+
105
+ # Restart (down + up)
106
+ compose-farm restart plex
107
+
108
+ # Update (pull + down + up) - the end-to-end update command
109
+ compose-farm update --all
110
+
111
+ # Capture image digests to a TOML log (per service or all)
112
+ compose-farm snapshot plex
113
+ compose-farm snapshot --all # writes ~/.config/compose-farm/dockerfarm-log.toml
114
+
115
+ # View logs
116
+ compose-farm logs plex
117
+ compose-farm logs -f plex # follow
118
+
119
+ # Show status
120
+ compose-farm ps
121
+ ```
122
+
123
+ ## Traefik Multihost Ingress (File Provider)
124
+
125
+ If you run a single Traefik instance on one “front‑door” host and want it to route to
126
+ Compose Farm services on other hosts, Compose Farm can generate a Traefik file‑provider
127
+ fragment from your existing compose labels.
128
+
129
+ **How it works**
130
+
131
+ - Your `docker-compose.yml` remains the source of truth. Put normal `traefik.*` labels on
132
+ the container you want exposed.
133
+ - Labels and port specs may use `${VAR}` / `${VAR:-default}`; Compose Farm resolves these
134
+ using the stack’s `.env` file and your current environment, just like Docker Compose.
135
+ - Publish a host port for that container (via `ports:`). The generator prefers
136
+ host‑published ports so Traefik can reach the service across hosts; if none are found,
137
+ it warns and you’d need L3 reachability to container IPs.
138
+ - If a router label doesn’t specify `traefik.http.routers.<name>.service` and there’s only
139
+ one Traefik service defined on that container, Compose Farm wires the router to it.
140
+ - `compose-farm.yaml` stays unchanged: just `hosts` and `services: service → host`.
141
+
142
+ Example `docker-compose.yml` pattern:
143
+
144
+ ```yaml
145
+ services:
146
+ plex:
147
+ ports: ["32400:32400"]
148
+ labels:
149
+ - traefik.enable=true
150
+ - traefik.http.routers.plex.rule=Host(`plex.lab.mydomain.org`)
151
+ - traefik.http.routers.plex.entrypoints=websecure
152
+ - traefik.http.routers.plex.tls.certresolver=letsencrypt
153
+ - traefik.http.services.plex.loadbalancer.server.port=32400
154
+ ```
155
+
156
+ **One‑time Traefik setup**
157
+
158
+ Enable a file provider watching a directory (any path is fine; a common choice is on your
159
+ shared/NFS mount):
160
+
161
+ ```yaml
162
+ providers:
163
+ file:
164
+ directory: /mnt/data/traefik/dynamic.d
165
+ watch: true
166
+ ```
167
+
168
+ **Generate the fragment**
169
+
170
+ ```bash
171
+ compose-farm traefik-file --output /mnt/data/traefik/dynamic.d/compose-farm.generated.yml
172
+ ```
173
+
174
+ Re‑run this after changing Traefik labels, moving a service to another host, or changing
175
+ published ports.
176
+
177
+ ## Requirements
178
+
179
+ - Python 3.11+
180
+ - SSH key-based authentication to your hosts (uses ssh-agent)
181
+ - Docker and Docker Compose installed on all target hosts
182
+ - **Shared storage**: All compose files at the same path on all hosts (NFS, Syncthing, etc.)
183
+
184
+ ## How It Works
185
+
186
+ 1. You run `compose-farm up plex`
187
+ 2. Compose Farm looks up which host runs `plex` (e.g., `nas01`)
188
+ 3. It SSHs to `nas01` (or runs locally if `localhost`)
189
+ 4. It executes `docker compose -f /opt/compose/plex/docker-compose.yml up -d`
190
+ 5. Output is streamed back with `[plex]` prefix
191
+
192
+ That's it. No orchestration, no service discovery, no magic.
193
+
194
+ ## License
195
+
196
+ MIT
@@ -0,0 +1,35 @@
1
+ # Compose Farm – Traefik Multihost Ingress Plan
2
+
3
+ ## Goal
4
+ Generate a Traefik file-provider fragment from existing docker-compose Traefik labels (no config duplication) so a single front-door Traefik on 192.168.1.66 with wildcard `*.lab.mydomain.org` can route to services running on other hosts. Keep the current simplicity (SSH + docker compose); no Swarm/K8s.
5
+
6
+ ## Requirements
7
+ - Traefik stays on main host; keep current `dynamic.yml` and Docker provider for local containers.
8
+ - Add a watched directory provider (any path works) and load a generated fragment (e.g., `compose-farm.generated.yml`).
9
+ - No edits to compose files: reuse existing `traefik.*` labels as the single source of truth; Compose Farm only reads them.
10
+ - Generator infers routing from labels and reachability from `ports:` mappings; prefer host-published ports so Traefik can reach services across hosts. Upstreams point to `<host address>:<published host port>`; warn if no published port is found.
11
+ - Only minimal data in `compose-farm.yaml`: hosts map and service→host mapping (already present).
12
+ - No new orchestration/discovery layers; respect KISS/YAGNI/DRY.
13
+
14
+ ## Non-Goals
15
+ - No Swarm/Kubernetes adoption.
16
+ - No global Docker provider across hosts.
17
+ - No health checks/service discovery layer.
18
+
19
+ ## Current State (Dec 2025)
20
+ - Compose Farm: Typer CLI wrapping `docker compose` over SSH; config in `compose-farm.yaml`; parallel by default; snapshot/log tooling present.
21
+ - Traefik: single instance on 192.168.1.66, wildcard `*.lab.mydomain.org`, Docker provider for local services, file provider via `dynamic.yml` already in use.
22
+
23
+ ## Proposed Implementation Steps
24
+ 1) Add generator command: `compose-farm traefik-file --output <path>`.
25
+ 2) Resolve per-service host from `compose-farm.yaml`; read compose file at `{compose_dir}/{service}/docker-compose.yml`.
26
+ 3) Parse `traefik.*` labels to build routers/services/middlewares as in compose; map container port to published host port (from `ports:`) to form upstream URLs with host address.
27
+ 4) Emit file-provider YAML to the watched directory (recommended default: `/mnt/data/traefik/dynamic.d/compose-farm.generated.yml`, but user chooses via `--output`).
28
+ 5) Warnings: if no published port is found, warn that cross-host reachability requires L3 reachability to container IPs.
29
+ 6) Tests: label parsing, port mapping, YAML render; scenario with published port; scenario without published port.
30
+ 7) Docs: update README/CLAUDE to describe directory provider flags and the generator workflow; note that compose files remain unchanged.
31
+
32
+ ## Open Questions
33
+ - How to derive target host address: use `hosts.<name>.address` verbatim, or allow override per service? (Default: use host address.)
34
+ - Should we support multiple hosts/backends per service for LB/HA? (Start with single server.)
35
+ - Where to store generated file by default? (Default to user-specified `--output`; maybe fallback to `./compose-farm-traefik.yml`.)