{
  "$type": "site.standard.document",
  "description": "Push-to-deploy has been a solved problem for a while. I used Heroku years ago and Render for the past 2 years — both handle deployment well. But I",
  "path": "/one-command-deploy-how-kamal-2-changed-how-i-ship/",
  "publishedAt": "2026-03-31T04:43:00.000Z",
  "site": "at://did:plc:bryys25pc2fnagnyxqgsglhd/site.standard.publication/3mn26bjkkmh23",
  "tags": [
    "Tools",
    "Web"
  ],
  "textContent": "Push-to-deploy has been a solved problem for a while. I used Heroku years ago and Render for the past 2 years — both handle deployment well. But I wanted to run on my own server without giving up that convenience. Kamal gives me that.\n\nI run one command and walk away:\n\nkamal deploy\n\nThat's it. Zero-downtime deploy. Docker containers built, pushed, and swapped on the server. Health checks pass before traffic switches. Old containers cleaned up. The whole thing takes about 2 minutes.\n\nWHAT KAMAL ACTUALLY DOES\n\nKamal — built by the Basecamp team, open source — deploys Docker containers to any server via SSH. No Kubernetes. No orchestration platform. No vendor lock-in. It connects to your server over SSH, pulls your Docker image, starts the new container, waits for health checks, then stops the old one.\n\nThe key pieces:\n\n * kamal-proxy as a reverse proxy, handling SSL via Let's Encrypt and routing traffic to your containers\n * Docker for packaging your app into images\n * SSH for communicating with servers — no agent to install, no daemon to manage\n * Health checks before traffic switches — if the new container fails its health check, the old container keeps serving traffic\n\nKamal 2 simplified the configuration significantly from v1. A config/deploy.yml defines the core setup, with secrets in .kamal/secrets.\n\nMY DEPLOY CONFIGURATION\n\nHere's a stripped-down version of what my deploy file looks like for a typical SaaS app:\n\nservice: myapp\nimage: myregistry/myapp\n\nservers:\n  web:\n    hosts:\n      - 123.45.67.89\n    cmd: node dist/server.js\n    options:\n      network: kamal\n  worker:\n    hosts:\n      - 123.45.67.89\n    cmd: node dist/worker.js\n    options:\n      network: kamal\n\nproxy:\n  ssl: true\n  host: myapp.com\n  app_port: 3000\n  healthcheck:\n    path: /health\n    interval: 3\n\nregistry:\n  server: ghcr.io\n  username: myuser\n  password:\n    - KAMAL_REGISTRY_PASSWORD\n\nenv:\n  clear:\n    NODE_ENV: production\n  secret:\n    - DATABASE_URL\n    - STRIPE_SECRET_KEY\n\naccessories:\n  db:\n    image: postgres:16\n    host: 123.45.67.89\n    port: \"127.0.0.1:5432:5432\"\n    env:\n      secret:\n        - POSTGRES_PASSWORD\n    directories:\n      - data:/var/lib/postgresql/data\n    options:\n      network: kamal\n\nThis defines two roles — a web server and a background worker — plus a PostgreSQL database as an \"accessory\" (a long-running container that persists across deploys). Secrets are read locally from .kamal/secrets and injected into the containers at deploy time.\n\nTHE DEPLOY FLOW\n\nWhen I run kamal deploy, here's what happens:\n\n 1. Docker builds the image locally (or in CI)\n 2. Image gets pushed to the container registry (I use GitHub Container Registry)\n 3. Kamal SSHs into the server\n 4. Pulls the new image\n 5. Starts a new container alongside the old one\n 6. Runs health checks against the new container\n 7. Once healthy, kamal-proxy switches traffic to the new container\n 8. Old container is stopped and removed\n\nNo downtime. If the health check fails, the old container keeps serving traffic. I get an error, fix the issue, and deploy again.\n\nWHY NOT JUST STAY ON A PAAS?\n\nRender and Heroku work fine for deployment. The issues are elsewhere:\n\n * Cost — PaaS pricing scales fast. A server, worker, and database on Render can easily hit $100+/month for what a $20 Hetzner box handles. Run multiple projects — common for indie developers — and the gap widens quickly\n * Vendor lock-in — your deploy pipeline, environment config, and scaling model are all tied to the platform\n * Limited control — need a custom network setup, a specific Postgres extension, or to tweak container resources? You're at the mercy of what the platform supports\n\nKamal gives me the same push-one-thing-and-it-deploys convenience, but on my own server. The container is built once and runs identically everywhere. If the deploy fails, the old container is still running. Rolling back is kamal rollback <VERSION>.\n\nZERO-DOWNTIME ROLLING UPDATES\n\nKamal starts the new container, runs health checks, and only switches traffic after the new container is confirmed healthy. The old container keeps serving requests during the transition.\n\nFor my users, a deploy is invisible. No maintenance windows. No \"we'll be right back\" pages. I deploy during peak hours without thinking twice.\n\nDEPLOYING MULTIPLE ROLES\n\nMost SaaS apps aren't just a web server. I have background workers for job queues, cron containers for scheduled tasks, and sometimes separate API services. Kamal handles this with roles:\n\nkamal deploy                   # deploy everything\nkamal deploy --roles=web       # deploy only the web role\nkamal deploy --roles=worker    # deploy only the worker\n\nI have a script that detects which parts of the codebase changed and only deploys the affected roles. A frontend-only change doesn't restart the worker.\n\nSECRETS MANAGEMENT\n\nKamal 2 reads secrets locally from .kamal/secrets when you run a deploy. It can pull from environment variables, 1Password, or other adapters. The simplest setup is referencing local env vars:\n\n# .kamal/secrets\nDATABASE_URL=$DATABASE_URL\nSTRIPE_SECRET_KEY=$STRIPE_SECRET_KEY\n\nKamal injects these into the container at deploy time. For adding or changing a secret, I update my local env and deploy again. The fresh container picks up the new values.\n\nNo external secrets manager required. For a solo developer running one or two servers, this is plenty.\n\nGETTING STARTED\n\nThe initial setup takes an afternoon — install Docker on your server, create a deploy.yml, push your first deploy. After that, every deploy is one command.",
  "title": "One-Command Deploy: How Kamal 2 Changed How I Ship"
}