Analytics cookies only (Google Analytics, PostHog). No ad networks, no cross-site tracking. See our privacy policy.
A command-line tool that deploys containerised web apps to your own servers over SSH, with zero-downtime switchovers through its kamal-proxy. It builds your image, pushes it to a registry, pulls it on each server and starts the new container once it answers its health check; there is no web dashboard and no server-side platform to run, just Docker on the target hosts. Originally built for Rails apps, it works for any web app that runs in a container.
Runs on a VPS from $6/mo (Kamatera 2GB, 2 GB RAM) · prices checked Sep 2026
Pick Kamal when you already have servers (or a cheap VPS) and a Dockerfile, and want `git push`-level simplicity without running a platform on the server: one `config/deploy.yml`, then `kamal setup` and `kamal deploy` build the image, push it to a registry, pull it on each host over SSH and switch traffic through kamal-proxy once the new container answers its health check. It is a Ruby gem on your workstation, MIT licensed, and the servers need only Docker. If you want a server-side Heroku-style platform you push to with git and manage with a CLI on the box, Dokku is the catalog's other option.
Paid link — we earn a commission if you shop through it.
Run these commands on your server:
# Kamal — deploys a containerised web app to servers over SSH, run from your workstation (gem install kamal is the official install). Pinned to 2.12.0. Prerequisites: SSH access to the server as a user in the docker group, and Docker on the server. Replace SERVER_IP and SSH_USER; here the workstation and the server are the same machine.
sudo apt-get update && sudo apt-get install -y ruby-full build-essential git
sudo gem install kamal -v 2.12.0 --no-document
# images go through a registry the server can pull from. With a separate server, `server: localhost:5555` makes Kamal run a temporary registry and tunnel it over SSH; that port collides when workstation and server are one machine, so this run starts its own.
docker run -d --restart unless-stopped --name registry -p 127.0.0.1:5000:5000 registry:3.1.2
mkdir demo && cd demo && git init -q
# a minimal app: Kamal's proxy waits for GET /up to return 200 before it routes traffic
cat > Dockerfile << 'EOF'
FROM nginx:1.30-alpine
RUN echo ok > /usr/share/nginx/html/up
EOF
kamal init
cat > config/deploy.yml << 'EOF'
service: demo
image: demo
servers:
web:
- SERVER_IP
ssh:
user: SSH_USER
proxy:
app_port: 80
healthcheck:
path: /up
registry:
server: 127.0.0.1:5000
username: demo
password:
- KAMAL_REGISTRY_PASSWORD
builder:
arch: amd64
driver: docker
EOF
# Kamal tags each release with the git commit
git add -A && git -c user.name=demo -c user.email=demo@example.com commit -qm demo
# Kamal insists on registry credentials even though this local registry does not check them
echo 'KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD' >> .kamal/secrets
export KAMAL_REGISTRY_PASSWORD=unused
kamal setup
# the app answers at http://SERVER_IP on port 80Kamal deploys what you give it and manages little else, so know these before you rely on it: