A journey: Odysseus

Announcing Odysseus a small docker+ssh+caddy based deployment tool.

A journey: Odysseus

I built a deployment tool because our infrastructure got hacked. In a few sessions I had all the blocks I wanted back online. I used that opportunity to actually build the deployment tool I needed: inspired by Kamal, and relying on Caddy.

Meet Odysseus: a Docker deployment tool that's simple, transparent, and boring in the best way possible.

The Story

Three weeks ago, a Coolify instance was compromised with an xmrig miner stemming from an Umami vulnerability. Fortunately, it stayed isolated within the containers. I immediately shut everything down and rebuilt from scratch. Since everything runs in containers, I wanted something simple to orchestrate them and connect them with Caddy as the reverse proxy. Docker Compose was an option—I've used it before—but the workflow and commands never quite stick between infrequent deployments.

So: script it, or a Makefile?

I've been using Kamal for several months on a client project. It's solid—clean code, thoughtful design, robust features. But it ships with kamal-proxy as the reverse proxy, not Caddy. And Caddy has an active development community, a vibrant plugin ecosystem, and genuinely excellent design. Using it directly made more sense.

That got me thinking: why not build a Kamal-like tool but with Caddy? Three reasons:

  1. Use Caddy. It's the right tool for the job. Worth choosing directly instead of through kamal-proxy.
  2. Learn how this works. Replicate Kamal's features quickly to understand deployment tools better. Turns out, the core architecture is elegant and simple.
  3. Independence. Not tied to 37signals' decisions or philosophy. I get to choose the direction.

So I built Odysseus.

What Odysseus Does

One Command to Deploy

odysseus deploy production --image myapp:v1.2.3

That's it. It builds the image, push it to a registry or directly to the server, starts the new container, updates Caddy, stops the old container. The app is live.

Works with anything Docker: Rails, Ghost, Django, Node, anything containerized.

  • Deploy, restart, console, shell, logs, status — all the commands you need
  • Multi-server — deploy to one server or a fleet in parallel
  • Roles (Worker jobs) & accessories — background workers, databases, Redis, etc
  • Caddy integration — automatic HTTPS, healthchecks, clean config
  • SSH based — so it even supports using Tailscale SSH, zero-trust networking, no exposed ports
  • Secrets management — encrypted file with environment variable decryption, simple and secure

Actually Simple

The core is a couple of thousands lines of Ruby. You can read it, understand it, modify it. No magic. Just:

  1. Parse your config (YAML, borrowed from Kamal)
  2. Run docker commands over SSH
  3. Talks to Caddy through its API
  4. Reload Caddy

That's the architecture.

Why This Matters

Transparency

As Odysseus runs it shows the commands being run and HTTP calls being made. Not entirely a blackbox.

Composable

Odysseus is focused on deploying containers and running a few commands around this (exec, logs, status, ...). One thing it's not doing: setting up servers. That's one difference with Kamal I wanted to have. In my case I prefer to use OpenToFu to setup servers, but as we merely need SSH + Docker installed ... pretty much anything could do really, even a simple cloud-init script.

Boring

Odysseus doesn't try to be everything. It deploys Docker containers and manages your reverse proxy. In infrastructure, boring is good. Boring means stable, predictable, debuggable.

Configuration is Familiar

service: myapp
image: myapp:v1.0.0

servers:
  web:
    hosts: [dedalus-prod]
    options:
      memory: 2g

proxy:
  hosts: [app.example.com]
  app_port: 3000

env:
  clear:
    RAILS_ENV: production
  secret:
    - DATABASE_URL
    - RAILS_MASTER_KEY

Same format as Kamal. Easy if you're coming from there, easy if you're new. I will try to keep it as close to kamal as possible for the core features, but it might diverge at some point.

Useful Commands

odysseus console production    # SSH into your container
odysseus shell production -- "rails db:migrate"
odysseus logs production       # What's happening?
odysseus status production     # What's deployed?

These save you when things go sideways.

The Philosophy

Both Kamal and Odysseus do the same job: deploy Docker containers reliably. Different philosophy on one key choice:

Kamal: Uses kamal-proxy (integrated, tied to Kamal's ecosystem)
Odysseus: Uses Caddy (independent, vibrant community, more tools available)

Both are valid. We're betting on Caddy being worth choosing directly.

What's Next

For now Odysseus does most of what I need it to do, I have a few more ideas but without a direct need for those, they will have to wait.

Open Source from Day One

Odysseus is LGPL v3 licensed. Core deployment stays free forever.

Eventually: A pro Tier might appear with some additional features.

Follows the Sidekiq model: community gets essential tools, teams pay for collaboration features.

Try It

Install:

gem install odysseus-cli

Docs: https://odysseus.wa-systems.eu

GitHub: https://github.com/WA-Systems-EU/odysseus

Example:

# Create config
cat > config/deploy.yml << EOF
service: myapp               # unique name of your service
image: myapp:v1.0.0
servers:
  web:                       # web role
    hosts: [your-server.com] # hostname to connect to
proxy:
  hosts: [app.example.com]   # names to serve the app from
  app_port: 3000             # the port the container is exposing
EOF

# Deploy
odysseus deploy production --image myapp:v1.0.0

Why This Exists

Three reasons:

  1. I wanted Caddy. Better tool, independent community, plugin ecosystem. Worth using directly instead of through another layer.
  2. I wanted to learn. Replicating Kamal features quickly showed us deployment tools are simpler than they look. Good way to understand infrastructure.
  3. I wanted independence. Not tied to 37signals' decisions. We get to make our own choices about direction.

What's Next

v0.1.0 shipped to RubyGems this week.

Then we listen. What breaks? What would help? That feedback shapes v0.2 and beyond.


Boring infrastructure for pragmatic teams.

Try it: https://odysseus.wa-systems.eu