Compose Farm - run docker compose commands across multiple hosts
Project description
Compose Farm
A minimal CLI tool to run Docker Compose commands across multiple hosts via SSH.
Why Compose Farm?
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:
- Kubernetes: Overkill for my use case. I don't need pods, services, ingress controllers, or YAML manifests 10x the size of my compose files.
- Docker Swarm: Effectively in maintenance mode—no longer being invested in by Docker.
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.
Key Assumption: Shared Storage
Compose Farm assumes all your compose files are accessible at the same path on all hosts. This is typically achieved via:
- NFS mount (e.g.,
/opt/composemounted from a NAS) - Synced folders (e.g., Syncthing, rsync)
- Shared filesystem (e.g., GlusterFS, Ceph)
# Example: NFS mount on all hosts
nas:/volume1/compose → /opt/compose (on nas01)
nas:/volume1/compose → /opt/compose (on nas02)
nas:/volume1/compose → /opt/compose (on nas03)
Compose Farm simply runs docker compose -f /opt/compose/{service}/docker-compose.yml on the appropriate host—it doesn't copy or sync files.
Installation
pip install compose-farm
# or
uv pip install compose-farm
Configuration
Create ~/.config/compose-farm/compose-farm.yaml (or ./compose-farm.yaml in your working directory):
compose_dir: /opt/compose # Must be the same path on all hosts
hosts:
nas01:
address: 192.168.1.10
user: docker
nas02:
address: 192.168.1.11
# user defaults to current user
local: localhost # Run locally without SSH
services:
plex: nas01
jellyfin: nas02
sonarr: nas01
radarr: local # Runs on the machine where you invoke compose-farm
Compose files are expected at {compose_dir}/{service}/docker-compose.yml.
Usage
# Start services
compose-farm up plex jellyfin
compose-farm up --all
# Stop services
compose-farm down plex
# Pull latest images
compose-farm pull --all
# Restart (down + up)
compose-farm restart plex
# Update (pull + down + up) - the end-to-end update command
compose-farm update --all
# Capture image digests to a TOML log (per service or all)
compose-farm snapshot plex
compose-farm snapshot --all # writes ~/.config/compose-farm/dockerfarm-log.toml
# View logs
compose-farm logs plex
compose-farm logs -f plex # follow
# Show status
compose-farm ps
Traefik Multihost Ingress (File Provider)
If you run a single Traefik instance on one “front‑door” host and want it to route to Compose Farm services on other hosts, Compose Farm can generate a Traefik file‑provider fragment from your existing compose labels.
How it works
- Your
docker-compose.ymlremains the source of truth. Put normaltraefik.*labels on the container you want exposed. - Labels and port specs may use
${VAR}/${VAR:-default}; Compose Farm resolves these using the stack’s.envfile and your current environment, just like Docker Compose. - Publish a host port for that container (via
ports:). The generator prefers host‑published ports so Traefik can reach the service across hosts; if none are found, it warns and you’d need L3 reachability to container IPs. - If a router label doesn’t specify
traefik.http.routers.<name>.serviceand there’s only one Traefik service defined on that container, Compose Farm wires the router to it. compose-farm.yamlstays unchanged: justhostsandservices: service → host.
Example docker-compose.yml pattern:
services:
plex:
ports: ["32400:32400"]
labels:
- traefik.enable=true
- traefik.http.routers.plex.rule=Host(`plex.lab.mydomain.org`)
- traefik.http.routers.plex.entrypoints=websecure
- traefik.http.routers.plex.tls.certresolver=letsencrypt
- traefik.http.services.plex.loadbalancer.server.port=32400
One‑time Traefik setup
Enable a file provider watching a directory (any path is fine; a common choice is on your shared/NFS mount):
providers:
file:
directory: /mnt/data/traefik/dynamic.d
watch: true
Generate the fragment
compose-farm traefik-file --output /mnt/data/traefik/dynamic.d/compose-farm.generated.yml
Re‑run this after changing Traefik labels, moving a service to another host, or changing published ports.
Requirements
- Python 3.11+
- SSH key-based authentication to your hosts (uses ssh-agent)
- Docker and Docker Compose installed on all target hosts
- Shared storage: All compose files at the same path on all hosts (NFS, Syncthing, etc.)
How It Works
- You run
compose-farm up plex - Compose Farm looks up which host runs
plex(e.g.,nas01) - It SSHs to
nas01(or runs locally iflocalhost) - It executes
docker compose -f /opt/compose/plex/docker-compose.yml up -d - Output is streamed back with
[plex]prefix
That's it. No orchestration, no service discovery, no magic.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file compose_farm-0.3.0.tar.gz.
File metadata
- Download URL: compose_farm-0.3.0.tar.gz
- Upload date:
- Size: 82.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
52409ca028f045498df639a518c03de0ebd75293ff7343382b1684fdc98e3069
|
|
| MD5 |
5f928e0e5b7288f8cf5e4f8f445c4563
|
|
| BLAKE2b-256 |
c4f6c6c545c87eaa785deb81f7a7094fc26a8c36bb7fe0b4f2f9e8c351bef0a4
|
Provenance
The following attestation bundles were made for compose_farm-0.3.0.tar.gz:
Publisher:
release.yml on basnijholt/compose-farm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
compose_farm-0.3.0.tar.gz -
Subject digest:
52409ca028f045498df639a518c03de0ebd75293ff7343382b1684fdc98e3069 - Sigstore transparency entry: 763594553
- Sigstore integration time:
-
Permalink:
basnijholt/compose-farm@5d21e64781ff09da3eefb4dfa08955a77e969604 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/basnijholt
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5d21e64781ff09da3eefb4dfa08955a77e969604 -
Trigger Event:
release
-
Statement type:
File details
Details for the file compose_farm-0.3.0-py3-none-any.whl.
File metadata
- Download URL: compose_farm-0.3.0-py3-none-any.whl
- Upload date:
- Size: 18.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
05b3db86d54a097696e929fd25126183ac9ac2f3971a2a64857c08b8eb9a92da
|
|
| MD5 |
aa025242f22456d088eba661cb8196ee
|
|
| BLAKE2b-256 |
6ad77fdec4fb5fc9a907b8a7003d3e2a696343537c88ff4eb962df73f95da50f
|
Provenance
The following attestation bundles were made for compose_farm-0.3.0-py3-none-any.whl:
Publisher:
release.yml on basnijholt/compose-farm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
compose_farm-0.3.0-py3-none-any.whl -
Subject digest:
05b3db86d54a097696e929fd25126183ac9ac2f3971a2a64857c08b8eb9a92da - Sigstore transparency entry: 763594557
- Sigstore integration time:
-
Permalink:
basnijholt/compose-farm@5d21e64781ff09da3eefb4dfa08955a77e969604 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/basnijholt
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5d21e64781ff09da3eefb4dfa08955a77e969604 -
Trigger Event:
release
-
Statement type: