Deployment
    Zero Downtime

    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

    ProjectKamal by 37signals (Basecamp)
    LicenseMIT
    Recommended PlanRamNode Cloud VPS 2GB or higher
    OSUbuntu 24.04 LTS
    Key FeatureZero-downtime deploys with automatic SSL via kamal-proxy
    Local RequirementsRuby 2.7+ or Docker on your dev machine
    Estimated Setup Time20–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
    1

    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 and update
    ssh root@YOUR_SERVER_IP
    apt update && apt upgrade -y

    Configure DNS

    DNS A record
    Type: A
    Name: app (or your preferred subdomain)
    Value: YOUR_SERVER_IP
    TTL: 300

    Note: You do not need to manually install Docker on the VPS. Kamal handles Docker installation automatically during initial setup.

    2

    Install and Initialize Kamal Locally

    Kamal runs from your local machine and connects to the VPS over SSH.

    Install Kamal
    gem install kamal
    kamal version
    Initialize in your project
    cd /path/to/your/project
    kamal init

    This generates config/deploy.yml and .kamal/secrets.

    3

    Configure Your Deployment

    config/deploy.yml
    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_BASE

    Configure Secrets

    .kamal/secrets
    KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
    DATABASE_URL=$DATABASE_URL
    SECRET_KEY_BASE=$SECRET_KEY_BASE
    Export secrets locally
    export 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)

    Dockerfile
    FROM node:20-slim
    
    WORKDIR /app
    COPY package*.json ./
    RUN npm ci --production
    COPY . .
    
    EXPOSE 3000
    CMD ["node", "server.js"]
    4

    Deploy Your Application

    Initial Setup

    First-time deploy
    kamal setup

    Kamal 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

    Check deployment
    kamal app details

    Subsequent Deploys

    Deploy updates
    kamal deploy

    Kamal builds a new image, runs a health check, then switches traffic seamlessly. If the health check fails, the old container keeps running.

    5

    Add Accessories (Database, Redis)

    config/deploy.yml — accessories
    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:/data
    Boot accessories
    kamal accessory boot all

    Important: Running kamal app remove will also remove associated volumes. Plan your backup strategy accordingly.

    6

    Essential Kamal Commands

    CommandDescription
    kamal setupFull initial server and app setup
    kamal deployBuild, push, and deploy a new version
    kamal app detailsShow running container details
    kamal app logs -fTail application logs
    kamal rollback [version]Roll back to a previous version
    kamal proxy logsView kamal-proxy logs
    kamal lock releaseRelease a stuck deploy lock
    kamal auditView deployment audit log

    Command Aliases

    config/deploy.yml — aliases
    aliases:
      console: app exec --interactive --reuse "bin/rails console"
      shell: app exec --interactive --reuse "bash"
      logs: app logs -f
    7

    Production Hardening

    Firewall

    UFW rules
    ufw allow OpenSSH
    ufw allow 80/tcp
    ufw allow 443/tcp
    ufw enable

    Bind database accessories to 127.0.0.1 instead of exposing to the public internet.

    Health Checks

    deploy.yml — custom health check
    proxy:
      ssl: true
      host: app.yourdomain.com
      app_port: 3000
      healthcheck:
        path: /health
        interval: 10
        timeout: 30

    Database Backups

    Cron backup
    0 3 * * * docker exec myapp-db-postgres pg_dumpall -U myapp | gzip > /backups/db-$(date +\%Y\%m\%d).sql.gz
    8

    Multiple 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.

    Second app deploy.yml
    service: secondapp
    image: yourdockerhubuser/secondapp
    
    servers:
      web:
        - YOUR_SERVER_IP
    
    proxy:
      ssl: true
      host: second.yourdomain.com
      app_port: 8080

    This 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 logs and verify the health check path returns 200.
    • Permission denied on SSH — Kamal connects as root by default. Use the ssh block for custom users.
    • "Lock already acquired" — Release with kamal lock release.