| 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 -
.lvtrc exists (most common scenario)
- Agent context - User is working with
lvt-assistant agent
- 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
docker build -t myapp:latest .
docker run -p 8080:8080 \
-v $(pwd)/data:/root/data \
-e DATABASE_PATH=/root/data/app.db \
myapp:latest
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
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
docker-compose up -d
docker-compose logs -f
docker-compose down
Fly.io Deployment
Fly.io is ideal for SQLite apps with built-in persistence and global distribution.
1. Install flyctl
brew install flyctl
curl -L https://fly.io/install.sh | sh
fly auth login
2. Create 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"
[mounts]
source = "myapp_data"
destination = "/data"
3. Deploy to Fly.io
fly launch
fly volumes create myapp_data --region sjc --size 1
fly deploy
fly status
fly logs
fly open
fly ssh console
4. Run Migrations on Fly.io
lvt migration up
fly deploy
fly ssh console
goose -dir database/migrations sqlite3 /data/app.db up
[deploy]
release_command = "goose -dir database/migrations sqlite3 /data/app.db up"
5. Scale on Fly.io
fly regions add iad lhr syd
fly scale vm shared-cpu-1x --memory 512
fly scale count 2
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
kubectl apply -f pvc.yaml
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
kubectl get pods
kubectl get services
kubectl logs -f deployment/myapp
kubectl scale deployment myapp --replicas=5
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
Option B: Read replicas (advanced)
Option C: Switch to PostgreSQL
Traditional VPS Deployment
Simple deployment to DigitalOcean, Linode, AWS EC2, etc.
1. Build Binary
GOOS=linux GOARCH=amd64 CGO_ENABLED=1 \
go build -o myapp cmd/myapp/main.go
ssh user@server
cd /opt/myapp
go build -o myapp cmd/myapp/main.go
2. Create systemd 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
scp -r . user@server:/opt/myapp/
ssh user@server
sudo useradd -r -s /bin/false myapp
sudo mkdir -p /var/lib/myapp
sudo chown myapp:myapp /var/lib/myapp
cd /opt/myapp
go mod download
go build -o myapp cmd/myapp/main.go
lvt migration up
go install github.com/pressly/goose/v3/cmd/goose@latest
goose -dir database/migrations sqlite3 /var/lib/myapp/app.db up
sudo systemctl enable myapp
sudo systemctl start myapp
sudo systemctl status myapp
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;
}
}
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d myapp.com
Production Considerations
1. Database Backups
SQLite backups:
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/var/backups/myapp"
DB_PATH="/var/lib/myapp/app.db"
mkdir -p $BACKUP_DIR
sqlite3 $DB_PATH ".backup '$BACKUP_DIR/app_$DATE.db'"
find $BACKUP_DIR -name "app_*.db" -mtime +7 -delete
echo "Backup completed: app_$DATE.db"
Cron job:
0 2 * * * /opt/myapp/backup.sh
Fly.io backup:
fly volumes snapshots create myapp_data
fly volumes snapshots list myapp_data
fly volumes restore myapp_data <snapshot-id>
2. Environment Variables
Create .env file (never commit to git):
PORT=8080
DATABASE_PATH=/var/lib/myapp/app.db
ENV=production
SECRET_KEY=your-secret-key-here
Load in app:
package main
import (
"os"
"github.com/joho/godotenv"
)
func main() {
if os.Getenv("ENV") != "production" {
godotenv.Load()
}
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
}
3. Migrations in Production
IMPORTANT: LiveTemplate migrations are run with lvt migration up (development) or goose (production).
Before deployment - Run migrations in development:
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 run --rm \
-v $(pwd)/data:/data \
myapp:latest \
goose -dir database/migrations sqlite3 /data/app.db up
fly ssh console
goose -dir database/migrations sqlite3 /data/app.db up
ssh user@server
cd /opt/myapp
lvt migration up
goose -dir database/migrations sqlite3 /var/lib/myapp/app.db up
sudo systemctl restart myapp
Option C: Auto-migrate on deploy (advanced):
import (
"database/sql"
"github.com/pressly/goose/v3"
)
func main() {
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
log.Fatal(err)
}
if err := goose.Up(db, "database/migrations"); err != nil {
log.Fatalf("Migration failed: %v", err)
}
}
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:
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
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:
db.Exec("PRAGMA journal_mode=WAL")
db.Exec("PRAGMA synchronous=NORMAL")
db.Exec("PRAGMA cache_size=-64000")
db.Exec("PRAGMA temp_store=MEMORY")
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):
import "embed"
var templates embed.FS
var static embed.FS
var client embed.FS
func main() {
}
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
Common Deployment Mistakes
❌ Missing CGO_ENABLED for SQLite
CGO_ENABLED=0 go build ./cmd/myapp
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
git push production main
ssh production
./myapp migrate up
sudo systemctl restart myapp
Why wrong: Production database is out of sync with code.
❌ Multiple SQLite Writers in K8s
spec:
replicas: 5
spec:
replicas: 1
Why wrong: SQLite doesn't support concurrent writes from multiple processes.
❌ Hardcoded Paths and Ports
db, err := sql.Open("sqlite3", "/Users/me/myapp/app.db")
server.ListenAndServe(":8080", nil)
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
GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build ./cmd/myapp
docker build -t myapp:latest .
ssh server
go build ./cmd/myapp
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:
After deploying:
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)
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)