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

  1. Create a deploy.yml in 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
  1. 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 registry config → uses pussh to transfer images directly via SSH
  • With registry config → 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 registry config → uses pussh (direct SSH transfer to each host)
  • Has registry config → 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 registry is configured, odysseus deploy --build automatically 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 limit
  • cpus - CPU limit (e.g., 2 for 2 cores, 1.5 for 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.yml
  • secret - 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

  1. Ensure Caddy starts the Caddy container if not running
  2. Deploy starts a new container with the specified image tag
  3. Health check waits for the container to become healthy
  4. Caddy update adds the new container to the upstream pool
  5. Drain removes old containers from Caddy and waits for connections to close
  6. Cleanup stops old containers and removes all but the 2 most recent
  7. 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