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.
- compose_farm-0.1.0/.envrc +1 -0
- compose_farm-0.1.0/.github/release-drafter.yml +4 -0
- compose_farm-0.1.0/.github/renovate.json +35 -0
- compose_farm-0.1.0/.github/workflows/ci.yml +60 -0
- compose_farm-0.1.0/.github/workflows/release-drafter.yml +14 -0
- compose_farm-0.1.0/.github/workflows/release.yml +22 -0
- compose_farm-0.1.0/.github/workflows/renovate.json +35 -0
- compose_farm-0.1.0/.github/workflows/toc.yaml +10 -0
- compose_farm-0.1.0/.github/workflows/update-readme.yml +53 -0
- compose_farm-0.1.0/.gitignore +44 -0
- compose_farm-0.1.0/.pre-commit-config.yaml +27 -0
- compose_farm-0.1.0/.python-version +1 -0
- compose_farm-0.1.0/AGENTS.md +1 -0
- compose_farm-0.1.0/CLAUDE.md +47 -0
- compose_farm-0.1.0/GEMINI.md +1 -0
- compose_farm-0.1.0/PKG-INFO +196 -0
- compose_farm-0.1.0/PLAN.md +35 -0
- compose_farm-0.1.0/README.md +184 -0
- compose_farm-0.1.0/compose-farm.example.yaml +25 -0
- compose_farm-0.1.0/examples/README.md +42 -0
- compose_farm-0.1.0/examples/compose-farm.yaml +11 -0
- compose_farm-0.1.0/examples/hello/docker-compose.yml +4 -0
- compose_farm-0.1.0/examples/nginx/docker-compose.yml +6 -0
- compose_farm-0.1.0/pyproject.toml +114 -0
- compose_farm-0.1.0/src/compose_farm/__init__.py +9 -0
- compose_farm-0.1.0/src/compose_farm/_version.py +34 -0
- compose_farm-0.1.0/src/compose_farm/cli.py +247 -0
- compose_farm-0.1.0/src/compose_farm/config.py +93 -0
- compose_farm-0.1.0/src/compose_farm/logs.py +231 -0
- compose_farm-0.1.0/src/compose_farm/py.typed +0 -0
- compose_farm-0.1.0/src/compose_farm/ssh.py +208 -0
- compose_farm-0.1.0/src/compose_farm/traefik.py +479 -0
- compose_farm-0.1.0/tests/__init__.py +1 -0
- compose_farm-0.1.0/tests/test_config.py +143 -0
- compose_farm-0.1.0/tests/test_logs.py +82 -0
- compose_farm-0.1.0/tests/test_ssh.py +118 -0
- compose_farm-0.1.0/tests/test_traefik.py +195 -0
- compose_farm-0.1.0/uv.lock +943 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
source .venv/bin/activate
|
|
@@ -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,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,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`.)
|