Skip to main content
This guide covers deploying your Kit application to a VPS (Virtual Private Server) using Hetzner Cloud. The same principles apply to other VPS providers like DigitalOcean Droplets, Linode, Vultr, or AWS EC2.

Prerequisites

  • A VPS running Ubuntu 22.04 or Debian 12
  • SSH access to your server
  • A domain name pointed to your server’s IP address
  • Your Kit project with a Dockerfile (run kit docker:init)

Server Setup

1. Create a VPS

  1. Go to Hetzner Cloud Console
  2. Create a new project and add a server
  3. Choose Ubuntu 22.04 as the image
  4. Select your server size (CX11 is fine for small apps)
  5. Add your SSH key for secure access

2. Initial Server Configuration

SSH into your server and run initial setup:
# Update packages
apt update && apt upgrade -y

# Create a non-root user for your app
useradd -m -s /bin/bash app
mkdir -p /opt/myapp
chown app:app /opt/myapp

# Install required packages
apt install -y curl postgresql redis-server

3. Configure PostgreSQL

# Create database and user
sudo -u postgres psql << EOF
CREATE USER myapp WITH PASSWORD 'your_secure_password';
CREATE DATABASE myapp_production OWNER myapp;
GRANT ALL PRIVILEGES ON DATABASE myapp_production TO myapp;
EOF
For production, consider using a managed database service like Hetzner’s upcoming managed PostgreSQL, or services like Neon, Supabase, or AWS RDS for better reliability and backups.

Deploy Options

Choose one of the following deployment methods:
Build on your local machine and upload the binary:
# On your local machine
# Cross-compile for Linux (if on macOS)
cargo build --release --target x86_64-unknown-linux-gnu

# Or build with Docker for Linux
docker build -t myapp .
docker create --name temp myapp
docker cp temp:/app/app ./app-linux
docker rm temp

# Upload to server
scp ./app-linux root@your-server:/opt/myapp/app

Environment Configuration

Create your production environment file:
cat > /opt/myapp/.env.production << 'EOF'
APP_ENV=production
SERVER_HOST=127.0.0.1
SERVER_PORT=8080

# Database
DATABASE_URL=postgres://myapp:your_secure_password@localhost:5432/myapp_production

# Redis (optional)
REDIS_URL=redis://127.0.0.1:6379

# Your app-specific variables
APP_KEY=your-secure-app-key
EOF

# Secure the file
chmod 600 /opt/myapp/.env.production
chown app:app /opt/myapp/.env.production

systemd Services

Web Server Service

Create /etc/systemd/system/myapp.service:
[Unit]
Description=Kit Application
After=network.target postgresql.service redis.service
Requires=postgresql.service

[Service]
Type=simple
User=app
Group=app
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/app
Restart=always
RestartSec=5

# Environment
EnvironmentFile=/opt/myapp/.env.production

# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/opt/myapp

[Install]
WantedBy=multi-user.target

Scheduler Service

If your app has scheduled tasks, create /etc/systemd/system/myapp-scheduler.service:
[Unit]
Description=Kit Scheduler
After=network.target myapp.service
Requires=myapp.service

[Service]
Type=simple
User=app
Group=app
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/app schedule:work
Restart=always
RestartSec=5

# Environment
EnvironmentFile=/opt/myapp/.env.production

# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/opt/myapp

[Install]
WantedBy=multi-user.target

Enable and Start Services

# Reload systemd
systemctl daemon-reload

# Enable services (start on boot)
systemctl enable myapp
systemctl enable myapp-scheduler

# Start services
systemctl start myapp
systemctl start myapp-scheduler

# Check status
systemctl status myapp
systemctl status myapp-scheduler

Caddy Reverse Proxy

Caddy automatically handles HTTPS certificates with Let’s Encrypt.

Install Caddy

apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
apt update
apt install caddy

Configure Caddy

Edit /etc/caddy/Caddyfile:
myapp.com {
    reverse_proxy localhost:8080

    # Enable compression
    encode gzip

    # Logging
    log {
        output file /var/log/caddy/myapp.log
    }
}
Replace myapp.com with your actual domain.

Start Caddy

systemctl enable caddy
systemctl start caddy
Caddy will automatically obtain and renew SSL certificates.

Health Checks

Kit includes a built-in /_kit/health endpoint that returns:
{
  "status": "ok",
  "timestamp": "2024-12-28T10:30:00Z"
}

Check Database Connectivity

Add ?db=true to also verify database connectivity:
curl https://myapp.com/_kit/health?db=true
Returns:
{
  "status": "ok",
  "timestamp": "2024-12-28T10:30:00Z",
  "database": "connected"
}

External Monitoring

Use the health endpoint with monitoring services:
  • UptimeRobot: Add HTTP monitor for https://myapp.com/_kit/health
  • Better Uptime: Configure health check endpoint
  • Grafana: Scrape health endpoint metrics

Deployment Script

Create a deployment script for easy updates:
#!/bin/bash
# deploy.sh - Run on your local machine

set -e

SERVER="root@your-server"
APP_PATH="/opt/myapp"

echo "Building application..."
cargo build --release --target x86_64-unknown-linux-gnu

echo "Uploading binary..."
scp target/x86_64-unknown-linux-gnu/release/app $SERVER:$APP_PATH/app.new

echo "Deploying..."
ssh $SERVER << 'EOF'
    cd /opt/myapp

    # Stop services
    systemctl stop myapp-scheduler || true
    systemctl stop myapp

    # Replace binary
    mv app.new app
    chmod +x app

    # Run migrations
    sudo -u app ./app migrate

    # Start services
    systemctl start myapp
    systemctl start myapp-scheduler

    # Verify health
    sleep 2
    curl -f http://localhost:8080/_kit/health || exit 1

    echo "Deployment complete!"
EOF
Make it executable:
chmod +x deploy.sh
./deploy.sh

Logs and Monitoring

View Logs

# Web server logs
journalctl -u myapp -f

# Scheduler logs
journalctl -u myapp-scheduler -f

# Caddy access logs
tail -f /var/log/caddy/myapp.log

Log Rotation

systemd’s journald handles log rotation automatically. For long-term storage, consider:
  • Loki + Grafana: Self-hosted log aggregation
  • Papertrail: Cloud-based logging service
  • Logtail: Simple log management

Firewall Configuration

Secure your server with UFW:
# Allow SSH
ufw allow 22/tcp

# Allow HTTP/HTTPS (Caddy)
ufw allow 80/tcp
ufw allow 443/tcp

# Enable firewall
ufw enable
Never expose port 8080 directly. Always use Caddy as a reverse proxy to handle SSL and security headers.

Scaling

Vertical Scaling

Upgrade your VPS to a larger instance for more CPU/memory.

Horizontal Scaling

For multiple instances:
  1. Set up a load balancer (Hetzner Load Balancer or HAProxy)
  2. Use a managed database (external PostgreSQL)
  3. Use Redis for session storage
  4. Deploy multiple app instances behind the load balancer

Costs

Hetzner offers competitive pricing:
InstancevCPURAMStorageMonthly
CX1112GB20GB~$4
CX2124GB40GB~$6
CX3128GB80GB~$12
CX41416GB160GB~$22
Plus managed PostgreSQL when available, or use external services.

Troubleshooting

Service Won’t Start

Check logs for errors:
journalctl -u myapp -n 50
Common issues:
  • Missing environment variables
  • Database connection failed
  • Port already in use

Caddy Certificate Errors

Ensure:
  • Domain DNS points to your server
  • Ports 80 and 443 are open
  • No other service is using port 80
caddy validate --config /etc/caddy/Caddyfile

Database Connection Issues

Test connection manually:
sudo -u app psql $DATABASE_URL -c "SELECT 1"

Health Check Failing

# Check if app is running
systemctl status myapp

# Test health endpoint directly
curl http://localhost:8080/_kit/health

# Check with database
curl http://localhost:8080/_kit/health?db=true