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:
- Building and pushing the new Docker image
- Starting a new container alongside the old one
- Health-checking the new container (backend and API hit
/health; web checks that Caddy is serving on port 80) - Switching the proxy to route traffic to the new container
- 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:
- Installs shared module dependencies
- Installs frontend dependencies (including devDependencies for Vite)
- Runs
pnpm run build-only(Vite build + pre-rendering) - Renames
index.htmltoindex-default.htmlfor SPA fallback - 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 setupandkamal 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
- Install Kamal 2:
gem install kamal - Create a Docker Hub account
- Update
config/deploy.ymlwith your server IP, hostnames, and Docker Hub username - Create
.env.kamalfrom.env.kamal.exampleand fill in your secrets - Point your domain's DNS A records to the server IP
- Run
kamal setupfor first-time deployment
Subsequent deploys: just run scripts/push-and-deploy.sh.