{"id":35804383,"url":"https://github.com/basnijholt/compose-farm","last_synced_at":"2026-07-01T00:00:44.440Z","repository":{"id":328221475,"uuid":"1114673790","full_name":"basnijholt/compose-farm","owner":"basnijholt","description":"Compose Farm - run docker compose commands across multiple hosts","archived":false,"fork":false,"pushed_at":"2026-06-24T16:27:51.000Z","size":1112,"stargazers_count":303,"open_issues_count":5,"forks_count":3,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-06-24T17:22:43.438Z","etag":null,"topics":["cli","compose","deployment","docker","homelab","multi-host","nas","orchestration","python","self-hosted","ssh","traefik"],"latest_commit_sha":null,"homepage":"https://compose-farm.nijho.lt","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/basnijholt.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2025-12-11T18:03:14.000Z","updated_at":"2026-06-24T16:27:25.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/basnijholt/compose-farm","commit_stats":null,"previous_names":["basnijholt/compose-farm"],"tags_count":85,"template":false,"template_full_name":null,"purl":"pkg:github/basnijholt/compose-farm","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/basnijholt%2Fcompose-farm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/basnijholt%2Fcompose-farm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/basnijholt%2Fcompose-farm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/basnijholt%2Fcompose-farm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/basnijholt","download_url":"https://codeload.github.com/basnijholt/compose-farm/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/basnijholt%2Fcompose-farm/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34987610,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-30T02:00:05.919Z","response_time":92,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","compose","deployment","docker","homelab","multi-host","nas","orchestration","python","self-hosted","ssh","traefik"],"created_at":"2026-01-07T12:17:55.538Z","updated_at":"2026-07-01T00:00:44.422Z","avatar_url":"https://github.com/basnijholt.png","language":"Python","funding_links":[],"categories":["Python"],"sub_categories":[],"readme":"# Compose Farm\n\n[![PyPI](https://img.shields.io/pypi/v/compose-farm)](https://pypi.org/project/compose-farm/)\n[![Python](https://img.shields.io/pypi/pyversions/compose-farm)](https://pypi.org/project/compose-farm/)\n[![License](https://img.shields.io/github/license/basnijholt/compose-farm)](LICENSE)\n[![GitHub stars](https://img.shields.io/github/stars/basnijholt/compose-farm)](https://github.com/basnijholt/compose-farm/stargazers)\n\n\u003cimg src=\"https://files.nijho.lt/compose-farm.png\" alt=\"Compose Farm logo\" align=\"right\" style=\"width: 300px;\" /\u003e\n\nA minimal CLI tool to run Docker Compose commands across multiple hosts via SSH.\n\n\u003e [!NOTE]\n\u003e Agentless multi-host Docker Compose. CLI-first with a web UI. Your files stay as plain folders—version-controllable, no lock-in. Run `cf apply` and reality matches your config.\n\n**Why Compose Farm?**\n- **Your files, your control** — Plain folders + YAML, not locked in Portainer. Version control everything.\n- **Agentless** — Just SSH, no agents to deploy (unlike [Dockge](https://github.com/louislam/dockge)).\n- **Zero changes required** — Existing compose files work as-is.\n- **Grows with you** — Start single-host, scale to multi-host seamlessly.\n- **Declarative** — Change config, run `cf apply`, reality matches.\n\n## Quick Demo\n\n**CLI:**\n\n![CLI Demo](docs/assets/quickstart.gif)\n\n**Web UI:**\n\n![Web UI Demo](docs/assets/web-workflow.gif)\n\n## Table of Contents\n\n\u003c!-- START doctoc generated TOC please keep comment here to allow auto update --\u003e\n\u003c!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --\u003e\n\n- [Why Compose Farm?](#why-compose-farm)\n- [How It Works](#how-it-works)\n- [Requirements](#requirements)\n- [Limitations \u0026 Best Practices](#limitations--best-practices)\n  - [What breaks when you move a stack](#what-breaks-when-you-move-a-stack)\n  - [Best practices](#best-practices)\n  - [What Compose Farm doesn't do](#what-compose-farm-doesnt-do)\n- [Installation](#installation)\n- [SSH Authentication](#ssh-authentication)\n  - [SSH Agent](#ssh-agent)\n  - [Dedicated SSH Key (default for Docker)](#dedicated-ssh-key-default-for-docker)\n- [Configuration](#configuration)\n  - [Single-host example](#single-host-example)\n  - [Multi-host example](#multi-host-example)\n  - [Multi-Host Stacks](#multi-host-stacks)\n  - [Config Command](#config-command)\n- [Usage](#usage)\n  - [Docker Compose Commands](#docker-compose-commands)\n  - [Compose Farm Commands](#compose-farm-commands)\n  - [Aliases](#aliases)\n  - [CLI `--help` Output](#cli---help-output)\n  - [Auto-Migration](#auto-migration)\n- [Traefik Multihost Ingress (File Provider)](#traefik-multihost-ingress-file-provider)\n- [Host Resource Monitoring (Glances)](#host-resource-monitoring-glances)\n- [Comparison with Alternatives](#comparison-with-alternatives)\n- [License](#license)\n\n\u003c!-- END doctoc generated TOC please keep comment here to allow auto update --\u003e\n\n## Why Compose Farm?\n\nI used to run 100+ Docker Compose stacks on a single machine that kept running out of memory. I needed a way to distribute stacks across multiple machines without the complexity of:\n\n- **Kubernetes**: Overkill for my use case. I don't need pods, services, ingress controllers, or YAML manifests 10x the size of my compose files.\n- **Docker Swarm**: Effectively in maintenance mode—no longer being invested in by Docker.\n\nBoth require changes to your compose files. **Compose Farm requires zero changes**—your existing `docker-compose.yml` files work as-is.\n\nI also wanted a declarative setup—one config file that defines where everything runs. Change the config, run `cf apply`, and everything reconciles—stacks start, migrate, or stop as needed. See [Comparison with Alternatives](#comparison-with-alternatives) for how this compares to other approaches.\n\n\u003cp align=\"center\"\u003e\n\u003ca href=\"https://xkcd.com/927/\"\u003e\n\u003cimg src=\"https://imgs.xkcd.com/comics/standards.png\" alt=\"xkcd: Standards\" width=\"400\" /\u003e\n\u003c/a\u003e\n\u003c/p\u003e\n\nBefore you say it—no, this is not a new standard. I changed nothing about my existing setup. When I added more hosts, I just mounted my drives at the same paths, and everything worked. You can do all of this manually today—SSH into a host and run `docker compose up`.\n\nCompose Farm just automates what you'd do by hand:\n- Runs `docker compose` commands over SSH\n- Tracks which stack runs on which host\n- **One command (`cf apply`) to reconcile everything**—start missing stacks, migrate moved ones, stop removed ones\n- Generates Traefik file-provider config for cross-host routing\n\n**It's a convenience wrapper, not a new paradigm.**\n\n## How It Works\n\n**The declarative way** — run `cf apply` and reality matches your config:\n\n1. Compose Farm compares your config to what's actually running\n2. Stacks in config but not running? **Starts them**\n3. Stacks on the wrong host? **Migrates them** (stops on old host, starts on new)\n4. Stacks running but removed from config? **Stops them**\n\n**Under the hood** — each stack operation is just SSH + docker compose:\n\n1. Look up which host runs the stack (e.g., `plex` → `server-1`)\n2. SSH to `server-1` (or run locally if `localhost`)\n3. Execute `docker compose -f /opt/compose/plex/docker-compose.yml up -d`\n4. Stream output back with `[plex]` prefix\n\nThat's it. No orchestration, no service discovery, no magic.\n\n## Requirements\n\n- Python 3.11+ (we recommend [uv](https://docs.astral.sh/uv/) for installation)\n- SSH key-based authentication to your hosts (uses ssh-agent)\n- Docker and Docker Compose installed on all target hosts\n- **Shared storage**: All compose files must be accessible at the same path on all hosts\n- **Docker networks**: External networks must exist on all hosts (use `cf init-network` to create)\n\nCompose Farm assumes your compose files are accessible at the same path on all hosts. This is typically achieved via:\n\n- **NFS mount** (e.g., `/opt/compose` mounted from a NAS)\n- **Synced folders** (e.g., Syncthing, rsync)\n- **Shared filesystem** (e.g., GlusterFS, Ceph)\n\n```\n# Example: NFS mount on all Docker hosts\nnas:/volume1/compose  →  /opt/compose (on server-1)\nnas:/volume1/compose  →  /opt/compose (on server-2)\nnas:/volume1/compose  →  /opt/compose (on server-3)\n```\n\nCompose Farm simply runs `docker compose -f /opt/compose/{stack}/docker-compose.yml` on the appropriate host—it doesn't copy or sync files.\n\n## Limitations \u0026 Best Practices\n\nCompose Farm moves containers between hosts but **does not provide cross-host networking**. Docker's internal DNS and networks don't span hosts.\n\n### What breaks when you move a stack\n\n- **Docker DNS** - `http://redis:6379` won't resolve from another host\n- **Docker networks** - Containers can't reach each other via network names\n- **Environment variables** - `DATABASE_URL=postgres://db:5432` stops working\n\n### Best practices\n\n1. **Keep dependent services together** - If an app needs a database, redis, or worker, keep them in the same compose file on the same host\n\n2. **Only migrate standalone stacks** - Stacks whose services don't talk to other containers (or only talk to external APIs) are safe to move\n\n3. **Expose ports for cross-host communication** - If services must communicate across hosts, publish ports and use IP addresses instead of container names:\n   ```yaml\n   # Instead of: DATABASE_URL=postgres://db:5432\n   # Use:        DATABASE_URL=postgres://192.168.1.66:5432\n   ```\n   This includes Traefik routing—containers need published ports for the file-provider to reach them\n\n### What Compose Farm doesn't do\n\n- No overlay networking (use Docker Swarm or Kubernetes for that)\n- No service discovery across hosts\n- No automatic dependency tracking between compose files\n\nIf you need containers on different hosts to communicate seamlessly, you need Docker Swarm, Kubernetes, or a service mesh—which adds the complexity Compose Farm is designed to avoid.\n\n## Installation\n\n```bash\n# One-liner (installs uv if needed)\ncurl -fsSL https://compose-farm.nijho.lt/install | sh\n\n# Or if you already have uv/pip\nuv tool install compose-farm\npip install compose-farm\n```\n\n\u003cdetails\u003e\u003csummary\u003e🐳 Docker\u003c/summary\u003e\n\nUsing the provided `docker-compose.yml`:\n```bash\ndocker compose run --rm cf up --all\n```\n\nOr directly:\n```bash\ndocker run --rm \\\n  -v $SSH_AUTH_SOCK:/ssh-agent -e SSH_AUTH_SOCK=/ssh-agent \\\n  -v ./compose-farm.yaml:/root/.config/compose-farm/compose-farm.yaml:ro \\\n  ghcr.io/basnijholt/compose-farm up --all\n```\n\n**Running as non-root user** (recommended for NFS mounts):\n\nBy default, containers run as root. To preserve file ownership on mounted volumes\n(e.g., `compose-farm-state.yaml`, config edits), set these environment variables:\n\n```bash\n# Add to .env file (one-time setup)\necho \"CF_UID=$(id -u)\" \u003e\u003e .env\necho \"CF_GID=$(id -g)\" \u003e\u003e .env\necho \"CF_HOME=$HOME\" \u003e\u003e .env\necho \"CF_USER=$USER\" \u003e\u003e .env\n```\n\nOr use [direnv](https://direnv.net/) (copies `.envrc.example` to `.envrc`):\n```bash\ncp .envrc.example .envrc \u0026\u0026 direnv allow\n```\n\n\u003c/details\u003e\n\n## SSH Authentication\n\nCompose Farm uses SSH to run commands on remote hosts. There are two authentication methods:\n\n### SSH Agent\n\nWorks out of the box when running locally if you have an SSH agent running with your keys loaded:\n\n```bash\n# Verify your agent has keys\nssh-add -l\n\n# Run compose-farm commands\ncf up --all\n```\n\n### Dedicated SSH Key (default for Docker)\n\nWhen running in Docker, SSH agent sockets are ephemeral and can be lost after container restarts. The `cf ssh` command sets up a dedicated key that persists:\n\n```bash\n# Generate key and copy to all configured hosts\ncf ssh setup\n\n# Check status\ncf ssh status\n```\n\nThis creates `~/.ssh/compose-farm/id_ed25519` (ED25519, no passphrase) and copies the public key to each host's `authorized_keys`. Compose Farm tries the SSH agent first, then falls back to this key.\n\n\u003cdetails\u003e\u003csummary\u003e🐳 Docker volume options for SSH keys\u003c/summary\u003e\n\nWhen running in Docker, mount a volume to persist the SSH keys. Choose ONE option and use it for both `cf` and `web` Compose services:\n\n**Option 1: Host path (default)** - keys at `~/.ssh/compose-farm/id_ed25519`\n```yaml\nvolumes:\n  - ~/.ssh/compose-farm:${CF_HOME:-/root}/.ssh\n```\n\n**Option 2: Named volume** - managed by Docker\n```yaml\nvolumes:\n  - cf-ssh:${CF_HOME:-/root}/.ssh\n```\n\n**Option 3: SSH agent forwarding** - if you prefer using your host's ssh-agent\n```yaml\nvolumes:\n  - ${SSH_AUTH_SOCK}:/ssh-agent:ro\n```\nNote: Requires `SSH_AUTH_SOCK` environment variable to be set. The socket path is ephemeral and changes across sessions.\n\nRun setup once after starting the container (while the SSH agent still works):\n\n```bash\ndocker compose exec web cf ssh setup\n```\n\nThe keys will persist across restarts.\n\n**Note:** When running as non-root (with `CF_UID`/`CF_GID`), set `CF_HOME` to your home directory so SSH finds the keys at the correct path.\n\n\u003c/details\u003e\n\n## Configuration\n\nCreate `compose-farm.yaml` in the directory where you'll run commands (e.g., `/opt/stacks`). This keeps config near your stacks. Alternatively, use `~/.config/compose-farm/compose-farm.yaml` for a global config, or symlink from one to the other with `cf config symlink`.\n\n### Single-host example\n\nNo SSH, shared storage, or Traefik file-provider required.\n\n```yaml\ncompose_dir: /opt/stacks\n\nhosts:\n  local: localhost  # Run locally without SSH\n\nstacks:\n  plex: local\n  jellyfin: local\n  traefik: local\n```\n\n### Multi-host example\n```yaml\ncompose_dir: /opt/compose  # Must be the same path on all hosts\n\nhosts:\n  server-1:\n    address: 192.168.1.10\n    user: docker\n  server-2:\n    address: 192.168.1.11\n    # user defaults to current user\n\nstacks:\n  plex: server-1\n  jellyfin: server-2\n  grafana: server-1\n\n  # Multi-host stacks (run on multiple/all hosts)\n  autokuma: all              # Runs on ALL configured hosts\n  dozzle: [server-1, server-2]  # Explicit list of hosts\n```\n\nFor cross-host HTTP routing, add Traefik labels to your compose files and set `traefik_file` so Compose Farm can generate the file-provider config.\n\nEach entry in `stacks:` maps to a folder under `compose_dir` that contains a compose file. Compose files are expected at `{compose_dir}/{stack}/compose.yaml` (also supports `compose.yml`, `docker-compose.yml`, `docker-compose.yaml`).\n\n### Multi-Host Stacks\n\nSome stacks need to run on every host. This is typically required for tools that access **host-local resources** like the Docker socket (`/var/run/docker.sock`), which cannot be accessed remotely without security risks.\n\nCommon use cases:\n- **AutoKuma** - auto-creates Uptime Kuma monitors from container labels (needs local Docker socket)\n- **Dozzle** - real-time log viewer (needs local Docker socket)\n- **Promtail/Alloy** - log shipping agents (needs local Docker socket and log files)\n- **node-exporter** - Prometheus host metrics (needs access to host /proc, /sys)\n\nThis is the same pattern as Docker Swarm's `deploy.mode: global`.\n\nUse the `all` keyword or an explicit list:\n\n```yaml\nstacks:\n  # Run on all configured hosts\n  autokuma: all\n  dozzle: all\n\n  # Run on specific hosts\n  node-exporter: [server-1, server-2, server-3]\n```\n\nWhen you run `cf up autokuma`, it starts the stack on all hosts in parallel. Multi-host stacks:\n- Are excluded from migration logic (they always run everywhere)\n- Show output with `[stack@host]` prefix for each host\n- Track all running hosts in state\n\n### Config Command\n\nCompose Farm includes a `config` subcommand to help manage configuration files:\n\n```bash\ncf config init      # Create a new config file with documented example\ncf config show      # Display current config with syntax highlighting\ncf config path      # Print the config file path (useful for scripting)\ncf config validate  # Validate config syntax and schema\ncf config edit      # Open config in $EDITOR\n```\n\nUse `cf config init` to get started with a fully documented template.\n\n## Usage\n\nThe CLI is available as both `compose-farm` and the shorter `cf` alias.\n\n### Docker Compose Commands\n\nThese wrap `docker compose` with multi-host superpowers:\n\n| Command | Wraps | Compose Farm Additions |\n|---------|-------|------------------------|\n| `cf up` | `up -d` | `--all`, `--host`, parallel execution, auto-migration |\n| `cf down` | `down` | `--all`, `--host`, `--orphaned`, state tracking |\n| `cf stop` | `stop` | `--all`, `--host`, `--service` |\n| `cf restart` | `restart` | `--all`, `--host`, `--service` |\n| `cf pull` | `pull` | `--all`, `--host`, `--service`, parallel execution |\n| `cf logs` | `logs` | `--all`, `--host`, multi-stack output |\n| `cf ps` | `ps` | `--all`, `--host`, unified cross-host view |\n| `cf compose` | any | passthrough for commands not listed above |\n\n### Compose Farm Commands\n\nMulti-host orchestration that Docker Compose can't do:\n\n| Command | Description |\n|---------|-------------|\n| **`cf apply`** | **Reconcile: start missing, migrate moved, stop orphans** |\n| `cf update` | Shorthand for `up --pull --build`; supports `--host` |\n| `cf refresh` | Sync state from what's actually running |\n| `cf check` | Validate config, mounts, networks |\n| `cf init-network` | Create Docker network on all hosts |\n| `cf traefik-file` | Generate Traefik file-provider config |\n| `cf config` | Manage config files (init, show, validate, edit, symlink) |\n| `cf ssh` | Manage SSH keys (setup, status, keygen) |\n| `cf list` | List all stacks and their assigned hosts |\n\n### Aliases\n\nShort aliases for frequently used commands:\n\n| Alias | Command | Alias | Command |\n|-------|---------|-------|---------|\n| `cf a` | `apply` | `cf s` | `stats` |\n| `cf l` | `logs` | `cf ls` | `list` |\n| `cf r` | `restart` | `cf rf` | `refresh` |\n| `cf u` | `update` | `cf ck` | `check` |\n| `cf p` | `pull` | `cf tf` | `traefik-file` |\n| `cf c` | `compose` | | |\n\nEach command replaces: look up host → SSH → find compose file → run `ssh host \"cd /opt/compose/plex \u0026\u0026 docker compose up -d\"`.\n\n```bash\n# The main command: make reality match your config\ncf apply               # start missing + migrate + stop orphans\ncf apply --dry-run     # preview what would change\ncf apply --no-orphans  # skip stopping orphaned stacks\ncf apply --full        # also refresh all stacks (picks up config changes)\n\n# Or operate on individual stacks\ncf up plex jellyfin    # start stacks (auto-migrates if host changed)\ncf up --all\ncf down plex           # stop stacks\ncf down --orphaned     # stop stacks removed from config\n\n# Pull latest images\ncf pull --all\ncf pull --host pc\n\n# Restart running containers\ncf restart plex\ncf restart --host pc\n\n# Update (pull + build, only recreates containers if images changed)\ncf update --all\ncf update --host pc\n\n# Update state from reality (discovers running stacks + captures digests)\ncf refresh             # updates compose-farm-state.yaml and dockerfarm-log.toml\ncf refresh --dry-run   # preview without writing\n\n# Validate config, traefik labels, mounts, and networks\ncf check                 # full validation (includes SSH checks)\ncf check --local         # fast validation (skip SSH)\ncf check jellyfin        # check stack + show which hosts can run it\n\n# Create Docker network on new hosts (before migrating stacks)\ncf init-network nuc hp   # create mynetwork on specific hosts\ncf init-network          # create on all hosts\n\n# View logs\ncf logs plex\ncf logs -f plex  # follow\n\n# Show status\ncf ps\n```\n\n### CLI `--help` Output\n\nFull `--help` output for each command. See the [Usage](#usage) table above for a quick overview.\n\n\u003cdetails\u003e\n\u003csummary\u003eSee the output of \u003ccode\u003ecf --help\u003c/code\u003e\u003c/summary\u003e\n\n\u003c!-- CODE:BASH:START --\u003e\n\u003c!-- echo '```yaml' --\u003e\n\u003c!-- export NO_COLOR=1 --\u003e\n\u003c!-- export TERM=dumb --\u003e\n\u003c!-- export TERMINAL_WIDTH=90 --\u003e\n\u003c!-- cf --help --\u003e\n\u003c!-- echo '```' --\u003e\n\u003c!-- CODE:END --\u003e\n\u003c!-- OUTPUT:START --\u003e\n\u003c!-- ⚠️ This content is auto-generated by `markdown-code-runner`. --\u003e\n```yaml\n\n Usage: cf [OPTIONS] COMMAND [ARGS]...\n\n Compose Farm - run docker compose commands across multiple hosts\n\n╭─ Options ──────────────────────────────────────────────────────────────────────────────╮\n│ --version             -v        Show version and exit                                  │\n│ --install-completion            Install completion for the current shell.              │\n│ --show-completion               Show completion for the current shell, to copy it or   │\n│                                 customize the installation.                            │\n│ --help                -h        Show this message and exit.                            │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n╭─ Configuration ────────────────────────────────────────────────────────────────────────╮\n│ traefik-file  Generate a Traefik file-provider fragment from compose Traefik labels.   │\n│ refresh       Update local state from running stacks.                                  │\n│ check         Validate configuration, traefik labels, mounts, and networks.            │\n│ init-network  Create Docker network on hosts with consistent settings.                 │\n│ config        Manage compose-farm configuration files.                                 │\n│ ssh           Manage SSH keys for passwordless authentication.                         │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n╭─ Lifecycle ────────────────────────────────────────────────────────────────────────────╮\n│ up            Start stacks (docker compose up -d). Auto-migrates if host changed.      │\n│ down          Stop stacks (docker compose down).                                       │\n│ stop          Stop services without removing containers (docker compose stop).         │\n│ pull          Pull latest images (docker compose pull).                                │\n│ restart       Restart running containers (docker compose restart).                     │\n│ update        Update stacks (pull + build + up). Shorthand for 'up --pull --build'.    │\n│ apply         Make reality match config (start, migrate, stop strays/orphans as        │\n│               needed).                                                                 │\n│ compose       Run any docker compose command on a stack.                               │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n╭─ Monitoring ───────────────────────────────────────────────────────────────────────────╮\n│ logs          Show stack logs. With --service, shows logs for just that service.       │\n│ ps            Show status of stacks.                                                   │\n│ stats         Show overview statistics for hosts and stacks.                           │\n│ list          List all stacks and their assigned hosts.                                │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n╭─ Server ───────────────────────────────────────────────────────────────────────────────╮\n│ web           Start the web UI server.                                                 │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n\n```\n\n\u003c!-- OUTPUT:END --\u003e\n\n\u003c/details\u003e\n\n**Lifecycle**\n\n\u003cdetails\u003e\n\u003csummary\u003eSee the output of \u003ccode\u003ecf up --help\u003c/code\u003e\u003c/summary\u003e\n\n\u003c!-- CODE:BASH:START --\u003e\n\u003c!-- echo '```yaml' --\u003e\n\u003c!-- export NO_COLOR=1 --\u003e\n\u003c!-- export TERM=dumb --\u003e\n\u003c!-- export TERMINAL_WIDTH=90 --\u003e\n\u003c!-- cf up --help --\u003e\n\u003c!-- echo '```' --\u003e\n\u003c!-- CODE:END --\u003e\n\u003c!-- OUTPUT:START --\u003e\n\u003c!-- ⚠️ This content is auto-generated by `markdown-code-runner`. --\u003e\n```yaml\n\n Usage: cf up [OPTIONS] [STACKS]...\n\n Start stacks (docker compose up -d). Auto-migrates if host changed.\n\n╭─ Arguments ────────────────────────────────────────────────────────────────────────────╮\n│   [stacks]...      TEXT  Stacks to operate on                                          │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n╭─ Options ──────────────────────────────────────────────────────────────────────────────╮\n│ --all      -a            Run on all stacks                                             │\n│ --host     -H      TEXT  Filter to stacks on this host                                 │\n│ --service  -s      TEXT  Target a specific service within the stack                    │\n│ --pull                   Pull images before starting (--pull always)                   │\n│ --build                  Build images before starting                                  │\n│ --config   -c      PATH  Path to config file                                           │\n│ --help     -h            Show this message and exit.                                   │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n\n```\n\n\u003c!-- OUTPUT:END --\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eSee the output of \u003ccode\u003ecf down --help\u003c/code\u003e\u003c/summary\u003e\n\n\u003c!-- CODE:BASH:START --\u003e\n\u003c!-- echo '```yaml' --\u003e\n\u003c!-- export NO_COLOR=1 --\u003e\n\u003c!-- export TERM=dumb --\u003e\n\u003c!-- export TERMINAL_WIDTH=90 --\u003e\n\u003c!-- cf down --help --\u003e\n\u003c!-- echo '```' --\u003e\n\u003c!-- CODE:END --\u003e\n\u003c!-- OUTPUT:START --\u003e\n\u003c!-- ⚠️ This content is auto-generated by `markdown-code-runner`. --\u003e\n```yaml\n\n Usage: cf down [OPTIONS] [STACKS]...\n\n Stop stacks (docker compose down).\n\n╭─ Arguments ────────────────────────────────────────────────────────────────────────────╮\n│   [stacks]...      TEXT  Stacks to operate on                                          │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n╭─ Options ──────────────────────────────────────────────────────────────────────────────╮\n│ --all       -a            Run on all stacks                                            │\n│ --orphaned                Stop orphaned stacks (in state but removed from config)      │\n│ --host      -H      TEXT  Filter to stacks on this host                                │\n│ --config    -c      PATH  Path to config file                                          │\n│ --help      -h            Show this message and exit.                                  │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n\n```\n\n\u003c!-- OUTPUT:END --\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eSee the output of \u003ccode\u003ecf stop --help\u003c/code\u003e\u003c/summary\u003e\n\n\u003c!-- CODE:BASH:START --\u003e\n\u003c!-- echo '```yaml' --\u003e\n\u003c!-- export NO_COLOR=1 --\u003e\n\u003c!-- export TERM=dumb --\u003e\n\u003c!-- export TERMINAL_WIDTH=90 --\u003e\n\u003c!-- cf stop --help --\u003e\n\u003c!-- echo '```' --\u003e\n\u003c!-- CODE:END --\u003e\n\u003c!-- OUTPUT:START --\u003e\n\u003c!-- ⚠️ This content is auto-generated by `markdown-code-runner`. --\u003e\n```yaml\n\n Usage: cf stop [OPTIONS] [STACKS]...\n\n Stop services without removing containers (docker compose stop).\n\n╭─ Arguments ────────────────────────────────────────────────────────────────────────────╮\n│   [stacks]...      TEXT  Stacks to operate on                                          │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n╭─ Options ──────────────────────────────────────────────────────────────────────────────╮\n│ --all      -a            Run on all stacks                                             │\n│ --host     -H      TEXT  Filter to stacks on this host                                 │\n│ --service  -s      TEXT  Target a specific service within the stack                    │\n│ --config   -c      PATH  Path to config file                                           │\n│ --help     -h            Show this message and exit.                                   │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n\n```\n\n\u003c!-- OUTPUT:END --\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eSee the output of \u003ccode\u003ecf pull --help\u003c/code\u003e\u003c/summary\u003e\n\n\u003c!-- CODE:BASH:START --\u003e\n\u003c!-- echo '```yaml' --\u003e\n\u003c!-- export NO_COLOR=1 --\u003e\n\u003c!-- export TERM=dumb --\u003e\n\u003c!-- export TERMINAL_WIDTH=90 --\u003e\n\u003c!-- cf pull --help --\u003e\n\u003c!-- echo '```' --\u003e\n\u003c!-- CODE:END --\u003e\n\u003c!-- OUTPUT:START --\u003e\n\u003c!-- ⚠️ This content is auto-generated by `markdown-code-runner`. --\u003e\n```yaml\n\n Usage: cf pull [OPTIONS] [STACKS]...\n\n Pull latest images (docker compose pull).\n\n╭─ Arguments ────────────────────────────────────────────────────────────────────────────╮\n│   [stacks]...      TEXT  Stacks to operate on                                          │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n╭─ Options ──────────────────────────────────────────────────────────────────────────────╮\n│ --all      -a            Run on all stacks                                             │\n│ --host     -H      TEXT  Filter to stacks on this host                                 │\n│ --service  -s      TEXT  Target a specific service within the stack                    │\n│ --config   -c      PATH  Path to config file                                           │\n│ --help     -h            Show this message and exit.                                   │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n\n```\n\n\u003c!-- OUTPUT:END --\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eSee the output of \u003ccode\u003ecf restart --help\u003c/code\u003e\u003c/summary\u003e\n\n\u003c!-- CODE:BASH:START --\u003e\n\u003c!-- echo '```yaml' --\u003e\n\u003c!-- export NO_COLOR=1 --\u003e\n\u003c!-- export TERM=dumb --\u003e\n\u003c!-- export TERMINAL_WIDTH=90 --\u003e\n\u003c!-- cf restart --help --\u003e\n\u003c!-- echo '```' --\u003e\n\u003c!-- CODE:END --\u003e\n\u003c!-- OUTPUT:START --\u003e\n\u003c!-- ⚠️ This content is auto-generated by `markdown-code-runner`. --\u003e\n```yaml\n\n Usage: cf restart [OPTIONS] [STACKS]...\n\n Restart running containers (docker compose restart).\n\n╭─ Arguments ────────────────────────────────────────────────────────────────────────────╮\n│   [stacks]...      TEXT  Stacks to operate on                                          │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n╭─ Options ──────────────────────────────────────────────────────────────────────────────╮\n│ --all      -a            Run on all stacks                                             │\n│ --host     -H      TEXT  Filter to stacks on this host                                 │\n│ --service  -s      TEXT  Target a specific service within the stack                    │\n│ --config   -c      PATH  Path to config file                                           │\n│ --help     -h            Show this message and exit.                                   │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n\n```\n\n\u003c!-- OUTPUT:END --\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eSee the output of \u003ccode\u003ecf update --help\u003c/code\u003e\u003c/summary\u003e\n\n\u003c!-- CODE:BASH:START --\u003e\n\u003c!-- echo '```yaml' --\u003e\n\u003c!-- export NO_COLOR=1 --\u003e\n\u003c!-- export TERM=dumb --\u003e\n\u003c!-- export TERMINAL_WIDTH=90 --\u003e\n\u003c!-- cf update --help --\u003e\n\u003c!-- echo '```' --\u003e\n\u003c!-- CODE:END --\u003e\n\u003c!-- OUTPUT:START --\u003e\n\u003c!-- ⚠️ This content is auto-generated by `markdown-code-runner`. --\u003e\n```yaml\n\n Usage: cf update [OPTIONS] [STACKS]...\n\n Update stacks (pull + build + up). Shorthand for 'up --pull --build'.\n\n╭─ Arguments ────────────────────────────────────────────────────────────────────────────╮\n│   [stacks]...      TEXT  Stacks to operate on                                          │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n╭─ Options ──────────────────────────────────────────────────────────────────────────────╮\n│ --all      -a            Run on all stacks                                             │\n│ --host     -H      TEXT  Filter to stacks on this host                                 │\n│ --service  -s      TEXT  Target a specific service within the stack                    │\n│ --config   -c      PATH  Path to config file                                           │\n│ --help     -h            Show this message and exit.                                   │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n\n```\n\n\u003c!-- OUTPUT:END --\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eSee the output of \u003ccode\u003ecf apply --help\u003c/code\u003e\u003c/summary\u003e\n\n\u003c!-- CODE:BASH:START --\u003e\n\u003c!-- echo '```yaml' --\u003e\n\u003c!-- export NO_COLOR=1 --\u003e\n\u003c!-- export TERM=dumb --\u003e\n\u003c!-- export TERMINAL_WIDTH=90 --\u003e\n\u003c!-- cf apply --help --\u003e\n\u003c!-- echo '```' --\u003e\n\u003c!-- CODE:END --\u003e\n\u003c!-- OUTPUT:START --\u003e\n\u003c!-- ⚠️ This content is auto-generated by `markdown-code-runner`. --\u003e\n```yaml\n\n Usage: cf apply [OPTIONS]\n\n Make reality match config (start, migrate, stop strays/orphans as needed).\n\n This is the \"reconcile\" command that ensures running stacks match your\n config file. It will:\n\n 1. Stop orphaned stacks (in state but removed from config)\n 2. Stop stray stacks (running on unauthorized hosts)\n 3. Migrate stacks on wrong host (host in state ≠ host in config)\n 4. Start missing stacks (in config but not in state)\n\n Use --dry-run to preview changes before applying.\n Use --no-orphans to skip stopping orphaned stacks.\n Use --no-strays to skip stopping stray stacks.\n Use --full to also run 'up' on all stacks (picks up compose/env changes).\n\n╭─ Options ──────────────────────────────────────────────────────────────────────────────╮\n│ --dry-run     -n            Show what would change without executing                   │\n│ --no-orphans                Only migrate, don't stop orphaned stacks                   │\n│ --no-strays                 Don't stop stray stacks (running on wrong host)            │\n│ --full        -f            Also run up on all stacks to apply config changes          │\n│ --config      -c      PATH  Path to config file                                        │\n│ --help        -h            Show this message and exit.                                │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n\n```\n\n\u003c!-- OUTPUT:END --\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eSee the output of \u003ccode\u003ecf compose --help\u003c/code\u003e\u003c/summary\u003e\n\n\u003c!-- CODE:BASH:START --\u003e\n\u003c!-- echo '```yaml' --\u003e\n\u003c!-- export NO_COLOR=1 --\u003e\n\u003c!-- export TERM=dumb --\u003e\n\u003c!-- export TERMINAL_WIDTH=90 --\u003e\n\u003c!-- cf compose --help --\u003e\n\u003c!-- echo '```' --\u003e\n\u003c!-- CODE:END --\u003e\n\u003c!-- OUTPUT:START --\u003e\n\u003c!-- ⚠️ This content is auto-generated by `markdown-code-runner`. --\u003e\n```yaml\n\n Usage: cf compose [OPTIONS] STACK COMMAND [ARGS]...\n\n Run any docker compose command on a stack.\n\n Passthrough to docker compose for commands not wrapped by cf.\n Options after COMMAND are passed to docker compose, not cf.\n\n Examples:\n   cf compose mystack --help        - show docker compose help\n   cf compose mystack top           - view running processes\n   cf compose mystack images        - list images\n   cf compose mystack exec web bash - interactive shell\n   cf compose mystack config        - view parsed config\n\n╭─ Arguments ────────────────────────────────────────────────────────────────────────────╮\n│ *    stack          TEXT  Stack to operate on (use '.' for current dir) [required]     │\n│ *    command        TEXT  Docker compose command [required]                            │\n│      [args]...      TEXT  Additional arguments                                         │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n╭─ Options ──────────────────────────────────────────────────────────────────────────────╮\n│ --host    -H      TEXT  Filter to stacks on this host                                  │\n│ --config  -c      PATH  Path to config file                                            │\n│ --help    -h            Show this message and exit.                                    │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n\n```\n\n\u003c!-- OUTPUT:END --\u003e\n\n\u003c/details\u003e\n\n**Configuration**\n\n\u003cdetails\u003e\n\u003csummary\u003eSee the output of \u003ccode\u003ecf traefik-file --help\u003c/code\u003e\u003c/summary\u003e\n\n\u003c!-- CODE:BASH:START --\u003e\n\u003c!-- echo '```yaml' --\u003e\n\u003c!-- export NO_COLOR=1 --\u003e\n\u003c!-- export TERM=dumb --\u003e\n\u003c!-- export TERMINAL_WIDTH=90 --\u003e\n\u003c!-- cf traefik-file --help --\u003e\n\u003c!-- echo '```' --\u003e\n\u003c!-- CODE:END --\u003e\n\u003c!-- OUTPUT:START --\u003e\n\u003c!-- ⚠️ This content is auto-generated by `markdown-code-runner`. --\u003e\n```yaml\n\n Usage: cf traefik-file [OPTIONS] [STACKS]...\n\n Generate a Traefik file-provider fragment from compose Traefik labels.\n\n╭─ Arguments ────────────────────────────────────────────────────────────────────────────╮\n│   [stacks]...      TEXT  Stacks to operate on                                          │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n╭─ Options ──────────────────────────────────────────────────────────────────────────────╮\n│ --all     -a            Run on all stacks                                              │\n│ --output  -o      PATH  Write Traefik file-provider YAML to this path (stdout if       │\n│                         omitted)                                                       │\n│ --config  -c      PATH  Path to config file                                            │\n│ --help    -h            Show this message and exit.                                    │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n\n```\n\n\u003c!-- OUTPUT:END --\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eSee the output of \u003ccode\u003ecf refresh --help\u003c/code\u003e\u003c/summary\u003e\n\n\u003c!-- CODE:BASH:START --\u003e\n\u003c!-- echo '```yaml' --\u003e\n\u003c!-- export NO_COLOR=1 --\u003e\n\u003c!-- export TERM=dumb --\u003e\n\u003c!-- export TERMINAL_WIDTH=90 --\u003e\n\u003c!-- cf refresh --help --\u003e\n\u003c!-- echo '```' --\u003e\n\u003c!-- CODE:END --\u003e\n\u003c!-- OUTPUT:START --\u003e\n\u003c!-- ⚠️ This content is auto-generated by `markdown-code-runner`. --\u003e\n```yaml\n\n Usage: cf refresh [OPTIONS] [STACKS]...\n\n Update local state from running stacks.\n\n Discovers which stacks are running on which hosts, updates the state\n file, and captures image digests. This is a read operation - it updates\n your local state to match reality, not the other way around.\n\n Without arguments: refreshes all stacks (same as --all).\n With stack names: refreshes only those stacks.\n\n Use 'cf apply' to make reality match your config (stop orphans, migrate).\n\n╭─ Arguments ────────────────────────────────────────────────────────────────────────────╮\n│   [stacks]...      TEXT  Stacks to operate on                                          │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n╭─ Options ──────────────────────────────────────────────────────────────────────────────╮\n│ --all       -a            Run on all stacks                                            │\n│ --config    -c      PATH  Path to config file                                          │\n│ --log-path  -l      PATH  Path to Dockerfarm TOML log                                  │\n│ --dry-run   -n            Show what would change without writing                       │\n│ --help      -h            Show this message and exit.                                  │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n\n```\n\n\u003c!-- OUTPUT:END --\u003e\n\n\u003c/details\u003e\n\n\n\u003cdetails\u003e\n\u003csummary\u003eSee the output of \u003ccode\u003ecf check --help\u003c/code\u003e\u003c/summary\u003e\n\n\u003c!-- CODE:BASH:START --\u003e\n\u003c!-- echo '```yaml' --\u003e\n\u003c!-- export NO_COLOR=1 --\u003e\n\u003c!-- export TERM=dumb --\u003e\n\u003c!-- export TERMINAL_WIDTH=90 --\u003e\n\u003c!-- cf check --help --\u003e\n\u003c!-- echo '```' --\u003e\n\u003c!-- CODE:END --\u003e\n\u003c!-- OUTPUT:START --\u003e\n\u003c!-- ⚠️ This content is auto-generated by `markdown-code-runner`. --\u003e\n```yaml\n\n Usage: cf check [OPTIONS] [STACKS]...\n\n Validate configuration, traefik labels, mounts, and networks.\n\n Without arguments: validates all stacks against configured hosts.\n With stack arguments: validates specific stacks and shows host compatibility.\n\n Use --local to skip SSH-based checks for faster validation.\n\n╭─ Arguments ────────────────────────────────────────────────────────────────────────────╮\n│   [stacks]...      TEXT  Stacks to operate on                                          │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n╭─ Options ──────────────────────────────────────────────────────────────────────────────╮\n│ --local                 Skip SSH-based checks (faster)                                 │\n│ --config  -c      PATH  Path to config file                                            │\n│ --help    -h            Show this message and exit.                                    │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n\n```\n\n\u003c!-- OUTPUT:END --\u003e\n\n\u003c/details\u003e\n\n\n\u003cdetails\u003e\n\u003csummary\u003eSee the output of \u003ccode\u003ecf init-network --help\u003c/code\u003e\u003c/summary\u003e\n\n\u003c!-- CODE:BASH:START --\u003e\n\u003c!-- echo '```yaml' --\u003e\n\u003c!-- export NO_COLOR=1 --\u003e\n\u003c!-- export TERM=dumb --\u003e\n\u003c!-- export TERMINAL_WIDTH=90 --\u003e\n\u003c!-- cf init-network --help --\u003e\n\u003c!-- echo '```' --\u003e\n\u003c!-- CODE:END --\u003e\n\u003c!-- OUTPUT:START --\u003e\n\u003c!-- ⚠️ This content is auto-generated by `markdown-code-runner`. --\u003e\n```yaml\n\n Usage: cf init-network [OPTIONS] [HOSTS]...\n\n Create Docker network on hosts with consistent settings.\n\n Creates an external Docker network that stacks can use for cross-host\n communication. Uses the same subnet/gateway on all hosts to ensure\n consistent networking.\n\n╭─ Arguments ────────────────────────────────────────────────────────────────────────────╮\n│   [hosts]...      TEXT  Hosts to create network on (default: all)                      │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n╭─ Options ──────────────────────────────────────────────────────────────────────────────╮\n│ --network  -n      TEXT  Network name [default: mynetwork]                             │\n│ --subnet   -s      TEXT  Network subnet [default: 172.20.0.0/16]                       │\n│ --gateway  -g      TEXT  Network gateway [default: 172.20.0.1]                         │\n│ --config   -c      PATH  Path to config file                                           │\n│ --help     -h            Show this message and exit.                                   │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n\n```\n\n\u003c!-- OUTPUT:END --\u003e\n\n\u003c/details\u003e\n\n\n\u003cdetails\u003e\n\u003csummary\u003eSee the output of \u003ccode\u003ecf config --help\u003c/code\u003e\u003c/summary\u003e\n\n\u003c!-- CODE:BASH:START --\u003e\n\u003c!-- echo '```yaml' --\u003e\n\u003c!-- export NO_COLOR=1 --\u003e\n\u003c!-- export TERM=dumb --\u003e\n\u003c!-- export TERMINAL_WIDTH=90 --\u003e\n\u003c!-- cf config --help --\u003e\n\u003c!-- echo '```' --\u003e\n\u003c!-- CODE:END --\u003e\n\u003c!-- OUTPUT:START --\u003e\n\u003c!-- ⚠️ This content is auto-generated by `markdown-code-runner`. --\u003e\n```yaml\n\n Usage: cf config [OPTIONS] COMMAND [ARGS]...\n\n Manage compose-farm configuration files.\n\n╭─ Options ──────────────────────────────────────────────────────────────────────────────╮\n│ --help  -h        Show this message and exit.                                          │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n╭─ Commands ─────────────────────────────────────────────────────────────────────────────╮\n│ init      Create a new config file with documented example.                            │\n│ edit      Open the config file in your default editor.                                 │\n│ show      Display the config file location and contents.                               │\n│ path      Print the config file path (useful for scripting).                           │\n│ validate  Validate the config file syntax and schema.                                  │\n│ symlink   Create a symlink from the default config location to a config file.          │\n│ init-env  Generate a .env file for Docker deployment.                                  │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n\n```\n\n\u003c!-- OUTPUT:END --\u003e\n\n\u003c/details\u003e\n\n\n\u003cdetails\u003e\n\u003csummary\u003eSee the output of \u003ccode\u003ecf ssh --help\u003c/code\u003e\u003c/summary\u003e\n\n\u003c!-- CODE:BASH:START --\u003e\n\u003c!-- echo '```yaml' --\u003e\n\u003c!-- export NO_COLOR=1 --\u003e\n\u003c!-- export TERM=dumb --\u003e\n\u003c!-- export TERMINAL_WIDTH=90 --\u003e\n\u003c!-- cf ssh --help --\u003e\n\u003c!-- echo '```' --\u003e\n\u003c!-- CODE:END --\u003e\n\u003c!-- OUTPUT:START --\u003e\n\u003c!-- ⚠️ This content is auto-generated by `markdown-code-runner`. --\u003e\n```yaml\n\n Usage: cf ssh [OPTIONS] COMMAND [ARGS]...\n\n Manage SSH keys for passwordless authentication.\n\n╭─ Options ──────────────────────────────────────────────────────────────────────────────╮\n│ --help  -h        Show this message and exit.                                          │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n╭─ Commands ─────────────────────────────────────────────────────────────────────────────╮\n│ keygen  Generate SSH key (does not distribute to hosts).                               │\n│ setup   Generate SSH key and distribute to all configured hosts.                       │\n│ status  Show SSH key status and host connectivity.                                     │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n\n```\n\n\u003c!-- OUTPUT:END --\u003e\n\n\u003c/details\u003e\n\n**Monitoring**\n\n\u003cdetails\u003e\n\u003csummary\u003eSee the output of \u003ccode\u003ecf logs --help\u003c/code\u003e\u003c/summary\u003e\n\n\u003c!-- CODE:BASH:START --\u003e\n\u003c!-- echo '```yaml' --\u003e\n\u003c!-- export NO_COLOR=1 --\u003e\n\u003c!-- export TERM=dumb --\u003e\n\u003c!-- export TERMINAL_WIDTH=90 --\u003e\n\u003c!-- cf logs --help --\u003e\n\u003c!-- echo '```' --\u003e\n\u003c!-- CODE:END --\u003e\n\u003c!-- OUTPUT:START --\u003e\n\u003c!-- ⚠️ This content is auto-generated by `markdown-code-runner`. --\u003e\n```yaml\n\n Usage: cf logs [OPTIONS] [STACKS]...\n\n Show stack logs. With --service, shows logs for just that service.\n\n╭─ Arguments ────────────────────────────────────────────────────────────────────────────╮\n│   [stacks]...      TEXT  Stacks to operate on                                          │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n╭─ Options ──────────────────────────────────────────────────────────────────────────────╮\n│ --all      -a               Run on all stacks                                          │\n│ --host     -H      TEXT     Filter to stacks on this host                              │\n│ --service  -s      TEXT     Target a specific service within the stack                 │\n│ --follow   -f               Follow logs                                                │\n│ --tail     -n      INTEGER  Number of lines (default: 20 for --all, 100 otherwise)     │\n│ --config   -c      PATH     Path to config file                                        │\n│ --help     -h               Show this message and exit.                                │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n\n```\n\n\u003c!-- OUTPUT:END --\u003e\n\n\u003c/details\u003e\n\n\n\u003cdetails\u003e\n\u003csummary\u003eSee the output of \u003ccode\u003ecf ps --help\u003c/code\u003e\u003c/summary\u003e\n\n\u003c!-- CODE:BASH:START --\u003e\n\u003c!-- echo '```yaml' --\u003e\n\u003c!-- export NO_COLOR=1 --\u003e\n\u003c!-- export TERM=dumb --\u003e\n\u003c!-- export TERMINAL_WIDTH=90 --\u003e\n\u003c!-- cf ps --help --\u003e\n\u003c!-- echo '```' --\u003e\n\u003c!-- CODE:END --\u003e\n\u003c!-- OUTPUT:START --\u003e\n\u003c!-- ⚠️ This content is auto-generated by `markdown-code-runner`. --\u003e\n```yaml\n\n Usage: cf ps [OPTIONS] [STACKS]...\n\n Show status of stacks.\n\n Without arguments: shows all stacks (same as --all).\n With stack names: shows only those stacks.\n With --host: shows stacks on that host.\n With --service: filters to a specific service within the stack.\n\n╭─ Arguments ────────────────────────────────────────────────────────────────────────────╮\n│   [stacks]...      TEXT  Stacks to operate on                                          │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n╭─ Options ──────────────────────────────────────────────────────────────────────────────╮\n│ --all      -a            Run on all stacks                                             │\n│ --host     -H      TEXT  Filter to stacks on this host                                 │\n│ --service  -s      TEXT  Target a specific service within the stack                    │\n│ --config   -c      PATH  Path to config file                                           │\n│ --help     -h            Show this message and exit.                                   │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n\n```\n\n\u003c!-- OUTPUT:END --\u003e\n\n\u003c/details\u003e\n\n\n\u003cdetails\u003e\n\u003csummary\u003eSee the output of \u003ccode\u003ecf stats --help\u003c/code\u003e\u003c/summary\u003e\n\n\u003c!-- CODE:BASH:START --\u003e\n\u003c!-- echo '```yaml' --\u003e\n\u003c!-- export NO_COLOR=1 --\u003e\n\u003c!-- export TERM=dumb --\u003e\n\u003c!-- export TERMINAL_WIDTH=90 --\u003e\n\u003c!-- cf stats --help --\u003e\n\u003c!-- echo '```' --\u003e\n\u003c!-- CODE:END --\u003e\n\u003c!-- OUTPUT:START --\u003e\n\u003c!-- ⚠️ This content is auto-generated by `markdown-code-runner`. --\u003e\n```yaml\n\n Usage: cf stats [OPTIONS]\n\n Show overview statistics for hosts and stacks.\n\n Without flags: Shows config/state info (hosts, stacks, pending migrations).\n With --live: Also queries Docker on each host for container counts.\n With --containers: Shows per-container resource stats (requires Glances).\n\n╭─ Options ──────────────────────────────────────────────────────────────────────────────╮\n│ --live        -l            Query Docker for live container stats                      │\n│ --containers  -C            Show per-container resource stats (requires Glances)       │\n│ --host        -H      TEXT  Filter to stacks on this host                              │\n│ --config      -c      PATH  Path to config file                                        │\n│ --help        -h            Show this message and exit.                                │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n\n```\n\n\u003c!-- OUTPUT:END --\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eSee the output of \u003ccode\u003ecf list --help\u003c/code\u003e\u003c/summary\u003e\n\n\u003c!-- CODE:BASH:START --\u003e\n\u003c!-- echo '```yaml' --\u003e\n\u003c!-- export NO_COLOR=1 --\u003e\n\u003c!-- export TERM=dumb --\u003e\n\u003c!-- export TERMINAL_WIDTH=90 --\u003e\n\u003c!-- cf list --help --\u003e\n\u003c!-- echo '```' --\u003e\n\u003c!-- CODE:END --\u003e\n\u003c!-- OUTPUT:START --\u003e\n\u003c!-- ⚠️ This content is auto-generated by `markdown-code-runner`. --\u003e\n```yaml\n\n Usage: cf list [OPTIONS]\n\n List all stacks and their assigned hosts.\n\n╭─ Options ──────────────────────────────────────────────────────────────────────────────╮\n│ --host    -H      TEXT  Filter to stacks on this host                                  │\n│ --simple  -s            Plain output (one stack per line, for scripting)               │\n│ --config  -c      PATH  Path to config file                                            │\n│ --help    -h            Show this message and exit.                                    │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n\n```\n\n\u003c!-- OUTPUT:END --\u003e\n\n\u003c/details\u003e\n\n**Server**\n\n\u003cdetails\u003e\n\u003csummary\u003eSee the output of \u003ccode\u003ecf web --help\u003c/code\u003e\u003c/summary\u003e\n\n\u003c!-- CODE:BASH:START --\u003e\n\u003c!-- echo '```yaml' --\u003e\n\u003c!-- export NO_COLOR=1 --\u003e\n\u003c!-- export TERM=dumb --\u003e\n\u003c!-- export TERMINAL_WIDTH=90 --\u003e\n\u003c!-- cf web --help --\u003e\n\u003c!-- echo '```' --\u003e\n\u003c!-- CODE:END --\u003e\n\u003c!-- OUTPUT:START --\u003e\n\u003c!-- ⚠️ This content is auto-generated by `markdown-code-runner`. --\u003e\n```yaml\n\n Usage: cf web [OPTIONS]\n\n Start the web UI server.\n\n╭─ Options ──────────────────────────────────────────────────────────────────────────────╮\n│ --host    -H      TEXT     Host to bind to [default: 0.0.0.0]                          │\n│ --port    -p      INTEGER  Port to listen on [default: 8000]                           │\n│ --reload  -r               Enable auto-reload for development                          │\n│ --help    -h               Show this message and exit.                                 │\n╰────────────────────────────────────────────────────────────────────────────────────────╯\n\n```\n\n\u003c!-- OUTPUT:END --\u003e\n\n\u003c/details\u003e\n\n### Auto-Migration\n\nWhen you change a stack's host assignment in config and run `up`, Compose Farm automatically:\n1. Checks that required mounts and networks exist on the new host (aborts if missing)\n2. Runs `down` on the old host\n3. Runs `up -d` on the new host\n4. Updates state tracking\n\nUse `cf apply` to automatically reconcile all stacks—it finds and migrates stacks on wrong hosts, stops orphaned stacks, and starts missing stacks.\n\n```yaml\n# Before: plex runs on server-1\nstacks:\n  plex: server-1\n\n# After: change to server-2, then run `cf up plex`\nstacks:\n  plex: server-2  # Compose Farm will migrate automatically\n```\n\n**Orphaned stacks**: When you remove (or comment out) a stack from config, it becomes \"orphaned\"—tracked in state but no longer in config. Use these commands to handle orphans:\n\n- `cf apply` — Migrate stacks AND stop orphans (the full reconcile)\n- `cf down --orphaned` — Only stop orphaned stacks\n- `cf apply --dry-run` — Preview what would change before applying\n\nThis makes the config truly declarative: comment out a stack, run `cf apply`, and it stops.\n\n## Traefik Multihost Ingress (File Provider)\n\nIf you run a single Traefik instance on one \"front‑door\" host and want it to route to\nCompose Farm stacks on other hosts, Compose Farm can generate a Traefik file‑provider\nfragment from your existing compose labels.\n\n**How it works**\n\n- Your `docker-compose.yml` remains the source of truth. Put normal `traefik.*` labels on\n  the container you want exposed.\n- Labels and port specs may use `${VAR}` / `${VAR:-default}`; Compose Farm resolves these\n  using the stack's `.env` file and your current environment, just like Docker Compose.\n- Publish a host port for that container (via `ports:`). The generator prefers\n  host‑published ports so Traefik can reach the stack across hosts; if none are found,\n  it warns and you'd need L3 reachability to container IPs.\n- If a router label doesn't specify `traefik.http.routers.\u003cname\u003e.service` and there's only\n  one Traefik service defined on that container, Compose Farm wires the router to it.\n- `compose-farm.yaml` stays unchanged: just `hosts` and `stacks: stack → host`.\n\nExample `docker-compose.yml` pattern:\n\n```yaml\nservices:\n  plex:\n    ports: [\"32400:32400\"]\n    labels:\n      - traefik.enable=true\n      - traefik.http.routers.plex.rule=Host(`plex.lab.mydomain.org`)\n      - traefik.http.routers.plex.entrypoints=websecure\n      - traefik.http.routers.plex.tls.certresolver=letsencrypt\n      - traefik.http.services.plex.loadbalancer.server.port=32400\n```\n\n**One‑time Traefik setup**\n\nEnable a file provider watching a directory (any path is fine; a common choice is on your\nshared/NFS mount):\n\n```yaml\nproviders:\n  file:\n    directory: /mnt/data/traefik/dynamic.d\n    watch: true\n```\n\n**Generate the fragment**\n\n```bash\ncf traefik-file --all --output /mnt/data/traefik/dynamic.d/compose-farm.yml\n```\n\nRe‑run this after changing Traefik labels, moving a stack to another host, or changing\npublished ports.\n\n**Auto-regeneration**\n\nTo automatically regenerate the Traefik config after `up`, `down`, or `update`,\nadd `traefik_file` to your config:\n\n```yaml\ncompose_dir: /opt/compose\ntraefik_file: /opt/traefik/dynamic.d/compose-farm.yml  # auto-regenerate on up/down/update\ntraefik_stack: traefik  # skip stacks on same host (docker provider handles them)\n\nhosts:\n  # ...\nstacks:\n  traefik: server-1  # Traefik runs here\n  plex: server-2     # Stacks on other hosts get file-provider entries\n  # ...\n```\n\nThe `traefik_stack` option specifies which stack runs Traefik. Stacks on the same host\nare skipped in the file-provider config since Traefik's docker provider handles them directly.\n\nNow `cf up plex` will update the Traefik config automatically—no separate\n`traefik-file` command needed.\n\n**Combining with existing config**\n\nIf you already have a `dynamic.yml` with manual routes, middlewares, etc., move it into the\ndirectory and Traefik will merge all files:\n\n```bash\nmkdir -p /opt/traefik/dynamic.d\nmv /opt/traefik/dynamic.yml /opt/traefik/dynamic.d/manual.yml\ncf traefik-file --all -o /opt/traefik/dynamic.d/compose-farm.yml\n```\n\nUpdate your Traefik config to use directory watching instead of a single file:\n\n```yaml\n# Before\n- --providers.file.filename=/dynamic.yml\n\n# After\n- --providers.file.directory=/dynamic.d\n- --providers.file.watch=true\n```\n\n## Host Resource Monitoring (Glances)\n\nThe web UI can display real-time CPU, memory, and load stats for all configured hosts. This uses [Glances](https://nicolargo.github.io/glances/), a cross-platform system monitoring tool with a REST API.\n\n**Setup**\n\n1. Deploy a Glances stack that runs on all hosts:\n\n```yaml\n# glances/compose.yaml\nname: glances\nservices:\n  glances:\n    image: nicolargo/glances:latest\n    container_name: glances\n    restart: unless-stopped\n    pid: host\n    ports:\n      - \"61208:61208\"\n    environment:\n      - GLANCES_OPT=-w  # Enable web server mode\n    volumes:\n      - /var/run/docker.sock:/var/run/docker.sock:ro\n```\n\n2. Add it to your config as a multi-host stack:\n\n```yaml\n# compose-farm.yaml\nstacks:\n  glances: all  # Runs on every host\n\nglances_stack: glances  # Enables resource stats in web UI\n```\n\n3. Deploy: `cf up glances`\n\n4. **(Docker web UI only)** The web UI container infers the local host from `CF_WEB_STACK` and reaches Glances via the container name to avoid Docker network isolation issues.\n\nThe web UI dashboard will now show a \"Host Resources\" section with live stats from all hosts. Hosts where Glances is unreachable show an error indicator.\n\n**Live Stats Page**\n\nWith Glances configured, a Live Stats page (`/live-stats`) shows all running containers across all hosts:\n\n- **Columns**: Stack, Service, Host, Image, Status, Uptime, CPU, Memory, Net I/O\n- **Features**: Sorting, filtering, live updates (no SSH required—uses Glances REST API)\n\n## Comparison with Alternatives\n\nThere are many ways to run containers on multiple hosts. Here is where Compose Farm sits:\n\n| | Compose Farm | Docker Contexts | K8s / Swarm | Ansible / Terraform | Portainer / Coolify |\n|---|:---:|:---:|:---:|:---:|:---:|\n| No compose rewrites | ✅ | ✅ | ❌ | ✅ | ✅ |\n| Version controlled | ✅ | ✅ | ✅ | ✅ | ❌ |\n| State tracking | ✅ | ❌ | ✅ | ✅ | ✅ |\n| Auto-migration | ✅ | ❌ | ✅ | ❌ | ❌ |\n| Interactive CLI | ✅ | ❌ | ❌ | ❌ | ❌ |\n| Parallel execution | ✅ | ❌ | ✅ | ✅ | ✅ |\n| Agentless | ✅ | ✅ | ❌ | ✅ | ❌ |\n| High availability | ❌ | ❌ | ✅ | ❌ | ❌ |\n\n**Docker Contexts** — You can use `docker context create remote ssh://...` and `docker compose --context remote up`. But it's manual: you must remember which host runs which stack, there's no global view, no parallel execution, and no auto-migration.\n\n**Kubernetes / Docker Swarm** — Full orchestration that abstracts away the hardware. But they require cluster initialization, separate control planes, and often rewriting compose files. They introduce complexity (consensus, overlay networks) unnecessary for static \"pet\" servers.\n\n**Ansible / Terraform** — Infrastructure-as-Code tools that can SSH in and deploy containers. But they're push-based configuration management, not interactive CLIs. Great for setting up state, clumsy for day-to-day operations like `cf logs -f` or quickly restarting a stack.\n\n**Portainer / Coolify** — Web-based management UIs. But they're UI-first and often require agents on your servers. Compose Farm is CLI-first and agentless.\n\n**Compose Farm is the middle ground:** a robust CLI that productizes the manual SSH pattern. You get the \"cluster feel\" (unified commands, state tracking) without the \"cluster cost\" (complexity, agents, control planes).\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbasnijholt%2Fcompose-farm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbasnijholt%2Fcompose-farm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbasnijholt%2Fcompose-farm/lists"}