Files
GopherGate/deploy.sh
hobokenchicken d5e326bc65
Some checks failed
CI / Check (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Formatting (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Release Build (push) Has been cancelled
fix(deploy): harden deploy.sh for Arch Linux and zero-downtime updates
- Add GIT_REPO config variable with actual Gitea remote URL
- Auto-detect nologin path for Arch/Debian compatibility
- Rewrite update() to build before stopping service (minimizes downtime)
- Abort update cleanly if build fails without interrupting service
- Fix all placeholder github.com/yourusername URLs
- Add status and logs convenience commands
2026-03-03 09:44:23 -05:00

666 lines
16 KiB
Bash
Executable File

#!/bin/bash
# LLM Proxy Gateway Deployment Script
# This script automates the deployment of the LLM Proxy Gateway on a Linux server
set -e # Exit on error
set -u # Exit on undefined variable
# Configuration
APP_NAME="llm-proxy"
APP_USER="llmproxy"
APP_GROUP="llmproxy"
GIT_REPO="ssh://git.dustin.coffee:2222/hobokenchicken/llm-proxy.git"
INSTALL_DIR="/opt/$APP_NAME"
CONFIG_DIR="/etc/$APP_NAME"
DATA_DIR="/var/lib/$APP_NAME"
LOG_DIR="/var/log/$APP_NAME"
SERVICE_FILE="/etc/systemd/system/$APP_NAME.service"
ENV_FILE="$CONFIG_DIR/.env"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Logging functions
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Check if running as root
check_root() {
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root"
exit 1
fi
}
# Install system dependencies
install_dependencies() {
log_info "Installing system dependencies..."
# Detect package manager
if command -v apt-get &> /dev/null; then
# Debian/Ubuntu
apt-get update
apt-get install -y \
build-essential \
pkg-config \
libssl-dev \
sqlite3 \
curl \
git
elif command -v yum &> /dev/null; then
# RHEL/CentOS
yum groupinstall -y "Development Tools"
yum install -y \
openssl-devel \
sqlite \
curl \
git
elif command -v dnf &> /dev/null; then
# Fedora
dnf groupinstall -y "Development Tools"
dnf install -y \
openssl-devel \
sqlite \
curl \
git
elif command -v pacman &> /dev/null; then
# Arch Linux
pacman -Syu --noconfirm \
base-devel \
openssl \
sqlite \
curl \
git
else
log_warn "Could not detect package manager. Please install dependencies manually."
fi
}
# Install Rust if not present
install_rust() {
log_info "Checking for Rust installation..."
if ! command -v rustc &> /dev/null; then
log_info "Installing Rust..."
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source "$HOME/.cargo/env"
else
log_info "Rust is already installed"
fi
# Verify installation
rustc --version
cargo --version
}
# Create system user and directories
setup_directories() {
log_info "Creating system user and directories..."
# Create user and group if they don't exist
if ! id "$APP_USER" &>/dev/null; then
# Arch uses /usr/bin/nologin, Debian/Ubuntu use /usr/sbin/nologin
NOLOGIN=$(command -v nologin 2>/dev/null || echo "/usr/bin/nologin")
useradd -r -s "$NOLOGIN" -M "$APP_USER"
fi
# Create directories
mkdir -p "$INSTALL_DIR"
mkdir -p "$CONFIG_DIR"
mkdir -p "$DATA_DIR"
mkdir -p "$LOG_DIR"
# Set permissions
chown -R "$APP_USER:$APP_GROUP" "$INSTALL_DIR"
chown -R "$APP_USER:$APP_GROUP" "$CONFIG_DIR"
chown -R "$APP_USER:$APP_GROUP" "$DATA_DIR"
chown -R "$APP_USER:$APP_GROUP" "$LOG_DIR"
chmod 750 "$INSTALL_DIR"
chmod 750 "$CONFIG_DIR"
chmod 750 "$DATA_DIR"
chmod 750 "$LOG_DIR"
}
# Build the application
build_application() {
log_info "Building the application..."
# Clone or update repository
if [[ ! -d "$INSTALL_DIR/.git" ]]; then
log_info "Cloning repository..."
git clone "$GIT_REPO" "$INSTALL_DIR"
else
log_info "Updating repository..."
cd "$INSTALL_DIR"
git pull
fi
# Build in release mode
cd "$INSTALL_DIR"
log_info "Building release binary..."
cargo build --release
# Verify build
if [[ -f "target/release/$APP_NAME" ]]; then
log_info "Build successful"
else
log_error "Build failed"
exit 1
fi
}
# Create configuration files
create_configuration() {
log_info "Creating configuration files..."
# Create .env file with API keys
cat > "$ENV_FILE" << EOF
# LLM Proxy Gateway Environment Variables
# Add your API keys here
# OpenAI API Key
# OPENAI_API_KEY=sk-your-key-here
# Google Gemini API Key
# GEMINI_API_KEY=AIza-your-key-here
# DeepSeek API Key
# DEEPSEEK_API_KEY=sk-your-key-here
# xAI Grok API Key
# GROK_API_KEY=gk-your-key-here
# Authentication tokens (comma-separated)
# LLM_PROXY__SERVER__AUTH_TOKENS=token1,token2,token3
EOF
# Create config.toml
cat > "$CONFIG_DIR/config.toml" << EOF
# LLM Proxy Gateway Configuration
[server]
port = 8080
host = "0.0.0.0"
# auth_tokens = ["token1", "token2", "token3"] # Uncomment to enable authentication
[database]
path = "$DATA_DIR/llm_proxy.db"
max_connections = 5
[providers.openai]
enabled = true
api_key_env = "OPENAI_API_KEY"
base_url = "https://api.openai.com/v1"
default_model = "gpt-4o"
[providers.gemini]
enabled = true
api_key_env = "GEMINI_API_KEY"
base_url = "https://generativelanguage.googleapis.com/v1"
default_model = "gemini-2.0-flash"
[providers.deepseek]
enabled = true
api_key_env = "DEEPSEEK_API_KEY"
base_url = "https://api.deepseek.com"
default_model = "deepseek-reasoner"
[providers.grok]
enabled = false # Disabled by default until API is researched
api_key_env = "GROK_API_KEY"
base_url = "https://api.x.ai/v1"
default_model = "grok-beta"
[model_mapping]
"gpt-*" = "openai"
"gemini-*" = "gemini"
"deepseek-*" = "deepseek"
"grok-*" = "grok"
[pricing]
openai = { input = 0.01, output = 0.03 }
gemini = { input = 0.0005, output = 0.0015 }
deepseek = { input = 0.00014, output = 0.00028 }
grok = { input = 0.001, output = 0.003 }
EOF
# Set permissions
chown "$APP_USER:$APP_GROUP" "$ENV_FILE"
chown "$APP_USER:$APP_GROUP" "$CONFIG_DIR/config.toml"
chmod 640 "$ENV_FILE"
chmod 640 "$CONFIG_DIR/config.toml"
}
# Create systemd service
create_systemd_service() {
log_info "Creating systemd service..."
cat > "$SERVICE_FILE" << EOF
[Unit]
Description=LLM Proxy Gateway
Documentation=https://git.dustin.coffee/hobokenchicken/llm-proxy
After=network.target
Wants=network.target
[Service]
Type=simple
User=$APP_USER
Group=$APP_GROUP
WorkingDirectory=$INSTALL_DIR
EnvironmentFile=$ENV_FILE
Environment="RUST_LOG=info"
Environment="LLM_PROXY__CONFIG_PATH=$CONFIG_DIR/config.toml"
ExecStart=$INSTALL_DIR/target/release/$APP_NAME
Restart=on-failure
RestartSec=5
# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=$DATA_DIR $LOG_DIR
# Resource limits (adjust based on your server)
MemoryMax=400M
MemorySwapMax=100M
CPUQuota=50%
LimitNOFILE=65536
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=$APP_NAME
[Install]
WantedBy=multi-user.target
EOF
# Reload systemd
systemctl daemon-reload
}
# Setup nginx reverse proxy (optional)
setup_nginx_proxy() {
if ! command -v nginx &> /dev/null; then
log_warn "nginx not installed. Skipping reverse proxy setup."
return
fi
log_info "Setting up nginx reverse proxy..."
cat > "/etc/nginx/sites-available/$APP_NAME" << EOF
server {
listen 80;
server_name your-domain.com; # Change to your domain
# Redirect to HTTPS (recommended)
return 301 https://\$server_name\$request_uri;
}
server {
listen 443 ssl http2;
server_name your-domain.com; # Change to your domain
# SSL certificates (adjust paths)
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
# SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# Proxy to LLM Proxy Gateway
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host \$host;
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;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Health check endpoint
location /health {
proxy_pass http://127.0.0.1:8080/health;
access_log off;
}
# Dashboard
location /dashboard {
proxy_pass http://127.0.0.1:8080/dashboard;
}
}
EOF
# Enable site
ln -sf "/etc/nginx/sites-available/$APP_NAME" "/etc/nginx/sites-enabled/"
# Test nginx configuration
nginx -t
log_info "nginx configuration created. Please update the domain and SSL certificate paths."
}
# Setup firewall
setup_firewall() {
log_info "Configuring firewall..."
# Check for ufw (Ubuntu)
if command -v ufw &> /dev/null; then
ufw allow 22/tcp # SSH
ufw allow 80/tcp # HTTP
ufw allow 443/tcp # HTTPS
ufw --force enable
log_info "UFW firewall configured"
fi
# Check for firewalld (RHEL/CentOS)
if command -v firewall-cmd &> /dev/null; then
firewall-cmd --permanent --add-service=ssh
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload
log_info "Firewalld configured"
fi
}
# Initialize database
initialize_database() {
log_info "Initializing database..."
# Run the application once to create database
sudo -u "$APP_USER" "$INSTALL_DIR/target/release/$APP_NAME" --help &> /dev/null || true
log_info "Database initialized at $DATA_DIR/llm_proxy.db"
}
# Start and enable service
start_service() {
log_info "Starting $APP_NAME service..."
systemctl enable "$APP_NAME"
systemctl start "$APP_NAME"
# Check status
sleep 2
systemctl status "$APP_NAME" --no-pager
}
# Verify installation
verify_installation() {
log_info "Verifying installation..."
# Check if service is running
if systemctl is-active --quiet "$APP_NAME"; then
log_info "Service is running"
else
log_error "Service is not running"
journalctl -u "$APP_NAME" -n 20 --no-pager
exit 1
fi
# Test health endpoint
if curl -s http://localhost:8080/health | grep -q "OK"; then
log_info "Health check passed"
else
log_error "Health check failed"
exit 1
fi
# Test dashboard
if curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/dashboard | grep -q "200"; then
log_info "Dashboard is accessible"
else
log_warn "Dashboard may not be accessible (this is normal if not configured)"
fi
log_info "Installation verified successfully!"
}
# Print next steps
print_next_steps() {
cat << EOF
${GREEN}=== LLM Proxy Gateway Installation Complete ===${NC}
${YELLOW}Next steps:${NC}
1. ${GREEN}Configure API keys${NC}
Edit: $ENV_FILE
Add your API keys for the providers you want to use
2. ${GREEN}Configure authentication${NC}
Edit: $CONFIG_DIR/config.toml
Uncomment and set auth_tokens for client authentication
3. ${GREEN}Configure nginx${NC}
Edit: /etc/nginx/sites-available/$APP_NAME
Update domain name and SSL certificate paths
4. ${GREEN}Test the API${NC}
curl -X POST http://localhost:8080/v1/chat/completions \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer your-token" \\
-d '{
"model": "gpt-4o",
"messages": [{"role": "user", "content": "Hello!"}]
}'
5. ${GREEN}Access the dashboard${NC}
Open: http://your-server-ip:8080/dashboard
Or: https://your-domain.com/dashboard (if nginx configured)
${YELLOW}Useful commands:${NC}
systemctl status $APP_NAME # Check service status
journalctl -u $APP_NAME -f # View logs
systemctl restart $APP_NAME # Restart service
${YELLOW}Configuration files:${NC}
Service: $SERVICE_FILE
Config: $CONFIG_DIR/config.toml
Environment: $ENV_FILE
Database: $DATA_DIR/llm_proxy.db
Logs: $LOG_DIR/
${GREEN}For more information, see:${NC}
https://git.dustin.coffee/hobokenchicken/llm-proxy
$INSTALL_DIR/README.md
$INSTALL_DIR/deployment.md
EOF
}
# Main deployment function
deploy() {
log_info "Starting LLM Proxy Gateway deployment..."
check_root
install_dependencies
install_rust
setup_directories
build_application
create_configuration
create_systemd_service
initialize_database
start_service
verify_installation
print_next_steps
# Optional steps (uncomment if needed)
# setup_nginx_proxy
# setup_firewall
log_info "Deployment completed successfully!"
}
# Update function
update() {
log_info "Updating LLM Proxy Gateway..."
check_root
# Pull latest changes (while service keeps running)
cd "$INSTALL_DIR"
log_info "Pulling latest changes..."
git pull
# Build new binary (service stays up on the old binary)
log_info "Building release binary (service still running)..."
if ! cargo build --release; then
log_error "Build failed — service was NOT interrupted. Fix the error and try again."
exit 1
fi
# Verify binary exists
if [[ ! -f "target/release/$APP_NAME" ]]; then
log_error "Binary not found after build — aborting."
exit 1
fi
# Restart service to pick up new binary
log_info "Build succeeded. Restarting service..."
systemctl restart "$APP_NAME"
sleep 2
if systemctl is-active --quiet "$APP_NAME"; then
log_info "Update completed successfully!"
systemctl status "$APP_NAME" --no-pager
else
log_error "Service failed to start after update. Check logs:"
journalctl -u "$APP_NAME" -n 20 --no-pager
exit 1
fi
}
# Uninstall function
uninstall() {
log_info "Uninstalling LLM Proxy Gateway..."
check_root
# Stop and disable service
systemctl stop "$APP_NAME" 2>/dev/null || true
systemctl disable "$APP_NAME" 2>/dev/null || true
rm -f "$SERVICE_FILE"
systemctl daemon-reload
# Remove application files
rm -rf "$INSTALL_DIR"
rm -rf "$CONFIG_DIR"
# Keep data and logs (comment out to remove)
log_warn "Data directory $DATA_DIR and logs $LOG_DIR have been preserved"
log_warn "Remove manually if desired:"
log_warn " rm -rf $DATA_DIR $LOG_DIR"
# Remove user (optional)
read -p "Remove user $APP_USER? [y/N]: " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
userdel "$APP_USER" 2>/dev/null || true
groupdel "$APP_GROUP" 2>/dev/null || true
fi
log_info "Uninstallation completed!"
}
# Show usage
usage() {
cat << EOF
LLM Proxy Gateway Deployment Script
Usage: $0 [command]
Commands:
deploy - Install and configure LLM Proxy Gateway
update - Pull latest changes, rebuild, and restart
status - Show service status and health check
logs - Tail the service logs (Ctrl+C to stop)
uninstall - Remove LLM Proxy Gateway
help - Show this help message
Examples:
$0 deploy # Full installation
$0 update # Update to latest version
$0 status # Check if service is healthy
$0 logs # Follow live logs
EOF
}
# Status function
status() {
echo ""
log_info "Service status:"
systemctl status "$APP_NAME" --no-pager 2>/dev/null || log_warn "Service not found"
echo ""
# Health check
if curl -sf http://localhost:8080/health &>/dev/null; then
log_info "Health check: OK"
else
log_warn "Health check: FAILED (service may not be running or port 8080 not responding)"
fi
# Show current git commit
if [[ -d "$INSTALL_DIR/.git" ]]; then
echo ""
log_info "Installed version:"
git -C "$INSTALL_DIR" log -1 --format=" %h %s (%cr)" 2>/dev/null
fi
}
# Logs function
logs() {
log_info "Tailing $APP_NAME logs (Ctrl+C to stop)..."
journalctl -u "$APP_NAME" -f
}
# Parse command line arguments
case "${1:-}" in
deploy)
deploy
;;
update)
update
;;
status)
status
;;
logs)
logs
;;
uninstall)
uninstall
;;
help|--help|-h)
usage
;;
*)
usage
exit 1
;;
esac