Docs

Kamal 2 Deployment

Stacknaut deploys to your own server using Kamal 2, the deployment tool from 37signals. One command deploys your entire stack with zero downtime — frontend, backend, API, database, and log collection.

How It Works

Kamal builds Docker images locally, pushes them to Docker Hub, and deploys them to your server. The Kamal proxy handles SSL termination (via Let's Encrypt), routing, and zero-downtime container swaps.

Your app runs as three services on a single server:

  • Web — Vue frontend served by Caddy (port 80)
  • Backend — Fastify API for the frontend (port 3001)
  • API — Public-facing API service (port 3002)

Plus two accessories:

  • PostgreSQL 16 — Your database, with persistent volume storage
  • Vector — Log collector that forwards to BetterStack

Each service has its own deploy configuration and environment variables. Backend and API have explicit health checks (/health endpoint); the web service is a static file server. Kamal proxy routes traffic to the right service based on hostname.

Deploy Commands

# First-time setup (provisions everything)
kamal setup

# Full deploy (all services)
kamal deploy

# Deploy a single service
kamal deploy --roles=backend
kamal deploy --roles=api
kamal deploy --roles=web

# Smart deploy (auto-detects which services changed)
scripts/push-and-deploy.sh

# Force deploy without prompts
scripts/push-and-deploy.sh --force

Smart Deploy Script

The included push-and-deploy.sh script compares your local changes against origin/main and only deploys services that actually changed:

  • Changes in backend/ → deploys backend
  • Changes in frontend/ → deploys web
  • Changes in api/ → deploys API
  • Changes in shared/ → deploys all (shared code affects everything)
  • Schema changes in shared/src/schemas/ → deploys backend first (runs migrations), then the rest

This saves time on every deploy — if you only changed the frontend, only the frontend gets rebuilt and deployed.

Zero-Downtime Deploys

Kamal achieves zero downtime by:

  1. Building and pushing the new Docker image
  2. Starting a new container alongside the old one
  3. Health-checking the new container (backend and API hit /health; web checks that Caddy is serving on port 80)
  4. Switching the proxy to route traffic to the new container
  5. Stopping the old container

If the health check fails, the old container keeps serving traffic and the deploy fails safely.

Frontend Build Process

The frontend has a unique build process — it builds at deploy time inside the container:

  1. Installs shared module dependencies
  2. Installs frontend dependencies (including devDependencies for Vite)
  3. Runs pnpm run build-only (Vite build + pre-rendering)
  4. Renames index.html to index-default.html for SPA fallback
  5. Starts Caddy to serve the static files

Pre-rendered pages get their own HTML files (index-pricing.html, index-docs.html, etc.) and Caddy rewrites route paths to the correct file. Non-pre-rendered routes fall back to index-default.html for client-side routing.

Configuration

The central deployment config lives in config/deploy.yml:

  • Server IP and hostnames
  • Docker Hub credentials
  • Environment variables per service (tagged by role)
  • Database and Vector accessory configuration
  • SSL and proxy settings

Environment secrets go in .env.kamal (never committed to git).

What You Get

  • One-command deployment with kamal setup and kamal deploy
  • Zero-downtime container swaps with health checks
  • Smart deploy script that auto-detects changed services
  • Schema-aware deploy ordering (backend first when migrations needed)
  • SSL via Let's Encrypt (auto-renewed)
  • Per-service environment variable management
  • Frontend pre-rendering at build time
  • Caddy file server with security headers and scanner blocking
  • PostgreSQL as a managed Kamal accessory
  • Vector log collection to BetterStack
  • 5 container retention for quick rollbacks

Key Files

config/deploy.yml                    — Kamal deployment configuration
kamal-deploy/Caddyfile               — Frontend proxy config with rewrites
kamal-deploy/run-frontend            — Frontend build + Caddy startup
kamal-deploy/run-backend             — Backend startup script
kamal-deploy/run-api                 — API startup script
scripts/push-and-deploy.sh           — Smart deploy with change detection
.env.kamal                           — Production secrets (not committed)

Setup

  1. Install Kamal 2: gem install kamal
  2. Create a Docker Hub account
  3. Update config/deploy.yml with your server IP, hostnames, and Docker Hub username
  4. Create .env.kamal from .env.kamal.example and fill in your secrets
  5. Point your domain's DNS A records to the server IP
  6. Run kamal setup for first-time deployment

Subsequent deploys: just run scripts/push-and-deploy.sh.

7a9a8821

© 2026 Stacknaut