Manage remote services from a single CLI — Docker Compose stacks and bare-metal scripts alike. Syncs files via rsync, then controls services over SSH.
Local (the machine running herd):
bash4+sshwith key-based auth configured for every target server (no password prompts)rsync
Remote (each target server):
-
Docker and Docker Compose
curl -fsSL https://get.docker.com | shThis installs Docker Engine and the Compose plugin (
docker compose) in one shot. -
aria2c— only required if you useherd dl# Debian / Ubuntu sudo apt install aria2
herd uses short host aliases everywhere (my-server, my-pve, …). Define them
in ~/.ssh/config so SSH knows how to reach each host:
Host my-server
HostName 192.168.1.50
User docker
IdentityFile ~/.ssh/id_ed25519
Host my-pve
HostName 192.168.1.10
User root
IdentityFile ~/.ssh/id_ed25519
Host my-vm
HostName 192.168.1.20
User ubuntu
IdentityFile ~/.ssh/id_ed25519
Test each connection before using herd:
ssh my-server "docker ps"Copy herd somewhere on your PATH and make it executable:
cp herd ~/.local/bin/herd
chmod +x ~/.local/bin/herdOr symlink it so changes in this repo take effect immediately:
ln -s "$PWD/herd" ~/.local/bin/herdherd discovers its workspace by walking up from $PWD until it finds a
servers.conf file (same behaviour as git finding .git). The directory
containing servers.conf is the workspace root.
Maps SSH host aliases to the remote directory where services live.
A template is provided at servers.conf.example — copy and edit it:
# host-alias = /remote/path/to/appdata
my-unraid = /mnt/flash/appdata
my-server = /home/docker
my-pve = /root
my-vm = ~/dockerEach server gets a subdirectory. Inside it, each service gets its own folder.
herd syncs the local service folder to the matching path on the remote host.
my-services/ ← workspace root (contains servers.conf)
├── servers.conf
├── herd
├── my-server/
│ ├── nginx/
│ │ ├── compose.yaml
│ │ └── .env ← gitignored
│ └── postgres/
│ └── compose.yaml
├── my-pve/
│ └── my-agent/
│ └── install.sh ← bare-metal, no Docker
└── my-vm/
└── grafana-stack/
└── compose.yaml
.envfiles and secrets are gitignored. See.gitignore.
herd ships with completion for bash and zsh. Servers and services are
completed dynamically at tab-press time — no hardcoded lists.
bash — add to ~/.bashrc:
eval "$(herd completion bash)"zsh — add to ~/.zshrc:
eval "$(herd completion zsh)"| Command | Arguments | What it does |
|---|---|---|
deploy |
<server> <service> |
Sync files then docker compose up -d |
sync |
<server> <service> |
Sync files only, no restart |
down |
<server> <service> |
docker compose down — stop and remove containers |
restart |
<server> <service> |
docker compose restart — restart in-place |
pull |
<server> <service> |
Pull latest images |
logs |
<server> <service> [args] |
View logs — pass -f to follow |
ps |
<server> |
List all containers on a server |
exec |
<server> <container> [cmd] |
Open a shell inside a running container |
| Command | Arguments | What it does |
|---|---|---|
run |
<server> <service> <script> |
Sync files then run a shell script on the host |
dl |
<server> <url> [dir] |
Download a file on the remote host (aria2c must be installed there) |
cpy |
<server> <remote> <local> |
Copy a file or directory from remote to local |
# Deploy (sync + bring up) nginx on my-server
herd deploy my-server nginx
# Follow logs, last 50 lines
herd logs my-server nginx -f --tail 50
# List all containers on my-pve
herd ps my-pve
# Open a shell in the nginx container
herd exec my-server nginx
# Run a one-off command without opening a full shell
herd exec my-server nginx "nginx -t"
# Run a bare-metal install script on my-pve
herd run my-pve my-agent install.sh
# Download a file to a specific remote directory
herd dl my-unraid https://example.com/file.tar.gz /mnt/flash/downloads
# Pull a file back to your local machine
herd cpy my-server /home/docker/nginx/nginx.conf ./backups/Set HERD_WORKSPACE to point herd at a specific workspace regardless of
where you run it from:
export HERD_WORKSPACE=/home/user/work/internal-services