Agentless remote deployment tool for Docker Compose and K3s.
ssd is a lightweight CLI tool that simplifies deploying containerized applications to remote servers via SSH. Supports both Docker Compose and K3s (Kubernetes) runtimes. No agents, no complex setup—just SSH access.
- Simple: Convention-over-configuration approach
- Flexible: Works with monorepos and simple projects
- Agentless: Only requires SSH access
- Dual runtime: Docker Compose or K3s — same ssd.yaml, same commands
- Smart: Auto-increments build numbers
- Fast: Builds on the server, no image registry needed
- Reliable: Zero-downtime deployments with automatic version tracking
- Polished output: Docker-style live progress in your terminal — spinner + per-step timer, frozen ✓/✗ summary on completion. Falls back to plain text in CI and pipes automatically.
Quick install (Linux/macOS)
curl -sSL https://raw.githubusercontent.com/byteink/ssd/main/install.sh | shHomebrew (macOS/Linux)
brew install byteink/tap/ssdGo
go install github.com/byteink/ssd@latestLinux packages
Download from Releases:
- Debian/Ubuntu:
ssd_*_linux_amd64.deb - RHEL/Fedora:
ssd_*_linux_amd64.rpm
Windows
Download ssd_Windows_x86_64.zip from Releases, extract, and add to PATH.
- Initialize your project:
# Interactive mode
ssd init
# Or with flags
ssd init -s myserver -d myapp.example.com -p 3000- Deploy:
ssd deploy appThat's it! ssd will:
- Sync your code to the server via rsync
- Build the container image on the server
- Auto-increment the version number
- Update compose.yaml (or K8s manifests for k3s) and restart the stack
# Initialize with K3s runtime
ssd init -s myserver -r k3s -d myapp.example.com -p 3000
# Provision the server (installs K3s, nerdctl, buildkit, configures Traefik)
ssd provision
# Deploy
ssd deploy appssd looks up its config in this order:
--config <path>(explicit override).ssd/ssd.yaml(preferred — keeps the repo root clean)ssd.yaml(legacy — kept for back-compat with existing projects)
Fresh ssd init writes to .ssd/ssd.yaml and adds .ssd/.gitignore
so generated artifacts under .ssd/.cache/ stay out of version
control. Existing projects with ./ssd.yaml are left alone.
If you're still on the legacy layout, ssd migrate moves your
./ssd.yaml into .ssd/ssd.yaml and seeds the .gitignore. Until
you migrate, every command prints a one-line warning to stderr.
For multiple environments, drop sibling files next to the base config:
.ssd/
├── ssd.yaml # base / shared
├── ssd.dev.yaml # dev overlay
└── ssd.prod.yaml # prod overlay
Apply with --env:
ssd deploy --env prod
ssd deploy -e devOverlays are deep-merged onto the base — only the keys you set in the overlay are overridden, everything else inherits.
# ssd.yaml
server: myserver
services:
app:
# name defaults to service key ("app")
# stack defaults to /stacks/app# ssd.yaml
server: myserver
stack: /custom/stacks/myapp # Shared by all services
services:
web:
name: myapp-web
context: ./apps/web
dockerfile: ./apps/web/Dockerfile# ssd.yaml
server: myserver
stack: /stacks/myproject # All services share this stack
services:
web:
context: ./apps/web
dockerfile: ./apps/web/Dockerfile
api:
context: ./apps/api
dockerfile: ./apps/api/DockerfileDeploy specific service:
ssd deploy web# ssd.yaml
server: myserver
services:
web:
name: myapp-web
stack: /stacks/myapp
context: ./apps/web
dockerfile: ./apps/web/Dockerfile
target: production # Docker build target stage (optional)
domain: example.com # Enable Traefik routing
path: /api # Path prefix routing (optional)
https: true # Default true, set false to disable
port: 3000 # Container port, default 80
ports: # Host:container port mappings (optional)
- "3000:3000"
- "8080:80"
depends_on: # Simple list or map with conditions
- db
- redis
files:
./config.yaml: /app/config.yaml # Local file -> container path
volumes:
postgres-data: /var/lib/postgresql/data
redis-data: /data
healthcheck:
cmd: "curl -f http://localhost:3000/health || exit 1"
interval: 30s
timeout: 10s
retries: 3server: myserver
services:
api:
files:
./config.yaml: /app/config.yaml # relative to project
/opt/shared/ca.pem: /etc/ssl/ca.pem # absolute path outside projectCopy local files to the stack directory and bind-mount into the container. Files are transferred via SSH on every deploy, independent of git tracking (works with .gitignored files). Relative paths resolve from the working directory where ssd is run. Absolute paths work for files outside the project. Basenames must be unique per service.
# ssd.yaml
server: myserver
services:
web:
depends_on:
db:
condition: service_healthy
redis:
condition: service_startedConditions: service_started (default), service_healthy (requires healthcheck), service_completed_successfully.
# ssd.yaml
server: myserver
services:
app:
ports:
- "3000:3000" # Expose on host for Tailscale/CF tunnelWhen no domain or domains is set, the service is deployed without Traefik labels or the traefik_web network. Use ports to map host:container ports for access via Tailscale, Cloudflare tunnels, or direct host access.
# ssd.yaml
server: myserver
services:
nginx:
image: nginx:latest # Use pre-built image, skip build step
domain: example.com# ssd.yaml
server: myserver
services:
web:
domains:
- example.com
- www.example.com
- api.example.com
port: 3000All domains work independently, no redirects. Useful for multi-brand apps, different locales, or A/B testing.
# ssd.yaml
server: myserver
services:
web:
domains:
- example.com
- www.example.com
- old-domain.com
redirect_to: example.com # All other domains redirect to this
port: 3000When redirect_to is set, all other domains automatically redirect to it with a 302 temporary redirect. Common use cases:
- www redirect: Redirect www to non-www (or vice versa)
- Domain migration: Redirect old domains to new primary domain
- Multi-TLD consolidation: Redirect .net, .org to primary .com
# ssd.yaml
server: myserver
stack: /stacks/myapp
services:
api:
context: ./apps/api
dockerfile: ./apps/api/Dockerfile
domain: api.example.com
port: 8080
depends_on:
- db
healthcheck:
cmd: "curl -f http://localhost:8080/health || exit 1"
interval: 30s
timeout: 10s
retries: 3
db:
image: postgres:16-alpine
volumes:
postgres-data: /var/lib/postgresql/data
healthcheck:
cmd: "pg_isready -U postgres"
interval: 10s
timeout: 5s
retries: 5Service-level fields:
name: Service name (defaults to service key)stack: Path to stack directory on server (defaults to/stacks/{name})context: Build context path (defaults to.)dockerfile: Dockerfile path (defaults to./Dockerfile)image: Pre-built image to use (skips build step if specified)target: Docker build target stage for multi-stage builds (e.g.,production)domain: Single domain for Traefik routingdomains: Multiple domains for Traefik routing. Cannot use bothdomainanddomainsredirect_to: When set, all domains except this one redirect to it (302 temporary). Must be one of the domains indomainsarraypath: Path prefix for routing (e.g.,/api). Requiresdomainordomains. GeneratesPathPrefixrule withStripPrefixmiddlewarehttps: Enable HTTPS (default:true)port: Container port (default:80)ports: Host:container port mappings (e.g.,["3000:3000"]). Maps directly to Docker Composeports:depends_on: Service dependencies (list or map with conditions)volumes: Map of volume names to mount pathsfiles: Map of local file paths to container mount paths. Copied to stack directory and bind-mounted on every deploy. Works with.gitignored fileshealthcheck: Health check configuration (exactly one ofcmd/exec)cmd: Shell command, rendered as["CMD","sh","-c",cmd]exec: Array form, rendered as["CMD",arg0,arg1,...]. Use for scratch images with no shell.interval: Check interval (e.g.,30s)timeout: Command timeout (e.g.,10s)retries: Number of retries before unhealthy
Root-level fields:
server: SSH server name (from~/.ssh/config)stack: Default stack path for all services
ssd init # Interactive mode
ssd init -s myserver # Non-interactive with flagsFlags:
-s, --server- SSH host name (required in non-interactive mode)--stack- Stack path (e.g.,/dockge/stacks/myapp)--service- Service name (default:app)-d, --domain- Domain for Traefik routing--path- Path prefix for routing (e.g.,/api)-p, --port- Container port-f, --force- Overwrite existingssd.yaml
ssd deploy|up [service] # Deploy service (or all if omitted)
ssd down [service] # Stop services (or all if omitted)
ssd rm [service] # Permanently remove services (or entire stack)
ssd restart <service> # Restart without rebuilding
ssd rollback <service> # Rollback to previous version
ssd status <service> # Check container status
ssd logs <service> [-f] # View logs, -f to follow
ssd scale <service> <count> # Live-scale a service (does not edit ssd.yaml)Set a persistent replica count in ssd.yaml:
services:
web:
deploy:
replicas: 3 # default 1- k3s: written to Deployment
spec.replicasand applied on deploy. - compose: written to
services.<svc>.deploy.replicas. Docker Compose v2 honors this in non-swarm mode only when deploying withdocker compose --compatibility. ssd does NOT add this flag; document it in your own deploy wrapper if you need >1 replica persisted across restarts. For ephemeral scaling, usessd scale.
Live-scale without editing ssd.yaml (matches kubectl scale):
ssd scale web 3
ssd scale worker 0 # scale down to zeroDeploy behavior:
- With no argument, deploys all services in alphabetical order
- With a service name, deploys that single service
- Dependencies are started first (respects
depends_on) - Example:
ssd deploy apiwill also startdbifapidepends on it
ssd config # Show all services config
ssd config <service> # Show specific service configssd env <service> set KEY=VALUE # Set environment variable
ssd env <service> list # List all environment variables
ssd env <service> rm KEY # Remove environment variableNote: Environment variables are stored in {service}.env files in the stack directory on the server. For k3s, they are synced into a {service}-env ConfigMap on every deploy.
services:
web:
env_file: ./.env # local path, relative to project rootWhen env_file is set, the local file is uploaded to
{stack}/{service}.env (mode 600) on every deploy. This overwrites
any values set via ssd env set. To manage env vars via CLI only, remove
env_file from ssd.yaml first.
ssd provision # Provision server from ssd.yaml
ssd provision --server myserver --email admin@x.com # Explicit server and email
ssd provision check # Verify server readiness
ssd provision check --server myserver # Check a specific serverProvisions the target server with:
- Docker and Docker Compose installation
- docker-rollout plugin for zero-downtime deploys
- Traefik reverse proxy with automatic HTTPS (Let's Encrypt),
--pingendpoint, and Docker healthcheck traefik_webDocker network for service discovery
All steps are idempotent and safe to run multiple times.
provision check verifies that Docker, Docker Compose, docker-rollout, the traefik_web network, and Traefik are all present and running.
ssd reclaims disk space on the server in two ways: automatically on deploy (tag retention, per-service) and manually via ssd prune (orphans, images, build cache, dangling).
Per-deploy tag retention:
cleanup:
retention: 2 # keep last N image tags (root default, applied to all services)
services:
web:
cleanup:
retention: 5 # per-service override- Default retention is 2 (current + rollback target).
- Minimum is 1;
0disables auto cleanup on deploy. - Per-deploy cleanup is warn-only — it never fails a deploy.
Manual prune:
ssd prune # Remove orphaned services (default, preserved)
ssd prune --images # Remove old image tags beyond per-service retention
ssd prune --build-cache # Prune build cache entries older than 168h
ssd prune --dangling # Remove unreferenced images
ssd prune --all # All of the above
ssd prune --keep N # Override retention for --images/--all
ssd prune --dry-run # Preview, combinable with any flagBuild cache pruning is opt-in only — never runs automatically on deploy. Threshold is 168h (7 days).
ssd completion install # auto-detect $SHELL (bash, zsh, fish)
ssd completion install --shell zsh # pick the shell explicitly
ssd completion bash > /path/to/ssd # print script to stdoutInstalled paths:
- bash:
~/.local/share/bash-completion/completions/ssd - zsh:
~/.zsh/completions/_ssd(add the directory tofpathin.zshrcbeforecompinit) - fish:
~/.config/fish/completions/ssd.fish(auto-loaded by fish)
Completes top-level commands, sub-commands (env/secret set|list|rm,
provision check, completion install), common flags, and dynamically
lists services from your ssd.yaml.
ssd version # Show version
ssd help # Show help- Reads
ssd.yamlfrom current directory - SSHs into the configured server (uses
~/.ssh/config) - Rsyncs code to a temp directory (excludes .git, node_modules, .next)
- Builds Docker image on the server (or skips if using pre-built
image) - Parses current version from compose.yaml, increments it
- Recreates the service with
docker compose up -d --force-recreate - Cleans up temp directory
- SSH access to target server (configured in
~/.ssh/config) - Docker and Docker Compose on the server
- A
compose.yamlalready set up in the stack directory - rsync installed locally
# Clone and setup
git clone https://github.com/byteink/ssd.git
cd ssd
make setup # Configures git hooks for linting
# Build and test
make build # Build binary
make test # Run tests
make lint # Run linterssd ships with a Claude Code skill that lets Claude deploy and manage your services via the /ssd slash command.
ssd skillThis symlinks the bundled skill directory into ~/.claude/skills/ssd. The skill auto-updates whenever you run brew upgrade ssd.
/ssd deploy web
/ssd status api
/ssd logs web -f
/ssd rollback api
Or ask Claude naturally and it will use ssd when appropriate.
MIT
Built by ByteInk