mirror of
https://github.com/basnijholt/compose-farm.git
synced 2026-02-11 01:22:06 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27f17a2451 | ||
|
|
98c2492d21 | ||
|
|
04339cbb9a |
37
README.md
37
README.md
@@ -117,33 +117,38 @@ Compose files are expected at `{compose_dir}/{service}/compose.yaml` (also suppo
|
||||
|
||||
## Usage
|
||||
|
||||
The CLI is available as both `compose-farm` and the shorter `cf` alias.
|
||||
|
||||
```bash
|
||||
# Start services (auto-migrates if host changed in config)
|
||||
compose-farm up plex jellyfin
|
||||
compose-farm up --all
|
||||
cf up plex jellyfin
|
||||
cf up --all
|
||||
|
||||
# Stop services
|
||||
compose-farm down plex
|
||||
cf down plex
|
||||
|
||||
# Pull latest images
|
||||
compose-farm pull --all
|
||||
cf pull --all
|
||||
|
||||
# Restart (down + up)
|
||||
compose-farm restart plex
|
||||
cf restart plex
|
||||
|
||||
# Update (pull + down + up) - the end-to-end update command
|
||||
compose-farm update --all
|
||||
cf update --all
|
||||
|
||||
# Sync state with reality (discovers running services + captures image digests)
|
||||
compose-farm sync # updates state.yaml and dockerfarm-log.toml
|
||||
compose-farm sync --dry-run # preview without writing
|
||||
cf sync # updates state.yaml and dockerfarm-log.toml
|
||||
cf sync --dry-run # preview without writing
|
||||
|
||||
# Check config vs disk (find missing services, validate traefik labels)
|
||||
cf check
|
||||
|
||||
# View logs
|
||||
compose-farm logs plex
|
||||
compose-farm logs -f plex # follow
|
||||
cf logs plex
|
||||
cf logs -f plex # follow
|
||||
|
||||
# Show status
|
||||
compose-farm ps
|
||||
cf ps
|
||||
```
|
||||
|
||||
### Auto-Migration
|
||||
@@ -158,7 +163,7 @@ When you change a service's host assignment in config and run `up`, Compose Farm
|
||||
services:
|
||||
plex: nas01
|
||||
|
||||
# After: change to nas02, then run `compose-farm up plex`
|
||||
# After: change to nas02, then run `cf up plex`
|
||||
services:
|
||||
plex: nas02 # Compose Farm will migrate automatically
|
||||
```
|
||||
@@ -211,7 +216,7 @@ providers:
|
||||
**Generate the fragment**
|
||||
|
||||
```bash
|
||||
compose-farm traefik-file --all --output /mnt/data/traefik/dynamic.d/compose-farm.yml
|
||||
cf traefik-file --all --output /mnt/data/traefik/dynamic.d/compose-farm.yml
|
||||
```
|
||||
|
||||
Re‑run this after changing Traefik labels, moving a service to another host, or changing
|
||||
@@ -238,7 +243,7 @@ services:
|
||||
The `traefik_service` option specifies which service runs Traefik. Services on the same host
|
||||
are skipped in the file-provider config since Traefik's docker provider handles them directly.
|
||||
|
||||
Now `compose-farm up plex` will update the Traefik config automatically—no separate
|
||||
Now `cf up plex` will update the Traefik config automatically—no separate
|
||||
`traefik-file` command needed.
|
||||
|
||||
**Combining with existing config**
|
||||
@@ -249,7 +254,7 @@ directory and Traefik will merge all files:
|
||||
```bash
|
||||
mkdir -p /opt/traefik/dynamic.d
|
||||
mv /opt/traefik/dynamic.yml /opt/traefik/dynamic.d/manual.yml
|
||||
compose-farm traefik-file --all -o /opt/traefik/dynamic.d/compose-farm.yml
|
||||
cf traefik-file --all -o /opt/traefik/dynamic.d/compose-farm.yml
|
||||
```
|
||||
|
||||
Update your Traefik config to use directory watching instead of a single file:
|
||||
@@ -272,7 +277,7 @@ Update your Traefik config to use directory watching instead of a single file:
|
||||
|
||||
## How It Works
|
||||
|
||||
1. You run `compose-farm up plex`
|
||||
1. You run `cf up plex`
|
||||
2. Compose Farm looks up which host runs `plex` (e.g., `nas01`)
|
||||
3. It SSHs to `nas01` (or runs locally if `localhost`)
|
||||
4. It executes `docker compose -f /opt/compose/plex/docker-compose.yml up -d`
|
||||
|
||||
@@ -179,7 +179,7 @@ async def _up_with_migration(
|
||||
return results
|
||||
|
||||
|
||||
@app.command()
|
||||
@app.command(rich_help_panel="Lifecycle")
|
||||
def up(
|
||||
services: ServicesArg = None,
|
||||
all_services: AllOption = False,
|
||||
@@ -192,7 +192,7 @@ def up(
|
||||
_report_results(results)
|
||||
|
||||
|
||||
@app.command()
|
||||
@app.command(rich_help_panel="Lifecycle")
|
||||
def down(
|
||||
services: ServicesArg = None,
|
||||
all_services: AllOption = False,
|
||||
@@ -211,7 +211,7 @@ def down(
|
||||
_report_results(results)
|
||||
|
||||
|
||||
@app.command()
|
||||
@app.command(rich_help_panel="Lifecycle")
|
||||
def pull(
|
||||
services: ServicesArg = None,
|
||||
all_services: AllOption = False,
|
||||
@@ -223,7 +223,7 @@ def pull(
|
||||
_report_results(results)
|
||||
|
||||
|
||||
@app.command()
|
||||
@app.command(rich_help_panel="Lifecycle")
|
||||
def restart(
|
||||
services: ServicesArg = None,
|
||||
all_services: AllOption = False,
|
||||
@@ -236,7 +236,7 @@ def restart(
|
||||
_report_results(results)
|
||||
|
||||
|
||||
@app.command()
|
||||
@app.command(rich_help_panel="Lifecycle")
|
||||
def update(
|
||||
services: ServicesArg = None,
|
||||
all_services: AllOption = False,
|
||||
@@ -249,7 +249,7 @@ def update(
|
||||
_report_results(results)
|
||||
|
||||
|
||||
@app.command()
|
||||
@app.command(rich_help_panel="Monitoring")
|
||||
def logs(
|
||||
services: ServicesArg = None,
|
||||
all_services: AllOption = False,
|
||||
@@ -266,7 +266,7 @@ def logs(
|
||||
_report_results(results)
|
||||
|
||||
|
||||
@app.command()
|
||||
@app.command(rich_help_panel="Monitoring")
|
||||
def ps(
|
||||
config: ConfigOption = None,
|
||||
) -> None:
|
||||
@@ -276,7 +276,7 @@ def ps(
|
||||
_report_results(results)
|
||||
|
||||
|
||||
@app.command("traefik-file")
|
||||
@app.command("traefik-file", rich_help_panel="Configuration")
|
||||
def traefik_file(
|
||||
services: ServicesArg = None,
|
||||
all_services: AllOption = False,
|
||||
@@ -364,7 +364,7 @@ def _report_sync_changes(
|
||||
)
|
||||
|
||||
|
||||
@app.command()
|
||||
@app.command(rich_help_panel="Configuration")
|
||||
def sync(
|
||||
config: ConfigOption = None,
|
||||
log_path: LogPathOption = None,
|
||||
@@ -420,7 +420,7 @@ def sync(
|
||||
err_console.print(f"[yellow]![/] {exc}")
|
||||
|
||||
|
||||
@app.command()
|
||||
@app.command(rich_help_panel="Configuration")
|
||||
def check(
|
||||
config: ConfigOption = None,
|
||||
) -> None:
|
||||
|
||||
@@ -29,7 +29,6 @@ class PortMapping:
|
||||
|
||||
target: int
|
||||
published: int | None
|
||||
protocol: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -121,7 +120,7 @@ def _parse_ports(raw: Any, env: dict[str, str]) -> list[PortMapping]: # noqa: P
|
||||
for item in items:
|
||||
if isinstance(item, str):
|
||||
interpolated = _interpolate(item, env)
|
||||
port_spec, _, protocol = interpolated.partition("/")
|
||||
port_spec, _, _ = interpolated.partition("/")
|
||||
parts = port_spec.split(":")
|
||||
published: int | None = None
|
||||
target: int | None = None
|
||||
@@ -136,9 +135,7 @@ def _parse_ports(raw: Any, env: dict[str, str]) -> list[PortMapping]: # noqa: P
|
||||
target = int(parts[-1])
|
||||
|
||||
if target is not None:
|
||||
mappings.append(
|
||||
PortMapping(target=target, published=published, protocol=protocol or None)
|
||||
)
|
||||
mappings.append(PortMapping(target=target, published=published))
|
||||
elif isinstance(item, dict):
|
||||
target_raw = item.get("target")
|
||||
if isinstance(target_raw, str):
|
||||
@@ -158,14 +155,7 @@ def _parse_ports(raw: Any, env: dict[str, str]) -> list[PortMapping]: # noqa: P
|
||||
published_val = int(str(published_raw)) if published_raw is not None else None
|
||||
except (TypeError, ValueError):
|
||||
published_val = None
|
||||
protocol_val = item.get("protocol")
|
||||
mappings.append(
|
||||
PortMapping(
|
||||
target=target_val,
|
||||
published=published_val,
|
||||
protocol=str(protocol_val) if protocol_val else None,
|
||||
)
|
||||
)
|
||||
mappings.append(PortMapping(target=target_val, published=published_val))
|
||||
|
||||
return mappings
|
||||
|
||||
|
||||
Reference in New Issue
Block a user