Odysseus CLI
Command-line interface for deploying Docker containers with zero-downtime using Caddy as a reverse proxy.
Installation
gem install odysseus-cli
Or add to your Gemfile:
gem 'odysseus-cli'
Quick Start
- Create a
deploy.ymlin your project:
service: myapp
image: myregistry/myapp
servers:
web:
hosts:
- server1.example.com
jobs:
hosts:
- server1.example.com
cmd: bundle exec good_job
proxy:
hosts:
- myapp.example.com
app_port: 3000
ssl: true
ssl_email: [email protected]
healthcheck:
path: /health
interval: 10
timeout: 5
env:
clear:
RAILS_ENV: production
secret:
- DATABASE_URL
- RAILS_MASTER_KEY
ssh:
user: root
keys:
- ~/.ssh/id_ed25519
- Build and deploy:
# Build, distribute, and deploy in one command
odysseus deploy --image v1.0.0 --build
The --build flag automatically chooses how to distribute the image:
- Without
registryconfig → uses pussh to transfer images directly via SSH - With
registryconfig → pushes to registry, hosts pull from there
Commands
deploy
Deploy all roles to their configured hosts.
odysseus deploy [options]
Options:
--config FILE- Path to deploy.yml (default: deploy.yml)--image TAG- Docker image tag (default: latest)--build- Build and distribute image before deploying--dry-run- Show what would be deployed without doing it-v, --verbose- Show SSH commands being executed
The --build flag automatically chooses the distribution method based on your config:
- No
registryconfig → uses pussh (direct SSH transfer to each host) - Has
registryconfig → pushes to registry (hosts pull from there)
Examples:
# Deploy existing image
odysseus deploy --image v1.0.0
# Build, distribute, and deploy in one command
odysseus deploy --image v1.0.0 --build
build
Build Docker image locally or on a remote build host.
odysseus build [options]
Options:
--config FILE- Path to deploy.yml (default: deploy.yml)--image TAG- Docker image tag (default: latest)--push- Push image to registry after build--context PATH- Build context path (default: . relative to deploy.yml)-v, --verbose- Show build commands being executed
Examples:
# Build locally
odysseus build --image v1.0.0
# Build and push to registry
odysseus build --image v1.0.0 --push
# Build with custom context path
odysseus build --image v1.0.0 --context ./app
pussh
Push Docker image directly to hosts via SSH (no registry needed). Uses docker-pussh/unregistry to transfer images.
Note: When no
registryis configured,odysseus deploy --buildautomatically uses pussh. This command is useful for manually pushing images without deploying.
odysseus pussh [options]
Options:
--config FILE- Path to deploy.yml (default: deploy.yml)--image TAG- Docker image tag (default: latest)--build- Build image before pushing-v, --verbose- Show commands being executed
Examples:
# Push existing local image to all hosts
odysseus pussh --image v1.0.0
# Build and push in one step
odysseus pussh --image v1.0.0 --build
Prerequisites: Install docker-pussh on your local machine:
# macOS/Linux
curl -fsSL https://github.com/psviderski/unregistry/releases/latest/download/docker-pussh-$(uname -s)-$(uname -m) \
-o ~/.docker/cli-plugins/docker-pussh && chmod +x ~/.docker/cli-plugins/docker-pussh
status
Show service status on a server.
odysseus status <server> [--config FILE]
containers
List containers for the service on a server.
odysseus containers <server> [--config FILE] [--service NAME]
logs
Show logs for a service.
odysseus logs <server> [options]
Options:
--role ROLE- Role to show logs for: web, jobs, etc (default: web)-f, --follow- Follow log output-n, --lines N- Number of lines to show (default: 100)--since TIME- Show logs since timestamp (e.g., '10m', '2h')
cleanup
Clean up old containers and optionally prune images.
odysseus cleanup <server> [--prune-images]
validate
Validate your deploy.yml configuration.
odysseus validate [--config FILE]
accessory
Manage accessories (databases, Redis, etc).
# These commands use hosts from accessory config (no server argument needed)
odysseus accessory boot --name db
odysseus accessory boot-all
odysseus accessory remove --name db
odysseus accessory restart --name db
odysseus accessory upgrade --name db
odysseus accessory status
# These commands require a server argument
odysseus accessory logs <server> --name db [-f] [-n 100]
odysseus accessory exec <server> --name db --command "psql -U postgres"
odysseus accessory shell <server> --name db
Accessory commands like boot, remove, restart, upgrade, and status read the target hosts from the accessory's hosts configuration in deploy.yml, similar to how deploy works. Only logs, exec, and shell require a server argument since they operate on a specific host.
app
Run commands in app containers.
odysseus app shell <server>
odysseus app exec <server> --command "rails db:migrate"
odysseus app console <server> [--cmd "rails c"]
secrets
Manage encrypted secrets files.
# Generate a new master key
odysseus secrets generate-key
# Encrypt a plaintext secrets file
odysseus secrets encrypt --input secrets.yml --file secrets.yml.enc
# Decrypt and display secrets (values are masked)
odysseus secrets decrypt --file secrets.yml.enc
# Edit encrypted secrets using $EDITOR
odysseus secrets edit --file secrets.yml.enc
The master key should be set as ODYSSEUS_MASTER_KEY environment variable.
Configuration Reference
service
The name of your service. Used for container naming and Caddy routing.
image
The Docker image name (without tag). Tags are specified at deploy time.
servers
Define roles and their target hosts:
servers:
web:
hosts:
- web1.example.com
- web2.example.com
options:
memory: 4g
cpus: 2
jobs:
hosts:
- worker1.example.com
cmd: bundle exec good_job
options:
memory: 2g
cpus: 1.5
Available options:
memory- Hard memory limit (e.g.,4g,512m)memory_reservation- Soft memory limitcpus- CPU limit (e.g.,2for 2 cores,1.5for 1.5 cores)cpu_shares- Relative CPU weight (default: 1024)
Dynamic hosts with AWS Auto Scaling Groups
Instead of a static hosts list, you can configure Odysseus to resolve hosts dynamically from an AWS Auto Scaling Group:
servers:
web:
aws:
asg: my-web-asg # ASG name (required)
region: us-east-1 # AWS region (required)
use_private_ip: false # Use private IPs instead of public (default: false)
state: InService # Instance lifecycle state filter (default: InService)
options:
memory: 4g
AWS credentials are loaded from the standard AWS credential chain:
- Environment variables (
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY) - Shared credentials file (
~/.aws/credentials) - IAM instance profile (when running on EC2)
Prerequisites: Install the AWS SDK gems:
gem install aws-sdk-autoscaling aws-sdk-ec2
Or add to your Gemfile:
gem 'aws-sdk-autoscaling'
gem 'aws-sdk-ec2'
SSH configuration (bastions, ProxyJump, etc.) is your responsibility. Odysseus only needs the hostnames/IPs and relies on your local SSH config.
proxy
Caddy reverse proxy configuration:
proxy:
hosts:
- myapp.example.com
- www.myapp.example.com
app_port: 3000
ssl: true
ssl_email: [email protected]
healthcheck:
path: /health
interval: 10
timeout: 5
expect_status: 200 # Optional: expected HTTP status (default: 2xx)
env
Environment variables:
env:
clear:
RAILS_ENV: production
secret:
- DATABASE_URL
- RAILS_MASTER_KEY
clear- Plaintext values stored in deploy.ymlsecret- Keys to load from encrypted secrets file or server environment
secrets_file
Path to an encrypted secrets file (relative to deploy.yml or absolute):
secrets_file: secrets.yml.enc
Create the encrypted file using odysseus secrets encrypt. The secrets file should be YAML format:
# secrets.yml (before encryption)
DATABASE_URL: postgres://user:pass@db/myapp
RAILS_MASTER_KEY: abc123def456
During deploy, secrets listed in env.secret are loaded from the encrypted file. If a key is not found in the secrets file, it falls back to the server's environment variables.
accessories
Long-running services like databases:
accessories:
db:
image: postgres:16
hosts:
- db.example.com
volumes:
- /var/lib/odysseus/myapp/postgres:/var/lib/postgresql/data
env:
clear:
POSTGRES_USER: myapp
POSTGRES_DB: myapp_production
healthcheck:
cmd: pg_isready -U myapp
interval: 10
timeout: 5
Each accessory must define hosts - the servers where it should run.
ssh
SSH connection settings:
ssh:
user: root
keys:
- ~/.ssh/id_ed25519
builder
Configuration for building Docker images:
builder:
strategy: local # 'local' or 'remote'
host: build-server # Required if strategy is 'remote'
dockerfile: Dockerfile # Dockerfile name (default: Dockerfile)
context: . # Build context path (default: .)
arch: amd64 # Target architecture
build_args: # Build arguments
RUBY_VERSION: "3.2"
NODE_VERSION: "18"
cache: true # Use Docker build cache (default: true)
push: false # Auto-push after build (default: false)
multiarch: false # Multi-platform builds with buildx
platforms: # Platforms for multi-arch builds
- linux/amd64
- linux/arm64
Build strategies:
local- Build on the local machine (default)remote- Build on a remote host via SSH (useful for CI or dedicated build servers)
registry
Docker registry configuration. When present, odysseus deploy --build will push images to the registry instead of using pussh:
registry:
server: docker.io # Registry server (required to enable registry mode)
username: myuser # Registry username
password: mypassword # Registry password (consider using secrets)
Image distribution modes:
| Config | deploy --build behavior |
|---|---|
No registry |
Build locally → pussh to each host via SSH |
Has registry |
Build locally → push to registry → hosts pull |
For better security, you can store registry credentials in your encrypted secrets file and reference them.
Server Requirements
Your target servers only need Docker installed. Odysseus automatically deploys and manages Caddy as a container (odysseus-caddy) - no manual Caddy installation required.
How It Works
- Ensure Caddy starts the Caddy container if not running
- Deploy starts a new container with the specified image tag
- Health check waits for the container to become healthy
- Caddy update adds the new container to the upstream pool
- Drain removes old containers from Caddy and waits for connections to close
- Cleanup stops old containers and removes all but the 2 most recent
- Stale upstream cleanup removes any Caddy routes pointing to stopped containers
This ensures zero-downtime deployments with automatic rollback if health checks fail.
License
MIT