What Is Kamal? Deploy Your AI-Built App Without Kubernetes
Your Vercel bill is $47/month for a side project. Railway's usage meter keeps climbing. Kubernetes sounds like learning a second language. Kamal deploys your Docker containers to any $5 VPS with zero-downtime deploys using nothing but SSH. No orchestrator, no cloud vendor lock-in, no YAML nightmare. Here's what it is, how it works, and whether it's the right tool for what you're building.
🚀 Quick version: Kamal is an open-source deployment tool created by DHH at 37signals (the Basecamp/HEY people). You run kamal deploy on your laptop, and it builds a Docker image of your app, pushes it to a container registry, SSHs into your server, pulls the image, and starts it — with zero downtime. It's like Capistrano for Docker. No Kubernetes cluster. No cloud platform. Just your code, a server, and SSH. 37signals uses it to deploy all of their production apps. It costs nothing. The server costs $5-6/month. Still here? Read on.
TL;DR
Kamal is a free deployment tool that ships Docker containers to any server via SSH. You define your app, servers, and environment in one deploy.yml file, run kamal deploy, and it handles the rest — building, pushing, pulling, and swapping containers with zero downtime. Created by DHH at 37signals, it's the deployment tool behind Basecamp, HEY, and ONCE. No Kubernetes. No cloud lock-in. Just a VPS, SSH access, and one command. Perfect for vibe coders who want the deploy experience of a platform without the platform bill.
Why AI Coders Need to Know This
Here's the deployment journey most vibe coders take:
You build something with Claude or Cursor. It works on your machine. Now you need it on the internet. You push to Vercel because someone on Reddit said it's the easiest. And it is — for a Next.js frontend.
Then your app grows. You need a backend that stays running. A database. A background worker that processes things. Suddenly Vercel's serverless model doesn't fit. You look at Railway — great, but $5-30/month per project adds up. You hear about Kubernetes — and immediately close the tab because the getting-started guide is 47 pages long.
This is where Kamal lives. It fills the gap between "I need something more than Vercel" and "I absolutely do not need Kubernetes." Here's the mental model:
- Vercel — deploys your frontend to their cloud. Easy. Limited.
- Railway — deploys anything to their cloud. Flexible. Usage-billed.
- Coolify — gives you a Vercel-like dashboard on your own server. GUI-first.
- Kamal — deploys Docker containers to your server via SSH. CLI-first. No dashboard. Maximum control.
- Kubernetes — orchestrates containers across server clusters. Enterprise-grade. Massive complexity.
Kamal is for people who want to own their infrastructure without needing a DevOps team. You tell it where your servers are, what your app looks like, and it handles the rest. One command: kamal deploy. Your app is live, with zero downtime, on hardware you control.
The economics are compelling: Kamal itself is free. A Hetzner VPS capable of running multiple apps costs $5-6/month. Compare that to $20/month Vercel Pro per team member, or $15-50/month across Railway projects, or the $100+/month minimum for managed Kubernetes. For indie developers and side projects, Kamal makes serious financial sense.
The Real-World Scenario
Chuck built a client portal with Claude Code. React frontend, Rails API backend, PostgreSQL database, and a Sidekiq worker that generates PDF reports overnight. He's paying $20/month on Railway for the backend, $7/month for the database, and $5/month for the Redis instance the worker needs. That's $32/month for a tool his 12 clients use.
A buddy in the vibe coding Discord mentions Kamal. "DHH's team uses it to deploy everything at 37signals. One config file, one command, zero-downtime deploys. You just need a VPS."
Chuck already has a $6 Hetzner VPS running his personal site. He asks his AI:
"I have a Rails API with PostgreSQL and Sidekiq workers. I'm currently paying $32/month on Railway. I heard about Kamal from 37signals — it deploys Docker containers to any server via SSH. I have a Hetzner VPS. Can you explain what Kamal is, how it compares to what I'm doing on Railway, and whether it makes sense to switch? What would the deploy.yml look like for my setup?"
Your AI will explain the concept well. What it might gloss over: the specific steps to get from "I have a VPS" to "my app is deployed with zero downtime," the parts of deploy.yml that trip people up, and what happens when things go wrong at 2am on a server you own. That's what this article covers.
What AI Generated: The deploy.yml
When you ask your AI to set up Kamal for your app, the core thing it generates is a deploy.yml file. This is the single configuration file that tells Kamal everything it needs to know. Here's what a real one looks like for a typical AI-built web app:
# config/deploy.yml — Kamal deployment configuration
# This file lives in your project root (or config/ directory)
# It tells Kamal: what to deploy, where to deploy it, and how
service: my-awesome-app # Name for your app (used in container naming)
image: yourname/my-awesome-app # Docker image name (pushed to registry)
servers:
web: # The "web" role — your main application servers
hosts:
- 49.13.100.25 # Your VPS IP address (can list multiple)
options:
network: "private" # Optional: use Docker network for inter-service comms
workers: # The "workers" role — background job processors
hosts:
- 49.13.100.25 # Same server (or different ones for larger apps)
cmd: bundle exec sidekiq # Command to run instead of the default web server
proxy: # Kamal's built-in reverse proxy (kamal-proxy)
ssl: true # Auto-provision SSL via Let's Encrypt
host: myapp.example.com # Your domain name
app_port: 3000 # Port your app listens on inside the container
registry: # Where Docker images get pushed/pulled
username: yourname
password: # Set via KAMAL_REGISTRY_PASSWORD env var
- KAMAL_REGISTRY_PASSWORD # Never hardcode passwords in config files
env: # Environment variables for your app
clear: # Variables that are okay to be in plaintext
RAILS_ENV: production
DB_HOST: my-awesome-app-db # References the accessory name below
secret: # Variables pulled from .kamal/secrets file
- RAILS_MASTER_KEY
- DATABASE_URL
- REDIS_URL
accessories: # Services that run alongside your app
db: # PostgreSQL database
image: postgres:16
host: 49.13.100.25
port: "5432:5432"
env:
clear:
POSTGRES_DB: my_awesome_app_production
secret:
- POSTGRES_PASSWORD
directories:
- data:/var/lib/postgresql/data # Persist data across container restarts
redis: # Redis for caching/job queues
image: redis:7
host: 49.13.100.25
port: "6379:6379"
directories:
- data:/data
# Health check — Kamal pings this before switching traffic
healthcheck:
path: /up # Your app should respond 200 at this path
port: 3000
interval: 3 # Check every 3 seconds during deploy
That's it. The entire deployment configuration for a production app with a database, Redis, background workers, SSL, and zero-downtime deploys — in one readable file.
Understanding Each Part
Let's break down what each section of that deploy.yml actually does, because when something breaks, you need to know which section to look at.
service and image — Naming Things
service is just a name. Kamal uses it to label containers on your server so it can tell them apart. image is where your built Docker image gets stored — usually Docker Hub (yourname/appname) or GitHub Container Registry (ghcr.io/yourname/appname). When you run kamal deploy, Kamal builds a Docker image from your code, tags it, and pushes it to this registry. Then it SSHs into your server and tells Docker to pull that image. The registry is the middleman between your machine and your server.
servers — Where Your App Runs
This is a list of IP addresses. That's literally it. Kamal SSHs into each IP and deploys your app. You can have one server or twenty. You can group them into roles — web servers that handle HTTP requests, workers that run background jobs, cron servers that run scheduled tasks. Each role can run a different command from the same Docker image.
For most vibe coders, this is one IP address. Your $5-6 VPS. Done.
proxy — The Zero-Downtime Magic
This is the part that makes Kamal different from just docker run. Kamal runs a lightweight reverse proxy called kamal-proxy on your server. When a deploy happens:
- Kamal starts the new container alongside the old one
- It runs a health check against the new container (hits
/upand waits for a 200 response) - Once the new container is healthy, kamal-proxy switches traffic to it
- The old container is stopped and removed
Users never see downtime. If the new container fails the health check, the old container keeps running and nothing changes. Your deploy failed safely — no broken production site.
The ssl: true option tells kamal-proxy to auto-provision an SSL certificate via Let's Encrypt. Point your domain's DNS at the server, and Kamal handles HTTPS. No certbot, no nginx config, no certificate renewal scripts.
registry — Where Images Live
Kamal needs somewhere to push the Docker image after building it, and somewhere the server can pull it from. Docker Hub is the most common choice (free for public images, one free private repo). You set your username here and the password via an environment variable — never hardcode registry passwords.
env — Your App's Configuration
Two categories: clear (values that are fine in plaintext) and secret (values pulled from a .kamal/secrets file that never gets committed to Git). This is where your database URLs, API keys, and framework-specific variables go. Same concept as Vercel's environment variables or Railway's variable panel — just in a YAML file.
accessories — Databases and Services
Accessories are supporting services — databases, Redis, message queues — that run alongside your app on the same server. Kamal manages their lifecycle separately from your app. When you deploy a new version of your app, the database container keeps running untouched. The directories section maps data to persistent storage on the host, so your data survives container restarts.
💡 The key mental model: Kamal is not a platform. It's a deployment script that got really good. It SSHs into your server, runs Docker commands, and manages a reverse proxy. That's the whole thing. There's no daemon running on your server, no agent phoning home, no control plane. When you're not deploying, Kamal isn't doing anything. Your server is just running Docker containers.
Kamal vs Coolify vs Vercel vs Railway
Four different deployment philosophies. Here's the honest breakdown:
Kamal
Best for: Developers who want full control, CLI-first workflow, and minimal overhead
- ✅ Free, open-source (MIT license)
- ✅ Zero-downtime deploys out of the box
- ✅ Deploys to any server with SSH access
- ✅ One config file, one deploy command
- ✅ Manages databases as accessories
- ✅ Auto SSL via Let's Encrypt
- ✅ Multi-server and multi-role support
- ✅ No vendor lock-in whatsoever
- ⚠️ CLI-only — no web dashboard
- ⚠️ Requires a Dockerfile (AI generates these easily)
- ⚠️ You manage the server yourself
- ⚠️ Needs Ruby installed locally (it's a Ruby gem)
Pricing: $0 for Kamal + $5-10/month for VPS
Coolify
Best for: Self-hosters who want a visual dashboard like Vercel
- ✅ Web UI for deploying and managing apps
- ✅ Auto-detects frameworks (no Dockerfile needed)
- ✅ One-click databases
- ✅ GitHub integration with auto-deploy
- ✅ Built-in monitoring and logs
- ⚠️ Coolify itself runs on your server (uses resources)
- ⚠️ More complex initial setup than Kamal
- ⚠️ Dashboard adds an attack surface
Pricing: $0 for Coolify + $5-10/month for VPS
Vercel
Best for: Next.js frontends, static sites, JAMstack
- ✅ Best Next.js integration available
- ✅ Global edge CDN, instant deploys
- ✅ Preview deployments on every PR
- ✅ Zero server management
- ⚠️ Serverless only — no persistent processes
- ⚠️ Function timeouts (10s free, 60s Pro)
- ⚠️ Bandwidth overages add up fast
- ⚠️ No native database hosting
Pricing: Free tier, Pro at $20/month per member
Railway
Best for: Full-stack apps where you don't want to manage servers
- ✅ Persistent servers (not serverless)
- ✅ One-click databases
- ✅ Excellent developer experience
- ✅ No server management at all
- ⚠️ Usage-based billing — can surprise you
- ⚠️ Cost scales per project
- ⚠️ No permanent free tier
Pricing: $5 credit, then usage-based (~$5-30/month per project)
📋 Quick decision guide:
- 🔵 Next.js frontend, light API → Vercel (made for this)
- 🟠 One app + database, don't want to touch servers → Railway (best managed DX)
- 🟢 Want a visual dashboard on your own server → Coolify (self-hosted Vercel)
- 🔴 Want maximum control with minimum overhead → Kamal (CLI-first, no bloat)
- 🔴 Multiple apps on one VPS, cost-sensitive → Kamal (flat cost, no resource overhead)
- 🔴 Already comfortable with the terminal → Kamal (it'll feel natural)
- ⚫ Running 50+ microservices across a cluster → Kubernetes (this is what it was built for)
Kamal vs Coolify specifically: Both deploy to your own server. The difference is philosophy. Coolify runs on your server as a persistent application with a web dashboard — it's always there, consuming resources, providing a GUI. Kamal runs on your laptop and only touches the server during deploys. Between deploys, your server is just running Docker containers with zero overhead from the deployment tool. If you like dashboards, Coolify. If you like the terminal, Kamal.
The Deploy Workflow: What Actually Happens
When you type kamal deploy and hit enter, here's the sequence — understanding this helps you debug when things go wrong:
# Step 1: Build the Docker image on your machine
$ kamal deploy
# Kamal reads config/deploy.yml
# Builds a Docker image using your Dockerfile
# Tags it with a git commit hash (e.g., yourname/myapp:abc123f)
# Step 2: Push the image to your container registry
# → Docker Hub, GitHub Container Registry, or wherever you configured
# This is like uploading a zip file of your app to a central location
# Step 3: SSH into each server listed in deploy.yml
# → Kamal connects to 49.13.100.25 (or whatever your server IP is)
# → Pulls the new image from the registry
# → Starts a new container from that image
# Step 4: Health check
# → Kamal hits http://localhost:3000/up on the new container
# → Waits for a 200 OK response
# → If the health check fails after retries, deploy is aborted
# → Old container keeps running, nothing breaks
# Step 5: Traffic switch
# → kamal-proxy switches incoming traffic to the new container
# → Old container receives a SIGTERM (graceful shutdown)
# → Zero downtime — users never saw a blip
# Step 6: Cleanup
# → Old container is removed
# → Old images are pruned to save disk space
# The whole process takes 1-3 minutes for a typical app
That's it. No orchestrator deciding where to place your container. No cloud API calls. No YAML manifests being applied to a cluster. Kamal SSHs into your server, runs Docker commands, and manages a reverse proxy. Beautifully simple.
Other Useful Commands
# See what's running on your server
kamal details
# Check real-time logs from your app
kamal app logs
# Open a Rails console (or any interactive command) on the server
kamal app exec -i "bin/rails console"
# Roll back to the previous version
kamal rollback [git-hash]
# Deploy just the app (skip accessories)
kamal app boot
# Restart the app without redeploying
kamal app restart
# Run database migrations
kamal app exec "bin/rails db:migrate"
💡 The killer feature for vibe coders: kamal app exec -i lets you open an interactive shell on your production server through Docker. Need to check a database record? Run a one-off script? Debug a specific request? You're one command away. On Vercel, this is literally impossible — there's no server to SSH into.
What AI Gets Wrong About Kamal
AI tools give solid explanations of what Kamal is. They're less reliable on the practical gotchas. Here's what they consistently miss or understate:
1. "Just Add a Dockerfile" — Easier Said Than Done
Unlike Coolify or Railway (which auto-detect your framework and build containers for you), Kamal requires a Dockerfile. Your AI will happily generate one, and it'll work 80% of the time. The other 20%? Your Node.js app needs native dependencies that aren't in the base image. Your Python app needs specific system libraries for PDF generation. Your app works locally but the Docker build fails because the base image uses a different architecture.
The fix is almost always straightforward — add a package install line to the Dockerfile — but AI often generates "generic" Dockerfiles that don't account for your specific dependencies. When the build fails, paste the exact error message back to your AI with: "This Dockerfile build failed during kamal deploy. Here's the error. Fix the Dockerfile."
2. Glossing Over the Registry Setup
Kamal needs a container registry to push and pull images. AI tools mention this in passing — "set up Docker Hub" — but don't explain that you need to:
- Create a Docker Hub account (or GitHub Container Registry)
- Create an access token (not your password)
- Store that token in your
.kamal/secretsfile - Make sure your server can pull from the registry (authentication needs to work on both ends)
This is a 5-minute setup, but it's a step AI often treats as obvious when it's not obvious to someone doing it for the first time.
3. Assuming Your Server Is Pre-Configured
AI will tell you to "deploy to your server" without mentioning that the server needs Docker installed. Kamal 2 handles this automatically on first deploy (kamal setup bootstraps Docker on your server), but if you're running on a non-standard OS or your server has restrictive security policies, the automatic setup might fail. A fresh Ubuntu 22.04 or 24.04 VPS works perfectly with zero prep. Anything else, test first.
4. Underestimating the .kamal/secrets File
Your deploy.yml references secret environment variables, but the actual values live in .kamal/secrets. AI tools often show the deploy.yml without explaining how secrets work, leading to deploy failures when Kamal can't find the values. The secrets file is simple:
# .kamal/secrets — environment variable values
# This file should NEVER be committed to Git
# Add it to .gitignore immediately
KAMAL_REGISTRY_PASSWORD=dckr_pat_xxxxxxxxxxxx
RAILS_MASTER_KEY=abc123def456
DATABASE_URL=postgresql://user:pass@my-awesome-app-db:5432/myapp
REDIS_URL=redis://my-awesome-app-redis:6379/0
POSTGRES_PASSWORD=supersecretpassword
5. Not Mentioning the Ruby Dependency
Kamal is a Ruby gem. You install it with gem install kamal. This means you need Ruby installed on your local machine (not on the server — just where you run deploys from). If you're a JavaScript/Python developer, you might not have Ruby set up. It's a 5-minute install, but it's a surprise dependency AI rarely mentions upfront. On macOS, Ruby is pre-installed. On Linux, apt install ruby. On Windows, use the RubyInstaller.
⚠️ The biggest gotcha: Kamal deploys are triggered from your machine, not from GitHub. There's no "push to main and it auto-deploys" built in. You run kamal deploy from your terminal. If you want automated deploys on git push, you set up a CI/CD pipeline (GitHub Actions is the most common choice) that runs kamal deploy for you. AI tools often describe Kamal as if auto-deploy is included — it's not. It's a manual command by default, with CI/CD as an add-on.
How to Debug Kamal Deploys
When kamal deploy fails — and it will, especially the first few times — here's the systematic approach:
Build Failures (Image Won't Build)
# The deploy fails during the Docker build step
# You'll see Docker build output with a specific error
# Common causes:
# 1. Missing system dependency in Dockerfile
# 2. Node/Python version mismatch
# 3. Build script expects an env var that isn't available at build time
# Debug prompt for your AI:
# "My kamal deploy failed during the Docker build step. Here's the
# error output: [paste it]. Here's my Dockerfile: [paste it].
# What's wrong and how do I fix it?"
Push/Pull Failures (Registry Issues)
# The image built but won't push, or the server can't pull it
# Check 1: Is your registry password correct?
docker login # Run this locally, use the same credentials
# Check 2: Does the image name match your registry?
# image: yourname/myapp → must match Docker Hub username
# Check 3: Can the server reach the registry?
kamal app exec "docker pull yourname/myapp:latest"
# Debug prompt:
# "Kamal built the image but failed pushing to Docker Hub.
# Error: [paste]. My registry config in deploy.yml is: [paste].
# What's wrong?"
Health Check Failures (App Won't Start)
# The image deployed but kamal-proxy won't switch traffic
# This means your app isn't responding to the health check
# Check the app logs on the server:
kamal app logs
# Common causes:
# 1. Missing environment variable (DATABASE_URL not set)
# 2. Database isn't reachable (accessory not running)
# 3. App crashes on startup (check logs for stack trace)
# 4. Health check path is wrong (your app doesn't have /up)
# 5. App is listening on wrong port (deploy.yml says 3000, app uses 8080)
# Debug prompt:
# "My kamal deploy completed but the health check failed.
# Here are my app logs: [paste kamal app logs output].
# My health check config is: [paste proxy section].
# What's preventing my app from starting?"
The Nuclear Option: Starting Fresh
# If everything is broken and you want to start clean on the server:
# Remove all Kamal-managed containers
kamal remove
# This removes: your app containers, kamal-proxy, accessories
# Your server is clean — just Docker installed
# Then re-setup and deploy from scratch:
kamal setup
kamal deploy
💡 The "what just happened" command: After any deploy (successful or failed), run kamal details. It shows you exactly what's running on your server — which containers, which versions, which ports. It's the first thing to check when something feels wrong. Think of it like docker ps but formatted for humans.
When Kamal Makes Sense (and When It Doesn't)
Kamal Makes Sense When...
- You're comfortable in the terminal — Kamal is CLI-first. No dashboard, no GUI. If you're already running git commands and using your AI via terminal, Kamal fits naturally.
- You want minimal server overhead — Unlike Coolify (which runs a full application on your server), Kamal leaves nothing running between deploys. Your server resources go entirely to your app.
- You deploy multiple apps to the same server — Run 5 apps on one $6 VPS with one deploy.yml per project. Flat cost.
- You want zero-downtime deploys without platform lock-in — Kamal works with any server: Hetzner, DigitalOcean, AWS EC2, bare metal in your closet. Switch providers by changing an IP address.
- You're building a Ruby/Rails app — Kamal was built by the Rails creator and is deeply integrated with the Rails ecosystem. Rails 8 ships with Kamal configuration by default.
- You want to integrate deploys into CI/CD — Kamal works perfectly in GitHub Actions or any CI pipeline. Push to main, CI runs
kamal deploy, done.
Kamal Doesn't Make Sense When...
- You've never used a terminal — If you're deploying your first-ever app, start with Vercel (for frontends) or Railway (for full-stack). Learn what deployment means, then graduate to Kamal when you want more control.
- You want a visual deployment dashboard — Kamal has no UI. If you want to see deploy status in a browser, check logs from a web interface, or click buttons to add databases, Coolify is a better fit.
- You're deploying a static site or Next.js frontend — Vercel's free tier, global CDN, and preview deployments are purpose-built for this. Kamal can serve static sites, but you're trading Vercel's edge network for a single server.
- You need auto-scaling — Kamal deploys to a fixed set of servers. If you need to handle sudden 10x traffic spikes automatically, you need a platform with auto-scaling (Railway, Fly.io) or an actual Kubernetes cluster.
- You don't want to manage a server at all — Kamal gives you control, but control means responsibility. Disk space, security updates, backups — that's on you.
What to Learn Next
Kamal connects to several infrastructure concepts. Understanding these makes you a much more effective deployer:
Frequently Asked Questions
Is Kamal free?
Yes. Kamal is completely free and open-source under the MIT license. You install it on your local machine with gem install kamal, and it deploys to any server you can SSH into. You pay for the server ($5-6/month on Hetzner, DigitalOcean, etc.) and optionally a private container registry (Docker Hub's free tier includes one private repo). The Kamal tool itself costs nothing. It was created by 37signals — the company behind Basecamp and HEY — and is actively maintained as the deployment tool they use for all their production apps.
What's the difference between Kamal and Kubernetes?
Kubernetes is a container orchestration system designed to manage hundreds or thousands of containers across clusters of servers. It handles auto-scaling, self-healing, service discovery, load balancing — enterprise infrastructure at Netflix/Google scale. Kamal deploys Docker containers to one or more servers via SSH. No cluster, no control plane, no 47-page getting-started guide. Think of Kubernetes as a container city planner and Kamal as a contractor who puts your container exactly where you told it to go. For indie apps, side projects, and even medium-traffic SaaS products, Kamal does everything you need. 37signals runs Basecamp and HEY (millions of users) on Kamal, not Kubernetes.
Do I need to know Docker to use Kamal?
You need a Dockerfile in your project, but you don't need to write one from scratch — your AI can generate a perfectly good Dockerfile for any framework in seconds. Kamal handles everything else: building the image, pushing it to a registry, pulling it on your server, and starting it. Understanding what Docker does at a high level (packages your app into a portable container) helps when debugging build failures, but deep Docker expertise is not required. If your AI-generated Dockerfile builds locally with docker build ., it'll work with Kamal.
Can Kamal deploy to multiple servers?
Yes. In your deploy.yml config, you list multiple server IP addresses under each role, and Kamal deploys to all of them. It supports roles — web servers, job workers, cron runners — each deploying to different machines with different commands from the same Docker image. This is how 37signals runs their production infrastructure: Kamal deploying to bare metal servers across multiple roles. For most vibe coders, a single VPS handles everything. But Kamal scales up when your app demands it — just add another IP address to the config.
How does Kamal handle zero-downtime deploys?
Kamal uses a lightweight reverse proxy called kamal-proxy that sits in front of your app. During a deploy: (1) Kamal starts the new container alongside the old one, (2) runs a health check against the new container (hits your health check path and waits for a 200 response), (3) once healthy, kamal-proxy atomically switches traffic to the new container, (4) the old container receives a graceful shutdown signal and stops. Users never see downtime because the old container keeps serving requests until the new one is confirmed healthy. If the new version fails the health check, the old container keeps running and the deploy is marked as failed. Nothing breaks.