README
¶
cdeploy

cdeploy is a TUI/CLI app for teams and solo developers who deploy and manage Docker Compose stacks — locally or on a few servers over SSH.
Instead of SSH-ing into each machine and running this rollout by hand:
docker compose stop
docker compose rm -f
docker compose pull
docker compose up --no-start
docker compose start
cdeploy wraps it into a single command or terminal UI.
No daemon. No agents to install on your servers. No cluster orchestrator. Single binary. Plain SSH.
Why cdeploy?
cdeploy is not a replacement for Kubernetes, Docker Swarm, or full deployment platforms like Kamal. It's for teams and solo developers who deploy to a handful of servers with plain docker compose and just want a faster, less error-prone way to do it — without installing anything on the servers themselves.
Requirements
- Go 1.26+ (for
go installor building from source) - Docker Compose v2 plugin (
docker compose) or v1 standalone (docker-compose) — auto-detected - SSH client (for remote server support)
Install
Prebuilt binaries (recommended): grab the latest .tar.gz, .deb, or .rpm for your platform from the Releases page. Linux and macOS, amd64 and arm64.
From source:
go install github.com/lexxzar/compose-deploy@latest
Or clone and build:
git clone https://github.com/lexxzar/compose-deploy.git
cd compose-deploy
go build -o cdeploy .
Usage
TUI Mode
Run without arguments to launch the interactive interface:
cdeploy
After you select a remote server, the server name is shown in the breadcrumb on subsequent screens. If that server has a color set in ~/.cdeploy/servers.yml, the breadcrumb renders it as a colored badge; if color is omitted, the breadcrumb stays plain text.
The TUI has six main screens, plus an inline settings editor reachable from screen 1:
- Server select — choose a remote server or "Local" (only shown when servers are configured); press
sto open the settings editor for managing servers - Project select — pick a Docker Compose project (auto-skipped if the current directory has a compose file)
- Service select — pick services and choose an action (
rrestart,ddeploy,sstop,llogs,cconfig,xexec,Ure-check updates); also shows CPU% and Mem (used/limit) columns for running services, refreshed on screen entry and after every operation. Services whose registry image is newer than the local copy get a yellow⇧marker next to the service name; the indicator is cached for 10 minutes andUforces a refresh. - Progress — watch step-by-step execution with status indicators
- Logs — live-stream logs for the selected service
- Config — inspect or edit the compose file, toggle between raw and resolved config, and see validation status
Navigation
| Key | Action |
|---|---|
esc |
Back to the previous screen; cancels confirmation prompts and in-flight operations |
q |
Back on nested screens (alias for esc); quits on root screens |
ctrl+c |
Quit. On remote sessions, prompts to confirm the disconnect (y to quit, n/esc to cancel) |
q is typeable inside settings-form text inputs and is a no-op on the progress screen while an operation is in flight — use esc to cancel.
CLI Mode
# Deploy specific containers (stop → remove → pull → create → start)
cdeploy deploy nginx postgres
# Deploy all containers
cdeploy deploy -a
# Restart specific containers (stop → remove → create → start)
cdeploy restart nginx
# Restart all containers
cdeploy restart -a
# Stop specific containers
cdeploy stop nginx
# List services and their status
cdeploy list
# List services with CPU and memory usage (~1.5s per discovered project)
cdeploy list --stats
# List services as JSON (for scripts and CI)
cdeploy list --json
# Combine stats with JSON output
cdeploy list --stats --json
# Show image-update indicators (opt-in; one registry probe per service)
cdeploy list --updates
cdeploy list -C /opt/myapp --updates
cdeploy list -s prod --updates --json
# Stream logs for a service
cdeploy logs nginx
# Dump last 100 lines and exit
cdeploy logs nginx -n 100 --no-follow
# Exec into a running container (default: tries bash, falls back to sh)
cdeploy exec nginx
# Run a specific command inside a container
cdeploy exec web -- rails console
list --json output (one entry per service, grouped by project):
[
{
"project": "myapp",
"service": "nginx",
"running": true,
"health": "healthy",
"created": "2026-05-14 10:22",
"uptime": "2d",
"ports": [
{ "host": "0.0.0.0", "host_port": 8080, "container_port": 80, "protocol": "tcp" }
],
"update_available": true
}
]
health, created, uptime, ports, and update_available are omitted when not applicable (no healthcheck, stopped container, no published ports, update check not performed or build-only service).
With --stats, three additional fields are populated per running service: cpu_percent (100.0 = one full core; sums across replicas for scaled services), memory_used (bytes), memory_limit (bytes; equals host memory when no explicit limit is set). The fields are omitted entirely when --stats is not passed, so existing scripts see byte-identical output. On stats fetch failure the CLI prints cdeploy: stats unavailable: <err> to stderr (single-project mode) or cdeploy: stats unavailable for "<project>": <err> (multi-project mode), exits 0, and renders blank cells — status is the load-bearing primary view.
Update-available indicators (--updates)
A yellow ⇧ glyph next to a service name in cdeploy list and the TUI service-select screen means the image in the registry has a different digest than the locally pulled copy. The check runs docker compose config --format json to map services to images, then per-image docker image inspect (local RepoDigest) and docker buildx imagetools inspect (registry manifest-list digest), falling back to docker manifest inspect --verbose when the buildx plugin is unavailable. Build-only services and services whose digest cannot be determined render a blank cell — the indicator is tri-state (unknown / current / update available).
--updates is opt-in in both single- and multi-project modes — each service costs one registry round-trip (buildx/manifest-inspect), and projects with many services (especially over SSH) can take 10+ seconds. Omit the flag for fast cdeploy list invocations; add it when you actually want to know what's behind.
Failures are non-fatal: cdeploy: updates unavailable: <err> (single-project) or cdeploy: updates unavailable for "<project>": <err> (multi-project) is written to stderr, the cell stays blank, exit code 0. JSON output adds update_available: true|false with omitempty; existing JSON consumers see the original wire shape when the flag is absent.
In the TUI, the indicator is cached for 10 minutes per (project, server) context. Press U on the service-select screen to bypass the cache and re-check immediately.
Multi-arch images: for multi-arch images (commonly: nginx, postgres, alpine, node, redis), the check uses docker buildx imagetools inspect which returns the manifest-LIST digest — matching what docker image inspect records locally — so multi-arch images are reported correctly. The legacy docker manifest inspect --verbose fallback (used only when the buildx plugin is unavailable, i.e. very old Docker installs) returns per-platform descriptor digests that never match the local manifest-list digest and can produce false positives for multi-arch images; upgrading to Docker v23+ (which ships buildx by default) eliminates that case. Run docker pull manually to confirm a flagged update before deploying if you suspect a false positive.
Exit codes: 0 on success, non-zero on failure (config errors, SSH/Docker failures, validation errors). Suitable for CI gating.
Remote servers (CLI)
# Deploy all containers on a configured remote server
cdeploy -s prod-web deploy -a
# Restart a service on a remote server with explicit project directory
cdeploy -s staging -C /opt/apps/web restart nginx
# List services on a remote server
cdeploy -s prod list
# Stream logs on a remote server
cdeploy logs nginx -s prod -C /opt/myapp
# Exec into a container on a remote server
cdeploy exec nginx -s prod -C /opt/myapp
Ad-hoc SSH connection (-S/--ssh)
For one-off remote operations (CI scripts, automation) without a ~/.cdeploy/servers.yml entry, pass an SSH connection string directly:
# Deploy against an ad-hoc host (uses default SSH user from ~/.ssh/config)
cdeploy deploy -S host -C /srv/app -a
# Deploy with explicit user
cdeploy deploy -S deploy@host -C /srv/app -a
# Restart with a non-default SSH port
cdeploy restart -S deploy@host:2222 -C /srv/app nginx
# List services on an ad-hoc host
cdeploy list -S deploy@host -C /srv/app
# Stream logs on an ad-hoc host
cdeploy logs nginx -S deploy@host -C /srv/app
# Exec into a container on an ad-hoc host
cdeploy exec nginx -S deploy@host -C /srv/app
# Use an ad-hoc SSH key (CI/automation workflows that write keys from secrets)
cdeploy -S deploy@1.2.3.4 -i ~/.ssh/ci.pem -C /opt/app deploy
The connection string format is [user@]host[:port]. The -S/--ssh flag is mutually exclusive with -s/--server and requires -C/--project-dir (no config lookup is performed).
SSH identity (-i/--identity): pass an SSH private key path inline. Only valid alongside -S/--ssh; intended for CI/ephemeral use where writing a ~/.ssh/config entry is impractical. For configured servers, use IdentityFile in ~/.ssh/config instead. The path supports ~/ expansion and is validated at parse time (must exist, be a regular file, and be readable).
CI usage: --ssh requires passwordless SSH authentication on the target host — configure keys via ~/.ssh/config, ssh-agent, or -i/--identity before running. Host-key verification still applies; either pre-populate ~/.ssh/known_hosts or use the standard StrictHostKeyChecking settings in your SSH config.
Global Flags
-s, --server string Remote server name from ~/.cdeploy/servers.yml
-S, --ssh string Ad-hoc SSH connection string [user@]host[:port] (mutually exclusive with --server)
-i, --identity string Path to SSH private key (requires --ssh)
-C, --project-dir string Docker compose project directory (default: current directory)
--log-dir string Log directory (default ~/.cdeploy/logs/)
Remote Server Configuration
Need a one-off connection without editing the config file? See Ad-hoc SSH connection (
-S/--ssh) above for a CLI-only alternative aimed at scripts and CI.
Define remote servers in ~/.cdeploy/servers.yml. Colors are defined once per group and inherited by every server in that group:
groups:
- name: Dev
color: green
- name: Production
color: red
servers:
- name: app.dev
host: deploy@app.dev
group: Dev
- name: discovery.dev
host: deploy@discovery.dev
group: Dev
- name: app.prod
host: deploy@app.prod
group: Production
- name: discovery.prod
host: deploy@discovery.prod
project_dir: /opt/apps/web
group: Production
Ungrouped servers may set color directly on the server entry. Older configs with per-server colors on grouped servers are auto-migrated on load (first-server-wins per group); rewriting them in the format above is recommended.
Top-level fields
| Field | Description |
|---|---|
groups[].name |
Group identifier referenced by servers[].group |
groups[].color |
Breadcrumb badge color shared by every server in the group |
servers[] |
List of server entries (see below) |
Server fields
| Field | Required | Description |
|---|---|---|
name |
yes | Identifier used in TUI and --server flag |
host |
yes | SSH destination (user@hostname) |
project_dir |
no | Default project directory on the remote host |
group |
no | Name of a group defined in groups: — servers with the same group are displayed together and share the group's color |
color |
no | Breadcrumb badge color. Only meaningful for ungrouped servers; ignored when group is set (the group's color wins) |
Allowed color values: red, green, yellow, blue, magenta, cyan, white, gray. A common pattern is to mark production servers red so they stand out before you run an operation.
SSH-specific options (keys, jump hosts, tunnels, ports) belong in ~/.ssh/config — cdeploy uses the system ssh binary and inherits its configuration. Exception: for ad-hoc CI/automation use, -i/--identity may be passed alongside -S/--ssh to supply a key path inline without a ~/.ssh/config entry.
How it works
cdeploy uses SSH ControlMaster multiplexing:
- A persistent control socket is established once (password/key prompts happen here)
- All subsequent docker compose commands and compose-file access reuse the socket with zero auth overhead
- The socket is torn down on disconnect or TUI quit
In TUI mode, the SSH connect command runs with full terminal access so interactive prompts (passwords, host key verification) work naturally.
Operations
| Operation | Steps |
|---|---|
| Deploy | stop → remove → pull → create → start |
| Restart | stop → remove → create → start |
| Stop | stop |
Health Checks
If your services define Docker health checks, cdeploy displays their status alongside the running/stopped indicator:
- ♥ healthy
- ✗ unhealthy
- ~ starting (health check hasn't passed yet)
For scaled services, the worst-case health is shown (unhealthy > starting > healthy). Services without a health check show only the running/stopped dot. The same icons appear in cdeploy list output.
Logging
All docker compose output is logged to ~/.cdeploy/logs/. Each log file is named cdeploy_on_{hostname}_{timestamp}.log, so you get a per-host, timestamped record of every operation. Override the directory with --log-dir.
Compose Config Screen
From the service screen, press c to open the compose config viewer/editor. This works for both local projects and remote servers selected through the TUI.
rtoggles between the raw compose file and resolved/interpolateddocker compose configoutputeopens the compose file in your editor. Local mode uses$EDITOR, then$VISUAL, thenvi; values likecode --waitare supported. Remote mode runs${EDITOR:-vi}over SSH on the target host.- After the editor exits, cdeploy reloads the raw file, switches back to raw view, and validates it with
docker compose config --quiet. Validation errors are shown inline in the TUI.
License
Documentation
¶
There is no documentation for this package.