This guide explains how to deploy applications using Kamal with automatic secrets management.
Kamal orchestrates Docker deployments to your servers:
- Zero-downtime deploys with rolling restarts
- Automatic SSL via Let's Encrypt and Traefik proxy
- Health checks and automatic rollback
- Multi-service support (Python API, TypeScript web/API, databases)
- Secrets from 1Password automatically injected
All managed via: make kamal ARGS="<service> <stage> <command>"
Before deploying, ensure:
- Infrastructure Deployed - Server and DNS configured
- Container Registry - Docker repositories created
- 1Password Configured - Stage-specific secrets in vault
gem install kamalVerify:
kamal versionServices are defined in config/deploy/:
config/deploy/
├── py.yml # Python FastAPI + PostgreSQL
├── ts-web.yml # TypeScript Vite web app
└── static.yml # Static file service (optional)
Each service config specifies:
- Docker image name and registry
- Server hostnames (from Terraform outputs)
- Environment variables (from 1Password)
- Health check endpoints
- Proxy configuration (Traefik)
- Accessories (databases, Redis, etc.)
CRITICAL: Before deploying ANY service to a fresh server, you MUST bootstrap the Kamal infrastructure.
The first time you deploy to a server, run setup to install Kamal infrastructure:
# Bootstrap server with Kamal infrastructure
make kamal ARGS="py production setup"This installs:
- Traefik reverse proxy - Handles SSL and routing
- Docker networks - For container communication
- Required directories - For logs, caches, volumes
You only need to run setup once per server, not once per service.
If your service has accessories (databases, Redis, etc.), boot them BEFORE deploying the app:
# For Python app with PostgreSQL
make kamal ARGS="py production accessory boot postgres"Check which accessories a service has by looking at its config file:
# Check py.yml for accessories
grep -A 10 "accessories:" config/deploy/py.ymlNow you can deploy the actual application:
# Deploy Python API
make kamal ARGS="py production deploy"
# Deploy TypeScript web app
make kamal ARGS="ts-web production deploy"For a fresh server with multiple services:
# 1. Bootstrap server (only needed once)
make kamal ARGS="py production setup"
# 2. Boot database for Python app
make kamal ARGS="py production accessory boot postgres"
# 3. Deploy Python app
make kamal ARGS="py production deploy"
# 4. Deploy web app (no setup needed - server already bootstrapped)
make kamal ARGS="ts-web production deploy"Subsequent deployments only need the deploy command:
make kamal ARGS="py production deploy"
make kamal ARGS="ts-web production deploy"# Deploy Python API
make kamal ARGS="py production deploy"
# Deploy TypeScript web app
make kamal ARGS="ts-web production deploy"The deploy process:
- Builds Docker image locally
- Pushes to container registry
- Pulls image on server
- Runs health checks
- Switches traffic to new version
- Removes old containers
# View Python API logs
make kamal ARGS="py production logs"
# View web app logs
make kamal ARGS="ts-web production logs"
# Follow logs (live tail)
make kamal ARGS="py production logs --follow"
# View specific number of lines
make kamal ARGS="py production logs --lines 100"# Stop service
make kamal ARGS="py production stop"
# Start service
make kamal ARGS="py production start"
# Restart service
make kamal ARGS="py production restart"
# Rollback to previous version
make kamal ARGS="py production rollback"Config: config/deploy/py.yml
Includes:
- FastAPI application (port 8000)
- PostgreSQL database (accessory)
- Google OAuth environment variables
- Health check on
/health - SSL via Traefik
Environment Variables:
Automatically loaded from 1Password vault <project>-production:
GOOGLE_O_AUTH_CLIENT_IDGOOGLE_O_AUTH_CLIENT_SECRETPOSTGRES_URL(generated by database accessory)
Database Management:
# Start database only
make kamal ARGS="py production accessory boot postgres"
# Stop database
make kamal ARGS="py production accessory stop postgres"
# View database logs
make kamal ARGS="py production accessory logs postgres"
# Execute command in database
make kamal ARGS="py production accessory exec postgres psql -U <dbname>"Deploying:
# First time (includes database setup)
make kamal ARGS="py production setup"
make kamal ARGS="py production accessory boot postgres"
make kamal ARGS="py production deploy"
# Subsequent deployments
make kamal ARGS="py production deploy"The deploy automatically runs Alembic migrations via the startup script.
Config: config/deploy/ts-web.yml
Includes:
- Vite React application (port 5173)
- Static asset serving
- Health check on
/ - SSL via Traefik
Environment Variables: Build-time environment variables (if needed):
VITE_API_URL- Backend API URL
Deploying:
# First time
make kamal ARGS="ts-web production setup"
make kamal ARGS="ts-web production deploy"
# Subsequent deployments
make kamal ARGS="ts-web production deploy"Run commands inside containers:
# Python: Run database migration manually
make kamal ARGS="py production app exec ./bin/db.sh migrate"
# Python: Open Python shell
make kamal ARGS="py production app exec uv run python"
# Python: Check environment
make kamal ARGS="py production app exec env"
# Database: Connect to PostgreSQL
make kamal ARGS="py production accessory exec postgres psql -U <dbname>"# Show running containers
make kamal ARGS="py production ps"
# Show all details (containers, health, etc.)
make kamal ARGS="py production details"
# Show audit log (recent deployments)
make kamal ARGS="py production audit"# List images on server
make kamal ARGS="py production images"
# Remove old images (free up space)
make kamal ARGS="py production prune all"# Validate config file before deploying
kamal config validate -c config/deploy/py.yml
# Show rendered config (with secrets redacted)
kamal config show -c config/deploy/py.ymlYou forgot to bootstrap the server. Run setup first:
make kamal ARGS="py production setup"This must be run once before any deployments on a fresh server.
You forgot to boot the PostgreSQL accessory. Boot it before deploying:
# Boot the database
make kamal ARGS="py production accessory boot postgres"
# Then deploy
make kamal ARGS="py production deploy"Check logs:
make kamal ARGS="py production logs"Common issues:
- Missing environment variable: Check 1Password vault has required secrets
- Health check failing: Check health endpoint returns 200 OK
- Port conflict: Ensure no other containers using the port
- Accessory not running: Boot accessories first
The deployment will fail if health checks don't pass. To debug:
# SSH to server and check container
bin/ssh production
docker ps -a # See if container is running
docker logs <container-name> # View container logs
# Check health endpoint manually
curl http://localhost:8000/healthAuthentication issues with container registry:
# Check Docker Hub credentials in 1Password
bin/vault read DOCKER_HUB_USERNAME
bin/vault read DOCKER_HUB_PASSWORD
# Re-authenticate on server
bin/ssh production
docker login -u USERNAME -p PASSWORDCheck SSH key and server IP:
# Verify infrastructure outputs
make iac production output server_ip
make iac production output -raw ssh_private_key
# Test SSH manually
bin/ssh productionTraefik handles SSL via Let's Encrypt. Common issues:
# Check Traefik logs
bin/ssh production
docker logs <traefik-container>
# Verify DNS points to server
dig yourdomain.com # Should return SERVER_IP
# Check Traefik dashboard (if enabled)
curl http://localhost:8080/dashboard/Note: Let's Encrypt requires:
- Domain must resolve to server IP
- Port 80/443 must be accessible
- Valid email address in Kamal config
Before deploying:
# Build and test locally
make build
make test
make checkAlways test on staging before production:
# Deploy to staging
make kamal ARGS="py staging deploy"
# Test thoroughly
# Deploy to production
make kamal ARGS="py production deploy"Watch logs during deployment:
# In one terminal
make kamal ARGS="py production deploy"
# In another terminal
make kamal ARGS="py production logs --follow"Optimize Dockerfiles:
- Use multi-stage builds
- Minimize layers
- Remove build dependencies in final image
Remove old images to save disk space:
# Weekly or after major deployments
make kamal ARGS="py production prune all"
make kamal ARGS="ts-web production prune all"Before major updates:
# Backup PostgreSQL
bin/ssh production
docker exec <postgres-container> pg_dump -U <dbname> <dbname> > backup.sqlAfter successful deployment:
- Verify services: Visit your domain (https://yourdomain.com)
- Check logs:
make kamal ARGS="<service> production logs" - Monitor resources:
bin/ssh production && docker stats - Set up monitoring: Consider Uptime Robot, Sentry, etc.
- Configure backups: Set up automated database backups
For local development workflows, see Local Development Guide.