CLI Reference
deploy
Deploy your application to production servers.
Synopsis
odysseus deploy [options]
Deploy your Docker containers to configured servers with zero-downtime rolling updates.
Options
--config FILE
Path to configuration file. Default: deploy.yml
odysseus deploy --config production.yml --image v1.0.0
--image TAG
Docker image tag to deploy. Optional — and the two ways of ending up with a version are not equivalent.
Omit it, from a clean git repository. Odysseus takes the version from the commit SHA and refuses to deploy while the working tree has uncommitted changes — so the tag it uses provably identifies the code that's running. It records that commit, and your identity (git's configured email, falling back to $USER), in each host's deploy log. This is the proper route.
Pass it explicitly, with --image TAG. No git repository is needed, so it works anywhere — the right tool for trying something out, or for deploying an image built elsewhere. It still records who ran the deploy; what it withholds is the commit, because an arbitrary tag has no commit it honestly identifies. It also doesn't stop the same tag being deployed twice, which leaves two entries in the deploy log odysseus can't tell apart.
Outside a git repository, --image stops being optional: with nothing to take a version from, odysseus has no version to deploy without it.
The cost of the quick route is felt at rollback time — see What a version can tell you.
odysseus deploy # clean git repo: version = commit SHA
odysseus deploy --image v1.0.0 # explicit tag: works anywhere
odysseus deploy --image latest
--build
Build the Docker image before deploying.
odysseus deploy --build --image v1.0.0
When used with a registry configuration, the image is pushed after building. Without a registry, the image is transferred directly to servers.
--dry-run
Show what would be deployed without making changes. No image is pulled, no container is started, and no host is touched.
odysseus deploy --dry-run --image v1.0.0
Output — four lines per role/host, not one: deploy_role (executor.rb:243-247) prints the Dry run - would deploy... line, then Service: and Role: unconditionally, then Proxy hosts: too when the role is web. Nothing filters these out of the streamed step, so all of them show up as sub-steps, repeating the Service: the header already printed once:
Odysseus Deploy
Service: myapp
Image: myapp:v1.0.0
01 > Deploying service
01 ✓ Dry run - would deploy myapp:v1.0.0 to app1.example.com
01 ✓ Service: myapp
01 ✓ Role: web
01 ✓ Proxy hosts: app.example.com
01 ✓ Dry run - would deploy myapp:v1.0.0 to app2.example.com
01 ✓ Service: myapp
01 ✓ Role: web
01 ✓ Proxy hosts: app.example.com
02 ✓ Deployment successful in 0.3s
The last line always carries a duration (cli.rb:69-70 times the whole deploy and passes it to deploy_complete) and its own step number — it's never the bare Deployment successful with nothing after it.
--verbose, -v
This is not the same as --debug for deploy, even though both touch the same command — see Global options for why they're different flags with different effects. -v only makes the executor and every SSH connection it opens verbose (the same Connecting to.../Connected to.../ > <command> lines --debug shows for the SSH layer); it does not switch the step-by-step spinner UI to plain text — that only happens under --debug or ODYSSEUS_DEBUG=1, which deploy never sets from -v (bin/odysseus:15, cli.rb:30).
odysseus deploy --verbose --image v1.0.0
Examples
Basic deployment
Deploy a pre-built image:
odysseus deploy --image v1.0.0
Build and deploy
Build locally and deploy:
odysseus deploy --build --image v1.0.0
Deploy with custom config
odysseus deploy --config staging.yml --image v1.0.0
Debug deployment
odysseus deploy --verbose --image v1.0.0
Test before deploying
odysseus deploy --dry-run --image v1.0.0
Deployment process
For each server and role, Odysseus:
- Ensures Caddy is running - Starts it if it isn't already (web roles only)
- Starts new container - With configured environment and options
- Waits for health check - Web roles always health-check (default timeout 60s); other roles only if that role has its own
healthcheckconfigured, otherwise odysseus pauses briefly for startup instead - Updates Caddy routing - For web roles only
- Drains and removes old containers - Out of the load balancer, then stopped and removed (
web_deploy.rb:252-253) — the container a deploy replaces never lingers as a stopped one - Cleans up crashed containers - Removes stopped (
exited) containers beyond the 2 most recent for this role and host (docker/client.rb:299-301). Because step 5 already removes the container being replaced, what this sweep finds is containers that exited on their own — crashes, not deploy history
Exit codes
| Code | Meaning |
|---|---|
| 0 | Deployment succeeded on every role and host |
| 1 | Configuration failed to load, a health check failed, or a host errored mid-deploy |
Environment variables
ODYSSEUS_MASTER_KEY
Master key for decrypting secrets.
ODYSSEUS_MASTER_KEY=your-key odysseus deploy --image v1.0.0