Introduction
Deploying a web application to production shouldn't require a Kubernetes cluster, a dedicated DevOps engineer, or a four-figure cloud bill. Yet in 2026, many developers find themselves trapped between two extremes: expensive managed platforms (Vercel, Railway) and the complexity of Kubernetes.
Kamal 2 is 37signals' answer to this dilemma (the creators of Ruby on Rails and Basecamp). Formerly known as MRSK, Kamal is an open-source deployment tool that lets you deploy containerized applications to any server — VPS, bare metal, or cloud — with zero downtime, instant rollbacks, and automatic SSL via Traefik.
What makes Kamal 2 particularly compelling:
- No vendor lock-in — works on any server with SSH and Docker
- Zero downtime — new versions are started before cutting off the old ones
- Simple configuration — a single YAML file (
config/deploy.yml) - One-command rollback — return to the previous version instantly
- Multi-server — deploy to multiple machines in parallel
- Accessory support — manage your databases and auxiliary services
In this tutorial, you will:
- Install and configure Kamal 2 on your local machine
- Prepare a VPS for deployment
- Containerize a Next.js application with an optimized Dockerfile
- Configure Kamal for zero-downtime deployment
- Set up automatic SSL with Traefik
- Deploy, monitor, and perform rollbacks
Prerequisites
Before starting, make sure you have:
- A VPS with at least 1 GB RAM and 1 vCPU (DigitalOcean, Hetzner, OVH, Contabo, etc.)
- Ubuntu 22.04 or 24.04 installed on the VPS
- A domain name pointing to your server (DNS A record)
- Docker installed locally (to build images)
- Ruby 3.1+ installed locally (Kamal is written in Ruby)
- A Next.js application ready to deploy
- A Docker Hub account (or another container registry)
- Basic knowledge of Linux command line and Docker
What You'll Build
By the end of this tutorial, you'll have a complete deployment infrastructure:
- A VPS automatically configured by Kamal
- A Next.js application in production with SSL
- A Traefik proxy handling routing and certificates
- A zero-downtime deployment pipeline
- The ability to rollback with a single command
Step 1: Install Kamal 2
Kamal is distributed as a Ruby gem. Install it globally on your development machine:
gem install kamalVerify that Kamal is properly installed:
kamal versionYou should see something like 2.4.0 or a newer version.
If you prefer not to install Ruby, you can use Kamal via Docker:
alias kamal='docker run -it --rm -v "${PWD}:/workdir" -v "/run/host-services/ssh-auth.sock:/run/host-services/ssh-auth.sock" -e SSH_AUTH_SOCK="/run/host-services/ssh-auth.sock" -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/basecamp/kamal:latest'Step 2: Prepare the VPS
2.1 Create the Server
Create a VPS from your preferred provider. For this tutorial, a server with these minimum specs is sufficient:
| Resource | Minimum | Recommended |
|---|---|---|
| RAM | 1 GB | 2 GB |
| CPU | 1 vCPU | 2 vCPUs |
| Storage | 20 GB SSD | 40 GB SSD |
| OS | Ubuntu 22.04 | Ubuntu 24.04 |
2.2 Configure DNS
Point your domain to the server:
Type: A
Name: app (or @ for root)
Value: <YOUR_SERVER_IP>
TTL: 300
2.3 Configure SSH
Make sure you can connect to the server via SSH with your key:
ssh root@your-server.comKamal connects via SSH to configure Docker and deploy your containers. It automatically handles Docker installation on the server during the first deployment — you don't need to install anything manually.
Step 3: Prepare the Next.js Application
3.1 Create the Project (if needed)
If you're starting from scratch:
npx create-next-app@latest my-kamal-app --typescript --tailwind --app
cd my-kamal-app3.2 Create the Dockerfile
Create an optimized production Dockerfile at the project root:
# Stage 1: Install dependencies
FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
# Stage 2: Build the application
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production
RUN npm run build
# Stage 3: Production image
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]3.3 Configure Next.js for Standalone Mode
Modify your next.config.js (or next.config.ts) to enable standalone mode:
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
}
module.exports = nextConfigThe standalone mode generates an autonomous Node.js server that includes only the necessary files, significantly reducing the Docker image size.
3.4 Add a .dockerignore File
Create a .dockerignore file to speed up builds:
node_modules
.next
.git
.gitignore
*.md
docker-compose*.yml
.env*.local
Step 4: Initialize Kamal
4.1 Generate the Configuration
From your Next.js project root:
kamal initThis command creates the following files:
config/
deploy.yml # Main configuration
.kamal/
secrets # Secret environment variables
.env # Local variables (added to .gitignore)
4.2 Configure deploy.yml
Replace the contents of config/deploy.yml with:
service: my-nextjs-app
image: your-docker-hub-user/my-nextjs-app
servers:
web:
hosts:
- your-server.com
labels:
traefik.http.routers.my-nextjs-app.rule: Host(`app.your-domain.com`)
traefik.http.routers.my-nextjs-app.entrypoints: websecure
traefik.http.routers.my-nextjs-app.tls.certresolver: letsencrypt
options:
network: kamal
proxy:
ssl: true
host: app.your-domain.com
registry:
username: your-docker-hub-user
password:
- KAMAL_REGISTRY_PASSWORD
env:
clear:
NODE_ENV: production
HOSTNAME: 0.0.0.0
secret:
- DATABASE_URL
builder:
dockerfile: Dockerfile
multiarch: false
healthcheck:
path: /api/health
port: 3000
interval: 10
max_attempts: 30Let's break down the important sections:
- service: your application name (used to name containers)
- image: the full path to your Docker image on the registry
- servers: target server list with Traefik labels for routing
- proxy: enables automatic SSL via Traefik and kamal-proxy
- registry: your Docker registry credentials
- env: environment variables (clear and secret separated)
- healthcheck: Kamal verifies your app responds before switching traffic
4.3 Configure Secrets
Edit the .kamal/secrets file:
KAMAL_REGISTRY_PASSWORD=your-docker-hub-password
DATABASE_URL=postgresql://user:pass@db-host:5432/mydbThis file should never be committed to Git. Verify that .kamal/secrets is in your .gitignore.
4.4 Create the Healthcheck Endpoint
Kamal uses a healthcheck to verify your application is ready before switching traffic. Create a simple API endpoint:
// app/api/health/route.ts
import { NextResponse } from 'next/server'
export async function GET() {
return NextResponse.json({
status: 'ok',
timestamp: new Date().toISOString(),
})
}Step 5: Understanding Zero-Downtime Deployment
Before launching the first deployment, let's understand how Kamal 2 ensures zero downtime:
The Deployment Flow
1. Build Docker image locally (or in CI)
2. Push image to registry
3. Pull image on server(s)
4. Start new container
5. Healthcheck: Kamal waits for /api/health to return 200
6. kamal-proxy switches traffic to new container
7. Graceful shutdown of old container
The key point is step 6: traffic only switches after the new container passes the healthcheck. This means your users will never see a 502 error or blank page during deployment.
kamal-proxy vs Traefik
Kamal 2 uses kamal-proxy, a lightweight reverse proxy developed specifically for Kamal. It replaces the Traefik dependency for basic routing, although Traefik remains available as an option for advanced configurations (multi-service, middlewares, etc.).
kamal-proxy handles:
- Traffic switching between old and new containers
- SSL termination with Let's Encrypt
- Automatic healthchecks
- Connection draining for existing requests
Step 6: First Deployment
6.1 Run the Initial Setup
The first deployment requires a setup step that configures Docker and kamal-proxy on the server:
kamal setupThis command performs the following operations:
- SSH connection to the server
- Docker installation (if not present)
- kamal-proxy installation
- Docker registry login
- Image build and push
- Container pull and start
- SSL configuration
You'll see detailed output for each step:
INFO [f97da026] Running docker login ...
INFO [f97da026] Finished in 1.337 seconds
INFO Building image ...
INFO Pushing image ...
INFO [48716498] Running docker pull ...
INFO [48716498] Finished in 8.234 seconds
INFO [48716498] Running docker run ...
INFO Waiting for healthy container ...
INFO Container is healthy!
6.2 Verify the Deployment
Once complete, verify everything is working:
# Check running containers
kamal details
# View application logs
kamal app logs
# Check proxy status
kamal proxy detailsVisit https://app.your-domain.com — your Next.js application should be accessible with a valid SSL certificate.
Step 7: Subsequent Deployments
For subsequent deployments, a single command is all you need:
kamal deployThat's it. Kamal will:
- Build the new image
- Push it to the registry
- Pull it on the server
- Start the new container
- Verify the healthcheck
- Switch traffic
- Stop the old container
Deploy Without Rebuilding the Image
If you've already built and pushed the image (e.g., in CI):
kamal deploy --skip-pushStep 8: Rollback
One of Kamal's greatest advantages is the ease of rollback. If a deployment causes issues:
# Roll back to a previous version
kamal rollback [VERSION_TAG]To see available versions:
kamal app containersRollback is nearly instantaneous because the previous image is already present on the server. Kamal simply restarts the old container and switches traffic.
Step 9: Managing Accessories (Databases, Redis, etc.)
Kamal can manage auxiliary services called accessories. Add them to your config/deploy.yml:
accessories:
db:
image: postgres:16-alpine
host: your-server.com
port: "5432:5432"
env:
clear:
POSTGRES_DB: my_app
secret:
- POSTGRES_PASSWORD
directories:
- data:/var/lib/postgresql/data
options:
network: kamal
redis:
image: redis:7-alpine
host: your-server.com
port: "6379:6379"
directories:
- data:/data
options:
network: kamalDeploy the accessories:
# Deploy all accessories
kamal accessory boot all
# Or a specific accessory
kamal accessory boot dbAccessories are persistent — they are not restarted during application deployments.
Step 10: Multi-Server Deployment
For high-traffic applications, you can deploy to multiple servers:
servers:
web:
hosts:
- server-1.your-domain.com
- server-2.your-domain.com
- server-3.your-domain.comKamal deploys to all servers in parallel. Combined with a load balancer (like a DigitalOcean Load Balancer or DNS round-robin), you get high availability with no extra effort.
Server Roles
You can define different roles for your servers:
servers:
web:
hosts:
- web-1.example.com
- web-2.example.com
worker:
hosts:
- worker-1.example.com
cmd: npm run workerStep 11: CI/CD Integration with GitHub Actions
Automate deployment with GitHub Actions:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
concurrency:
group: deploy
cancel-in-progress: true
env:
DOCKER_BUILDKIT: 1
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.3"
bundler-cache: true
- name: Install Kamal
run: gem install kamal
- name: Set up SSH
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Deploy with Kamal
env:
KAMAL_REGISTRY_PASSWORD: ${{ secrets.DOCKER_HUB_TOKEN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: kamal deployAdd the following secrets to your GitHub repository:
SSH_PRIVATE_KEY: your SSH private key for the serverDOCKER_HUB_TOKEN: your Docker Hub tokenDATABASE_URL: your database connection string
Step 12: Everyday Commands
Here are the Kamal commands you'll use most frequently:
Application Management
# Deploy
kamal deploy
# View logs in real-time
kamal app logs -f
# Execute a command in the container
kamal app exec "node -e 'console.log(process.env.NODE_ENV)'"
# Open an interactive session
kamal app exec -i bash
# Restart the application
kamal app boot
# Stop the application
kamal app stopProxy Management
# View proxy configuration
kamal proxy details
# Reload configuration
kamal proxy rebootAccessory Management
# View database logs
kamal accessory logs db
# Restart Redis
kamal accessory reboot redis
# Run psql
kamal accessory exec db "psql -U postgres my_app"Maintenance
# Clean up old Docker images
kamal prune all
# Lock deployments (maintenance)
kamal lock acquire -m "Maintenance in progress"
# Unlock
kamal lock releaseStep 13: Monitoring Your Application
Advanced Healthcheck
Improve your healthcheck endpoint to verify dependencies:
// app/api/health/route.ts
import { NextResponse } from 'next/server'
interface HealthStatus {
status: string
timestamp: string
uptime: number
checks: {
database?: string
memory?: {
used: number
total: number
}
}
}
export async function GET() {
const health: HealthStatus = {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
checks: {},
}
// Check memory
const memUsage = process.memoryUsage()
health.checks.memory = {
used: Math.round(memUsage.heapUsed / 1024 / 1024),
total: Math.round(memUsage.heapTotal / 1024 / 1024),
}
return NextResponse.json(health)
}External Monitoring
For comprehensive monitoring, you can add a monitoring service as an accessory:
accessories:
uptime-kuma:
image: louislam/uptime-kuma:1
host: your-server.com
port: "3001:3001"
directories:
- data:/app/data
options:
network: kamalTroubleshooting
Healthcheck Fails
If deployment fails with a healthcheck timeout:
-
Check container logs:
kamal app logs -
Test the healthcheck locally:
docker build -t test-app . docker run -p 3000:3000 test-app curl http://localhost:3000/api/health -
Increase the timeout in
deploy.yml:healthcheck: max_attempts: 60 interval: 5
SSH Permission Error
If Kamal can't connect to the server:
# Check that your key is loaded
ssh-add -l
# Add your key if needed
ssh-add ~/.ssh/id_ed25519
# Test the connection
ssh root@your-server.com "echo OK"Image Too Large
If the build or push takes too long, optimize your Dockerfile:
- Use multi-stage build (already done in our Dockerfile)
- Check your
.dockerignore - Use
node:20-alpineinstead ofnode:20 - Enable BuildKit for layer caching:
export DOCKER_BUILDKIT=1
SSL Certificate Issues
If SSL isn't working:
-
Verify DNS points to the server:
dig app.your-domain.com -
Check proxy logs:
kamal proxy logs -
Make sure ports 80 and 443 are open on the server firewall.
Kamal vs Alternatives
| Feature | Kamal 2 | Coolify | Vercel | Kubernetes |
|---|---|---|---|---|
| Zero downtime | Yes | Yes | Yes | Yes |
| Auto SSL | Yes | Yes | Yes | Complex |
| Rollback | 1 command | UI | Automatic | Complex |
| Multi-server | Yes | Yes | Managed | Yes |
| Cost | VPS only | VPS only | Per usage | High |
| Complexity | Low | Low | Very low | Very high |
| Vendor lock-in | None | Low | High | Low |
| Web UI | No | Yes | Yes | Dashboard |
| Built-in CI/CD | No (via GH Actions) | Yes | Yes | No |
Kamal stands out for its simplicity and zero vendor lock-in. A single YAML file, a few commands, and your application is in production. No web dashboard to manage, no cloud service to pay for — just SSH, Docker, and your server.
Next Steps
Now that your application is deployed with Kamal 2, here's how to go further:
- Add a CDN like Cloudflare in front of your server to improve performance
- Configure alerts with Uptime Kuma or Better Stack to be notified of issues
- Set up a staging environment with a second configuration file (
config/deploy.staging.yml) - Automate database backups with a cron job on the server
- Explore Kamal hooks to run scripts before/after deployment (database migrations, cache invalidation, etc.)
Conclusion
Kamal 2 represents a refreshing approach to deployment: no unnecessary complexity, no vendor lock-in, no surprise bills. With a 5 euro per month VPS and a few minutes of configuration, you get a professional deployment pipeline with zero downtime.
Kamal's philosophy aligns perfectly with the growing movement toward self-hosting and digital sovereignty. You maintain full control of your data and infrastructure while enjoying a smooth developer experience.
Whether you're deploying a side project, a startup, or an enterprise application, Kamal 2 gives you the power you need without the unnecessary complexity. And if tomorrow you need to switch server providers, just modify one line in your configuration file.