name: lvt-deploy description: Use when deploying LiveTemplate applications to production - covers Docker containerization, Fly.io deployment, Kubernetes setup, database persistence, and production best practices
lvt:deploy
Deploy LiveTemplate applications to production environments.
๐ฏ ACTIVATION RULES
Context Detection
This skill typically runs in existing LiveTemplate projects (.lvtrc exists).
โ Context Established By:
- Project context -
.lvtrcexists (most common scenario) - Agent context - User is working with
lvt-assistantagent - Keyword context - User mentions "lvt", "livetemplate", or "lt"
Keyword matching (case-insensitive): lvt, livetemplate, lt
Trigger Patterns
With Context: โ Generic prompts related to this skill's purpose
Without Context (needs keywords): โ Must mention "lvt", "livetemplate", or "lt" โ Generic requests without keywords
Overview
LiveTemplate apps are standard Go binaries with SQLite databases, making them easy to deploy anywhere Go runs. This skill covers:
- Docker - Containerize for any platform
- Fly.io - Optimized for SQLite apps with built-in persistence
- Kubernetes - For large-scale deployments
- Traditional VPS - Simple binary deployment
Prerequisites
Before deployment:
- โ App builds successfully (
go build ./cmd/myapp) - โ All migrations applied (
lvt migration status) - โ Tests pass (
go test ./...) - โ Production database prepared
- โ Environment variables configured
Docker Deployment
1. Create Dockerfile
# Build stage
FROM golang:1.26-alpine AS builder
WORKDIR /app
# Copy go mod files
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Build binary
RUN CGO_ENABLED=1 GOOS=linux go build -o /myapp cmd/myapp/main.go
# Runtime stage
FROM alpine:latest
# Install SQLite (required for CGO)
RUN apk --no-cache add ca-certificates sqlite-libs
WORKDIR /root/
# Copy binary from builder
COPY --from=builder /myapp .
# Copy database directory (optional, for bundled data)
# COPY app.db .
# Copy migrations (needed if embedding migration runner)
COPY database/migrations ./database/migrations
# Copy template files (if not embedded in binary)
COPY app ./app
# Copy static files and client library
COPY static ./static
COPY client ./client
# Expose port
EXPOSE 8080
# Run binary
CMD ["./myapp"]
2. Create .dockerignore
# .dockerignore
app.db
*.log
.git
.env
tmp/
dist/
*.test
coverage.out
3. Build and Run
# Build image
docker build -t myapp:latest .
# Run container
docker run -p 8080:8080 \
-v $(pwd)/data:/root/data \
-e DATABASE_PATH=/root/data/app.db \
myapp:latest
# Run with environment variables
docker run -p 8080:8080 \
-v $(pwd)/data:/root/data \
-e DATABASE_PATH=/root/data/app.db \
-e PORT=8080 \
-e ENV=production \
myapp:latest
4. Docker Compose
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
volumes:
- ./data:/root/data
environment:
- DATABASE_PATH=/root/data/app.db
- PORT=8080
- ENV=production
restart: unless-stopped
# Start with compose
docker-compose up -d
# View logs
docker-compose logs -f
# Stop
docker-compose down
Fly.io Deployment
Fly.io is ideal for SQLite apps with built-in persistence and global distribution.
1. Install flyctl
# macOS
brew install flyctl
# Linux
curl -L https://fly.io/install.sh | sh
# Login
fly auth login
2. Create fly.toml
# fly.toml
app = "myapp"
primary_region = "sjc"
[build]
builder = "paketobuildpacks/builder:base"
[env]
PORT = "8080"
[[services]]
http_checks = []
internal_port = 8080
processes = ["app"]
protocol = "tcp"
script_checks = []
[services.concurrency]
hard_limit = 25
soft_limit = 20
type = "connections"
[[services.ports]]
force_https = true
handlers = ["http"]
port = 80
[[services.ports]]
handlers = ["tls", "http"]
port = 443
[[services.tcp_checks]]
grace_period = "1s"
interval = "15s"
restart_limit = 0
timeout = "2s"
# SQLite persistence
[mounts]
source = "myapp_data"
destination = "/data"
3. Deploy to Fly.io
# Initialize app (first time only)
fly launch
# Create volume for SQLite database
fly volumes create myapp_data --region sjc --size 1
# Deploy
fly deploy
# Check status
fly status
# View logs
fly logs
# Open app
fly open
# SSH into instance
fly ssh console
4. Run Migrations on Fly.io
# Option 1: Run migrations before first deploy (in dev/CI)
lvt migration up
fly deploy
# Option 2: SSH and run goose
fly ssh console
goose -dir database/migrations sqlite3 /data/app.db up
# Option 3: Auto-run migrations on deploy (add goose to Dockerfile)
# Add to fly.toml:
[deploy]
release_command = "goose -dir database/migrations sqlite3 /data/app.db up"
5. Scale on Fly.io
# Scale to multiple regions
fly regions add iad lhr syd
# Scale VM size
fly scale vm shared-cpu-1x --memory 512
# Scale instance count
fly scale count 2
# Auto-scale
fly autoscale set min=1 max=5
Kubernetes Deployment
For large-scale production deployments.
1. Create Kubernetes Manifests
deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
labels:
app: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: myapp:latest
ports:
- containerPort: 8080
env:
- name: PORT
value: "8080"
- name: DATABASE_PATH
value: "/data/app.db"
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: myapp-pvc
service.yaml:
apiVersion: v1
kind: Service
metadata:
name: myapp-service
spec:
selector:
app: myapp
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: LoadBalancer
pvc.yaml (for SQLite persistence):
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: myapp-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
2. Deploy to Kubernetes
# Apply manifests
kubectl apply -f pvc.yaml
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
# Check status
kubectl get pods
kubectl get services
# View logs
kubectl logs -f deployment/myapp
# Scale replicas
kubectl scale deployment myapp --replicas=5
# Update image
kubectl set image deployment/myapp myapp=myapp:v2
3. Important: SQLite + Kubernetes
Warning: SQLite doesn't support concurrent writes across multiple pods. For K8s:
Option A: Single replica (simple)
spec:
replicas: 1 # Only one pod writes to SQLite
Option B: Read replicas (advanced)
# Use one writer pod + multiple read-only replicas
# Requires application-level read/write splitting
Option C: Switch to PostgreSQL
# For true horizontal scaling, migrate to PostgreSQL
# LiveTemplate's sqlc-based architecture makes this easier
Traditional VPS Deployment
Simple deployment to DigitalOcean, Linode, AWS EC2, etc.
1. Build Binary
# Build for Linux (if developing on macOS)
GOOS=linux GOARCH=amd64 CGO_ENABLED=1 \
go build -o myapp cmd/myapp/main.go
# Or build on the server itself
ssh user@server
cd /opt/myapp
go build -o myapp cmd/myapp/main.go
2. Create systemd Service
# /etc/systemd/system/myapp.service
[Unit]
Description=MyApp LiveTemplate Application
After=network.target
[Service]
Type=simple
User=myapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/myapp
Restart=on-failure
RestartSec=5s
Environment="PORT=8080"
Environment="DATABASE_PATH=/var/lib/myapp/app.db"
Environment="ENV=production"
[Install]
WantedBy=multi-user.target
3. Deploy Steps
# 1. Copy files to server
scp -r . user@server:/opt/myapp/
# 2. SSH to server
ssh user@server
# 3. Setup
sudo useradd -r -s /bin/false myapp
sudo mkdir -p /var/lib/myapp
sudo chown myapp:myapp /var/lib/myapp
cd /opt/myapp
# 4. Install dependencies & build
go mod download
go build -o myapp cmd/myapp/main.go
# 5. Run migrations
# Option A: If lvt CLI is available
lvt migration up
# Option B: Use goose directly
go install github.com/pressly/goose/v3/cmd/goose@latest
goose -dir database/migrations sqlite3 /var/lib/myapp/app.db up
# 6. Enable systemd service
sudo systemctl enable myapp
sudo systemctl start myapp
sudo systemctl status myapp
# 7. View logs
sudo journalctl -u myapp -f
4. Nginx Reverse Proxy
# /etc/nginx/sites-available/myapp
server {
listen 80;
server_name myapp.com;
location / {
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# Enable site
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
# Add SSL with certbot
sudo certbot --nginx -d myapp.com
Production Considerations
1. Database Backups
SQLite backups:
# Automated backup script
#!/bin/bash
# /opt/myapp/backup.sh
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/var/backups/myapp"
DB_PATH="/var/lib/myapp/app.db"
mkdir -p $BACKUP_DIR
# Backup with SQLite's backup command
sqlite3 $DB_PATH ".backup '$BACKUP_DIR/app_$DATE.db'"
# Keep only last 7 days
find $BACKUP_DIR -name "app_*.db" -mtime +7 -delete
echo "Backup completed: app_$DATE.db"
Cron job:
# Run backup daily at 2 AM
0 2 * * * /opt/myapp/backup.sh
Fly.io backup:
# Snapshot volume
fly volumes snapshots create myapp_data
# List snapshots
fly volumes snapshots list myapp_data
# Restore from snapshot
fly volumes restore myapp_data <snapshot-id>
2. Environment Variables
Create .env file (never commit to git):
# .env
PORT=8080
DATABASE_PATH=/var/lib/myapp/app.db
ENV=production
SECRET_KEY=your-secret-key-here
Load in app:
// cmd/myapp/main.go
package main
import (
"os"
"github.com/joho/godotenv"
)
func main() {
// Load .env in development
if os.Getenv("ENV") != "production" {
godotenv.Load()
}
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
// ... rest of main
}
3. Migrations in Production
IMPORTANT: LiveTemplate migrations are run with lvt migration up (development) or goose (production).
Before deployment - Run migrations in development:
# Best practice: Run migrations before building
cd /path/to/app
lvt migration status
lvt migration up
go build ./cmd/myapp
Option A: Include goose in Docker image:
# Add to Dockerfile before CMD
RUN go install github.com/pressly/goose/v3/cmd/goose@latest
# Run migrations on container start
CMD goose -dir database/migrations sqlite3 /data/app.db up && ./myapp
Option B: Run migrations manually before deploy:
# Docker
docker run --rm \
-v $(pwd)/data:/data \
myapp:latest \
goose -dir database/migrations sqlite3 /data/app.db up
# Fly.io
fly ssh console
goose -dir database/migrations sqlite3 /data/app.db up
# VPS
ssh user@server
cd /opt/myapp
lvt migration up # If lvt CLI is available
# OR
goose -dir database/migrations sqlite3 /var/lib/myapp/app.db up
sudo systemctl restart myapp
Option C: Auto-migrate on deploy (advanced):
// cmd/myapp/main.go
// Requires embedding goose or running migrations programmatically
import (
"database/sql"
"github.com/pressly/goose/v3"
)
func main() {
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
log.Fatal(err)
}
// Run migrations
if err := goose.Up(db, "database/migrations"); err != nil {
log.Fatalf("Migration failed: %v", err)
}
// Start server
// ...
}
4. Monitoring and Logs
Structured logging:
import "log/slog"
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("Server starting", "port", port)
logger.Error("Database error", "error", err)
Health check endpoint:
// Add to your routes
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
// Check database
if err := db.Ping(); err != nil {
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(map[string]string{
"status": "unhealthy",
"error": err.Error(),
})
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{
"status": "healthy",
})
})
5. Performance Tuning
SQLite optimizations:
// In database initialization
db.Exec("PRAGMA journal_mode=WAL") // Better concurrency
db.Exec("PRAGMA synchronous=NORMAL") // Faster writes
db.Exec("PRAGMA cache_size=-64000") // 64MB cache
db.Exec("PRAGMA temp_store=MEMORY") // In-memory temp tables
6. Static Files and Templates
LiveTemplate serves:
- Client library:
livetemplate-client.browser.js - Template files:
app/**/*.tmpl - Static assets: CSS, images, etc.
Option A: Embed in binary (recommended):
// cmd/myapp/main.go
import "embed"
//go:embed app
var templates embed.FS
//go:embed static
var static embed.FS
//go:embed client
var client embed.FS
func main() {
// Use embedded filesystems
// ...
}
Option B: Copy files in Docker:
# Already shown in Dockerfile above
COPY app ./app
COPY static ./static
COPY client ./client
Verify static files are served:
curl http://localhost:8080/static/livetemplate-client.browser.js
# Should return JavaScript file, not 404
Common Deployment Mistakes
โ Missing CGO_ENABLED for SQLite
# WRONG - SQLite won't work
CGO_ENABLED=0 go build ./cmd/myapp
# CORRECT
CGO_ENABLED=1 go build ./cmd/myapp
Why wrong: SQLite requires CGO. Without it, database operations fail.
โ Not Persisting Database
# WRONG - database lost on container restart
FROM alpine
COPY myapp .
CMD ["./myapp"]
# CORRECT - mount volume for persistence
FROM alpine
COPY myapp .
VOLUME ["/data"]
ENV DATABASE_PATH=/data/app.db
CMD ["./myapp"]
Why wrong: Without volume, database is lost when container stops.
โ Forgetting Migrations in Production
# WRONG - deploy without migrating
git push production main
# App crashes: "table not found"
# CORRECT
ssh production
./myapp migrate up # Run migrations first
sudo systemctl restart myapp
Why wrong: Production database is out of sync with code.
โ Multiple SQLite Writers in K8s
# WRONG - SQLite corruption
spec:
replicas: 5 # 5 pods writing to same SQLite file!
# CORRECT - single writer
spec:
replicas: 1 # SQLite handles one writer at a time
Why wrong: SQLite doesn't support concurrent writes from multiple processes.
โ Hardcoded Paths and Ports
// WRONG
db, err := sql.Open("sqlite3", "/Users/me/myapp/app.db")
server.ListenAndServe(":8080", nil)
// CORRECT
dbPath := os.Getenv("DATABASE_PATH")
if dbPath == "" {
dbPath = "./app.db"
}
db, err := sql.Open("sqlite3", dbPath)
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
server.ListenAndServe(":" + port, nil)
Why wrong: Environment-specific paths break in production.
โ Cross-Compiling with CGO
# WRONG - cross-compilation with CGO doesn't work simply
GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build ./cmd/myapp
# Error: C compiler not found or wrong architecture
# CORRECT - use Docker for cross-platform builds
docker build -t myapp:latest .
# OR - build on target platform
ssh server
go build ./cmd/myapp
# OR - use cross-compilation tools (advanced)
# Install cross-compiler: brew install FiloSottile/musl-cross/musl-cross
CC=x86_64-linux-musl-gcc GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build ./cmd/myapp
Why wrong: SQLite requires CGO, which needs platform-specific C compilers. Docker multi-stage builds handle this automatically.
Deployment Checklist
Before deploying:
- All tests pass (
go test ./...) - Builds successfully (
go build ./cmd/myapp) - Migrations applied (
lvt migration status) - Environment variables configured
- Database backup strategy in place
- Health check endpoint added
- Logs configured (structured JSON)
- .env file created (not committed)
- Dockerfile tested locally
- Static files/templates embedded or copied
- Client library accessible
- CGO enabled for SQLite (
CGO_ENABLED=1) - Reverse proxy configured (if needed)
After deploying:
- App accessible at production URL
- Health check returns 200
- Database queries work
- WebSocket connections work
- Static files load correctly
- Logs are being captured
- Backups running on schedule
- Monitoring alerts configured
Quick Reference
| I want to... | Best Option | Command |
|---|---|---|
| Deploy quickly | Fly.io | fly launch && fly deploy |
| Containerize | Docker | docker build -t myapp . |
| Use existing VPS | systemd | systemctl enable myapp |
| Auto-scale | Fly.io or K8s | fly autoscale set min=1 max=5 |
| Global distribution | Fly.io | fly regions add iad lhr |
| Test locally | Docker Compose | docker-compose up |
| Backup database | Cron + SQLite | sqlite3 app.db .backup |
| Run migrations | goose | goose -dir database/migrations sqlite3 app.db up |
Recommended: Fly.io for SQLite Apps
For most LiveTemplate apps, Fly.io is the best choice:
โ Built-in SQLite persistence (volumes) โ Global distribution (multi-region) โ Auto-scaling โ Zero-downtime deploys โ Built-in SSL โ Easy rollbacks โ Affordable ($0-5/month for small apps)
# Complete Fly.io deployment
fly launch
fly volumes create myapp_data --size 1
fly deploy
fly open
Remember
โ Enable CGO for SQLite builds (CGO_ENABLED=1)
โ Use volumes/mounts for database persistence
โ Run migrations with lvt migration up (dev) or goose (production)
โ Configure environment variables (never hardcode)
โ Set up automated backups
โ Add health check endpoint
โ Test Docker builds locally before deploying
โ SQLite = single writer (use replicas=1 in K8s)
โ Embed or copy static files/templates/client library
โ Use Docker for cross-platform builds (CGO cross-compilation is complex)
โ Don't deploy without testing build
โ Don't forget database persistence
โ Don't skip migrations in production
โ Don't assume ./myapp migrate up exists (use lvt or goose)
โ Don't use multiple SQLite writers in K8s
โ Don't hardcode paths or ports
โ Don't commit .env files or secrets
โ Don't deploy without backup strategy
โ Don't try simple cross-compilation with CGO (use Docker)