Deploy Any Web App with Kamal on a VPS
Zero-downtime Docker deploys with automatic SSL, built-in reverse proxy, and multi-app support — PaaS convenience on your own infrastructure, by the team behind Basecamp.
At a Glance
| Project | Kamal by 37signals (Basecamp) |
| License | MIT |
| Recommended Plan | RamNode Cloud VPS 2GB or higher |
| OS | Ubuntu 24.04 LTS |
| Key Feature | Zero-downtime deploys with automatic SSL via kamal-proxy |
| Local Requirements | Ruby 2.7+ or Docker on your dev machine |
| Estimated Setup Time | 20–30 minutes |
Prerequisites
- A RamNode VPS with at least 2 GB RAM (Ubuntu 24.04 LTS)
- SSH key-based authentication configured on the VPS
- A domain name with DNS access (for SSL and proxy routing)
- A Docker Hub account (or GitHub Container Registry)
- Ruby 2.7+ on your local development machine
Provision and Prepare Your VPS
Deploy a RamNode VPS with Ubuntu 24.04 LTS, 2 GB+ RAM, 25 GB SSD. SSH in and update:
ssh root@YOUR_SERVER_IP
apt update && apt upgrade -yConfigure DNS
Type: A
Name: app (or your preferred subdomain)
Value: YOUR_SERVER_IP
TTL: 300Note: You do not need to manually install Docker on the VPS. Kamal handles Docker installation automatically during initial setup.
Install and Initialize Kamal Locally
Kamal runs from your local machine and connects to the VPS over SSH.
gem install kamal
kamal versioncd /path/to/your/project
kamal initThis generates config/deploy.yml and .kamal/secrets.
Configure Your Deployment
service: myapp
image: yourdockerhubuser/myapp
servers:
web:
- YOUR_SERVER_IP
proxy:
ssl: true
host: app.yourdomain.com
app_port: 3000
registry:
username: yourdockerhubuser
password:
- KAMAL_REGISTRY_PASSWORD
builder:
arch: amd64
env:
clear:
APP_ENV: production
secret:
- DATABASE_URL
- SECRET_KEY_BASEConfigure Secrets
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
DATABASE_URL=$DATABASE_URL
SECRET_KEY_BASE=$SECRET_KEY_BASEexport KAMAL_REGISTRY_PASSWORD="your-docker-hub-token"
export DATABASE_URL="postgres://user:pass@localhost:5432/myapp_production"
export SECRET_KEY_BASE="your-secret-key"Dockerfile (Node.js example)
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]Deploy Your Application
Initial Setup
kamal setupKamal will: install Docker on the VPS, log into the registry, build & push the image, start kamal-proxy on ports 80/443, boot your app container, and provision an SSL certificate.
Verify
kamal app detailsSubsequent Deploys
kamal deployKamal builds a new image, runs a health check, then switches traffic seamlessly. If the health check fails, the old container keeps running.
Add Accessories (Database, Redis)
accessories:
db:
image: postgres:16
host: YOUR_SERVER_IP
port: 5432:5432
env:
clear:
POSTGRES_USER: myapp
POSTGRES_DB: myapp_production
secret:
- POSTGRES_PASSWORD
directories:
- data:/var/lib/postgresql/data
redis:
image: redis:7
host: YOUR_SERVER_IP
port: 6379:6379
directories:
- data:/datakamal accessory boot allImportant: Running kamal app remove will also remove associated volumes. Plan your backup strategy accordingly.
Essential Kamal Commands
| Command | Description |
|---|---|
kamal setup | Full initial server and app setup |
kamal deploy | Build, push, and deploy a new version |
kamal app details | Show running container details |
kamal app logs -f | Tail application logs |
kamal rollback [version] | Roll back to a previous version |
kamal proxy logs | View kamal-proxy logs |
kamal lock release | Release a stuck deploy lock |
kamal audit | View deployment audit log |
Command Aliases
aliases:
console: app exec --interactive --reuse "bin/rails console"
shell: app exec --interactive --reuse "bash"
logs: app logs -fProduction Hardening
Firewall
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enableBind database accessories to 127.0.0.1 instead of exposing to the public internet.
Health Checks
proxy:
ssl: true
host: app.yourdomain.com
app_port: 3000
healthcheck:
path: /health
interval: 10
timeout: 30Database Backups
0 3 * * * docker exec myapp-db-postgres pg_dumpall -U myapp | gzip > /backups/db-$(date +\%Y\%m\%d).sql.gzMultiple Apps on a Single VPS
Kamal 2 natively supports multiple apps on the same server. Each app gets its own deploy.yml with a unique service name and host. The shared kamal-proxy routes by hostname.
service: secondapp
image: yourdockerhubuser/secondapp
servers:
web:
- YOUR_SERVER_IP
proxy:
ssl: true
host: second.yourdomain.com
app_port: 8080This makes RamNode VPS instances cost-effective for hosting multiple lightweight applications on a single server.
Troubleshooting
- Deploy times out during build — Upgrade to a larger VPS or use remote builds.
- SSL certificate not issued — Verify DNS A record and ports 80/443 are open.
- Container fails health check — Check
kamal app logsand verify the health check path returns 200. - Permission denied on SSH — Kamal connects as root by default. Use the
sshblock for custom users. - "Lock already acquired" — Release with
kamal lock release.
