#!/bin/bash # ============================================================================= # TimeClock v3 - Deployment Script für Ubuntu 22.04 # ============================================================================= # Dieses Script automatisiert das Deployment auf dem Produktionsserver # # Verwendung: # ./deploy.sh [OPTION] # # Optionen: # install - Erste Installation (inkl. System-Setup) # update - Update einer bestehenden Installation # rollback - Rollback zur vorherigen Version # backup - Erstelle Backup der Datenbank # status - Zeige Status der Services # logs - Zeige Logs # help - Zeige diese Hilfe # # ============================================================================= set -e # Bei Fehler abbrechen # Farben für Output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Konfiguration PROJECT_NAME="TimeClock" PROJECT_DIR="/var/www/timeclock" BACKEND_DIR="$PROJECT_DIR/backend" FRONTEND_DIR="$PROJECT_DIR/frontend" DOMAIN="stechuhr3.tsschulz.de" BACKUP_DIR="/var/backups/timeclock" LOG_DIR="/var/log/timeclock" # Service-Name (PM2 oder systemd) USE_PM2=true # true für PM2, false für systemd SERVICE_NAME="timeclock-backend" # Webserver (nginx oder apache2) WEBSERVER="apache2" # "nginx" oder "apache2" # ============================================================================= # Hilfsfunktionen # ============================================================================= print_info() { echo -e "${BLUE}ℹ️ $1${NC}" } print_success() { echo -e "${GREEN}✅ $1${NC}" } print_warning() { echo -e "${YELLOW}⚠️ $1${NC}" } print_error() { echo -e "${RED}❌ $1${NC}" } print_header() { echo "" echo -e "${BLUE}═══════════════════════════════════════════════════════════════${NC}" echo -e "${BLUE} $1${NC}" echo -e "${BLUE}═══════════════════════════════════════════════════════════════${NC}" echo "" } check_root() { if [ "$EUID" -eq 0 ]; then print_error "Bitte führe dieses Script NICHT als root aus!" print_info "Verwende stattdessen sudo für einzelne Befehle." exit 1 fi } check_command() { if ! command -v $1 &> /dev/null; then print_error "$1 ist nicht installiert!" return 1 fi return 0 } # ============================================================================= # Installation - Erste Einrichtung # ============================================================================= install_dependencies() { print_header "Installiere System-Abhängigkeiten" print_info "Aktualisiere Paketquellen..." sudo apt update print_info "Installiere Node.js 20.x..." if ! check_command node; then curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt install -y nodejs else print_success "Node.js ist bereits installiert ($(node -v))" fi if [ "$WEBSERVER" = "nginx" ]; then print_info "Installiere Nginx..." if ! check_command nginx; then sudo apt install -y nginx else print_success "Nginx ist bereits installiert" fi else print_info "Installiere Apache2..." if ! check_command apache2; then sudo apt install -y apache2 else print_success "Apache2 ist bereits installiert" fi print_info "Aktiviere Apache2-Module..." sudo a2enmod proxy proxy_http ssl rewrite headers deflate expires fi print_info "Installiere Certbot..." if ! check_command certbot; then if [ "$WEBSERVER" = "nginx" ]; then sudo apt install -y certbot python3-certbot-nginx else sudo apt install -y certbot python3-certbot-apache fi else print_success "Certbot ist bereits installiert" fi if [ "$USE_PM2" = true ]; then print_info "Installiere PM2..." if ! check_command pm2; then sudo npm install -g pm2 else print_success "PM2 ist bereits installiert" fi fi print_success "Alle Abhängigkeiten installiert!" } setup_directories() { print_header "Erstelle Verzeichnisse" print_info "Erstelle Projekt-Verzeichnis..." sudo mkdir -p $PROJECT_DIR sudo chown -R $USER:$USER $PROJECT_DIR print_info "Erstelle Log-Verzeichnis..." sudo mkdir -p $LOG_DIR sudo chown -R www-data:www-data $LOG_DIR print_info "Erstelle Backup-Verzeichnis..." sudo mkdir -p $BACKUP_DIR sudo chown -R $USER:$USER $BACKUP_DIR print_success "Verzeichnisse erstellt!" } copy_project_files() { print_header "Kopiere Projekt-Dateien" CURRENT_DIR=$(pwd) print_info "Quell-Verzeichnis: $CURRENT_DIR" print_info "Ziel-Verzeichnis: $PROJECT_DIR" # Erstelle Ziel-Verzeichnisse falls nicht vorhanden mkdir -p "$BACKEND_DIR" mkdir -p "$FRONTEND_DIR" print_info "Kopiere Backend..." rsync -av --exclude 'node_modules' --exclude '.env' --delete "$CURRENT_DIR/backend/" "$BACKEND_DIR/" print_info "Kopiere Frontend..." rsync -av --exclude 'node_modules' --exclude 'dist' --exclude '.env*' --delete "$CURRENT_DIR/frontend/" "$FRONTEND_DIR/" print_info "Kopiere Root-Dateien..." rsync -av --exclude 'node_modules' --exclude '.git' --exclude 'frontend' --exclude 'backend' \ "$CURRENT_DIR"/*.{sh,conf,md,js,json,service} "$PROJECT_DIR/" 2>/dev/null || true print_success "Projekt-Dateien kopiert!" } setup_backend() { print_header "Setup Backend" cd $BACKEND_DIR print_info "Installiere Backend-Dependencies..." # npm Konfiguration verhärten (weniger Hänger/Output) npm config set fund false >/dev/null 2>&1 || true npm config set audit false >/dev/null 2>&1 || true npm config set progress false >/dev/null 2>&1 || true npm config set loglevel warn >/dev/null 2>&1 || true # Backend braucht alle Dependencies (auch dev für Build-Tools) timeout 600 bash -lc "npm ci --no-audit --no-fund --silent --loglevel=warn --no-progress" || { print_warning "npm ci ist fehlgeschlagen oder hat zu lange gedauert. Versuche fallback ohne timeout..." npm ci --no-audit --no-fund --silent --loglevel=warn --no-progress || { print_error "npm ci (Backend) fehlgeschlagen"; exit 1; } } if [ ! -f .env ]; then print_warning ".env Datei nicht gefunden!" if [ -f env.production.template ]; then print_info "Kopiere Template..." cp env.production.template .env print_warning "WICHTIG: Bitte bearbeite $BACKEND_DIR/.env und passe die Werte an!" read -p "Drücke Enter wenn du fertig bist..." else print_error "Auch env.production.template nicht gefunden!" exit 1 fi else print_success ".env bereits vorhanden" fi print_success "Backend Setup abgeschlossen!" } setup_frontend() { print_header "Setup Frontend" cd $FRONTEND_DIR print_info "Installiere Frontend-Dependencies..." # npm Konfiguration verhärten npm config set fund false >/dev/null 2>&1 || true npm config set audit false >/dev/null 2>&1 || true npm config set progress false >/dev/null 2>&1 || true npm config set loglevel warn >/dev/null 2>&1 || true # Frontend braucht dev-Dependencies für den Build (vite, etc.) timeout 600 bash -lc "npm ci --no-audit --no-fund --silent --loglevel=warn --no-progress" || { print_warning "npm ci ist fehlgeschlagen oder hat zu lange gedauert. Versuche fallback ohne timeout..." npm ci --no-audit --no-fund --silent --loglevel=warn --no-progress || { print_error "npm ci (Frontend) fehlgeschlagen"; exit 1; } } # .env.production erstellen falls nicht vorhanden if [ ! -f ".env.production" ]; then print_info "Erstelle .env.production..." cat > .env.production << 'EOF' # TimeClock v3 - Frontend Production Environment # API Base URL (relativ, da Apache als Proxy dient) VITE_API_URL=/api EOF print_success ".env.production erstellt" else print_success ".env.production bereits vorhanden" fi # .env.development erstellen falls nicht vorhanden (für lokale Entwicklung) if [ ! -f ".env.development" ]; then print_info "Erstelle .env.development..." cat > .env.development << 'EOF' # TimeClock v3 - Frontend Development Environment VITE_API_URL=http://localhost:3010/api EOF print_success ".env.development erstellt" fi # Prüfe ob api.js existiert if [ ! -f "src/config/api.js" ]; then print_warning "src/config/api.js fehlt! Bitte git pull ausführen." exit 1 fi # Alte dist/ löschen für sauberen Build print_info "Lösche alten Build..." rm -rf dist/ print_info "Erstelle Produktions-Build..." npm run build if [ ! -d "dist" ]; then print_error "Build fehlgeschlagen - dist/ Verzeichnis nicht gefunden!" exit 1 fi # Prüfe ob localhost:3010 noch im Build ist if grep -r "localhost:3010" dist/ > /dev/null 2>&1; then print_warning "⚠️ localhost:3010 noch im Build gefunden - eventuell falsches .env verwendet" print_info "Prüfe .env.production: $(cat .env.production 2>/dev/null || echo 'nicht gefunden')" else print_success "✅ Build verwendet korrekte API-URL" fi print_success "Frontend Build erstellt!" } setup_webserver() { if [ "$WEBSERVER" = "nginx" ]; then print_header "Setup Nginx" WEBSERVER_CONF="/etc/nginx/sites-available/$DOMAIN" if [ ! -f "$WEBSERVER_CONF" ]; then print_info "Kopiere Nginx-Konfiguration..." sudo cp "$PROJECT_DIR/nginx.conf" "$WEBSERVER_CONF" print_info "Aktiviere Site..." sudo ln -sf "$WEBSERVER_CONF" "/etc/nginx/sites-enabled/$DOMAIN" print_info "Teste Nginx-Konfiguration..." sudo nginx -t print_info "Lade Nginx neu..." sudo systemctl reload nginx print_success "Nginx konfiguriert!" else print_warning "Nginx-Konfiguration existiert bereits" read -p "Überschreiben? (y/N): " -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]]; then sudo cp "$PROJECT_DIR/nginx.conf" "$WEBSERVER_CONF" sudo nginx -t && sudo systemctl reload nginx print_success "Nginx-Konfiguration aktualisiert!" fi fi else print_header "Setup Apache2" WEBSERVER_CONF="/etc/apache2/sites-available/$DOMAIN.conf" if [ ! -f "$WEBSERVER_CONF" ]; then print_info "Kopiere Apache2-Konfiguration..." sudo cp "$PROJECT_DIR/apache2.conf" "$WEBSERVER_CONF" print_info "Aktiviere Site..." sudo a2ensite "$DOMAIN" # Deaktiviere Default-Site (optional) if [ -f "/etc/apache2/sites-enabled/000-default.conf" ]; then print_info "Deaktiviere Default-Site..." sudo a2dissite 000-default fi print_info "Teste Apache2-Konfiguration..." sudo apache2ctl configtest print_info "Lade Apache2 neu..." sudo systemctl reload apache2 print_success "Apache2 konfiguriert!" else print_warning "Apache2-Konfiguration existiert bereits" read -p "Überschreiben? (y/N): " -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]]; then sudo cp "$PROJECT_DIR/apache2.conf" "$WEBSERVER_CONF" sudo apache2ctl configtest && sudo systemctl reload apache2 print_success "Apache2-Konfiguration aktualisiert!" fi fi fi } setup_ssl() { print_header "Setup SSL-Zertifikat" print_info "Prüfe ob Zertifikat bereits existiert..." if [ -d "/etc/letsencrypt/live/$DOMAIN" ]; then print_success "SSL-Zertifikat bereits vorhanden" return fi print_warning "Stelle sicher, dass $DOMAIN auf diesen Server zeigt!" read -p "Fortfahren? (y/N): " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then print_info "SSL-Setup übersprungen" return fi print_info "Erstelle SSL-Zertifikat mit Let's Encrypt..." if [ "$WEBSERVER" = "nginx" ]; then sudo certbot --nginx -d $DOMAIN else sudo certbot --apache -d $DOMAIN fi print_success "SSL-Zertifikat erstellt!" } setup_backend_service() { print_header "Setup Backend-Service" if [ "$USE_PM2" = true ]; then print_info "Starte Backend mit PM2..." cd $BACKEND_DIR pm2 delete $SERVICE_NAME 2>/dev/null || true pm2 start src/index.js --name $SERVICE_NAME --env production pm2 save print_info "Richte PM2 Auto-Start ein..." sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u $USER --hp $HOME print_success "Backend läuft mit PM2!" else print_info "Installiere systemd-Service..." sudo cp "$PROJECT_DIR/timeclock.service" /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable $SERVICE_NAME sudo systemctl start $SERVICE_NAME print_success "Backend läuft mit systemd!" fi } setup_firewall() { print_header "Setup Firewall" if ! check_command ufw; then print_warning "UFW nicht installiert, überspringe Firewall-Setup" return fi print_info "Konfiguriere UFW..." sudo ufw allow ssh if [ "$WEBSERVER" = "nginx" ]; then sudo ufw allow 'Nginx Full' else sudo ufw allow 'Apache Full' fi if ! sudo ufw status | grep -q "Status: active"; then print_warning "UFW ist nicht aktiv" read -p "UFW aktivieren? (y/N): " -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]]; then sudo ufw --force enable print_success "UFW aktiviert!" fi else print_success "UFW bereits konfiguriert" fi } # ============================================================================= # Update - Aktualisierung einer bestehenden Installation # ============================================================================= do_update() { print_header "Update $PROJECT_NAME" CURRENT_DIR=$(pwd) # Prüfe ob wir im Quell-Verzeichnis sind if [ ! -f "deploy.sh" ] || [ ! -d "backend" ] || [ ! -d "frontend" ]; then print_error "Führe das Script aus dem Projekt-Verzeichnis aus!" print_info "Aktuell in: $CURRENT_DIR" print_info "Erwarte: backend/, frontend/ und deploy.sh im aktuellen Verzeichnis" exit 1 fi print_info "Quell-Verzeichnis: $CURRENT_DIR" print_info "Ziel-Verzeichnis: $PROJECT_DIR" # Backup erstellen do_backup # Kopiere aktualisierte Dateien print_header "Kopiere aktualisierte Dateien" print_info "Kopiere Backend..." rsync -av --exclude 'node_modules' --exclude '.env' --delete "$CURRENT_DIR/backend/" "$BACKEND_DIR/" print_info "Kopiere Frontend..." rsync -av --exclude 'node_modules' --exclude 'dist' --exclude '.env*' --delete "$CURRENT_DIR/frontend/" "$FRONTEND_DIR/" print_info "Kopiere Konfigurations-Dateien..." rsync -av "$CURRENT_DIR"/*.{sh,conf,md,service} "$PROJECT_DIR/" 2>/dev/null || true # Backend aktualisieren print_info "Aktualisiere Backend Dependencies..." cd $BACKEND_DIR npm config set fund false >/dev/null 2>&1 || true npm config set audit false >/dev/null 2>&1 || true npm config set progress false >/dev/null 2>&1 || true npm config set loglevel warn >/dev/null 2>&1 || true # Backend braucht alle Dependencies (auch dev für Build-Tools) timeout 600 bash -lc "npm ci --no-audit --no-fund --silent --loglevel=warn --no-progress" || { print_warning "npm ci ist fehlgeschlagen oder hat zu lange gedauert. Versuche fallback ohne timeout..." npm ci --no-audit --no-fund --silent --loglevel=warn --no-progress || { print_error "npm ci (Backend Update) fehlgeschlagen"; exit 1; } } # Frontend aktualisieren print_info "Aktualisiere Frontend..." cd $FRONTEND_DIR # .env.production prüfen/erstellen if [ ! -f ".env.production" ]; then print_info "Erstelle .env.production..." cat > .env.production << 'EOF' # TimeClock v3 - Frontend Production Environment VITE_API_URL=/api EOF fi npm config set fund false >/dev/null 2>&1 || true npm config set audit false >/dev/null 2>&1 || true npm config set progress false >/dev/null 2>&1 || true npm config set loglevel warn >/dev/null 2>&1 || true # Frontend braucht dev-Dependencies für den Build (vite, etc.) timeout 600 bash -lc "npm ci --no-audit --no-fund --silent --loglevel=warn --no-progress" || { print_warning "npm ci ist fehlgeschlagen oder hat zu lange gedauert. Versuche fallback ohne timeout..." npm ci --no-audit --no-fund --silent --loglevel=warn --no-progress || { print_error "npm ci (Frontend Update) fehlgeschlagen"; exit 1; } } # Sauberer Build rm -rf dist/ npm run build # Prüfe Build if grep -r "localhost:3010" dist/ > /dev/null 2>&1; then print_warning "⚠️ localhost:3010 noch im Build - bitte prüfen" fi # Service neu starten restart_backend # Apache Cache leeren if [ "$WEBSERVER" = "apache2" ]; then sudo systemctl reload apache2 else sudo systemctl reload nginx fi print_success "Update abgeschlossen!" } # ============================================================================= # Backup & Rollback # ============================================================================= do_backup() { print_header "Erstelle Backup" TIMESTAMP=$(date +%Y%m%d_%H%M%S) print_info "Erstelle Datenbank-Backup..." # Lese DB-Credentials aus .env DB_NAME=$(grep DB_NAME $BACKEND_DIR/.env | cut -d '=' -f2) DB_USER=$(grep DB_USER $BACKEND_DIR/.env | cut -d '=' -f2) DB_PASSWORD=$(grep DB_PASSWORD $BACKEND_DIR/.env | cut -d '=' -f2) # Verwende --no-tablespaces um PROCESS-Privileg zu vermeiden mysqldump --no-tablespaces -u $DB_USER -p"$DB_PASSWORD" $DB_NAME | gzip > "$BACKUP_DIR/${PROJECT_NAME,,}_$TIMESTAMP.sql.gz" || { print_warning "mysqldump mit --no-tablespaces fehlgeschlagen. Erzeuge nur Code-Backup." } print_info "Erstelle Code-Backup..." tar -czf "$BACKUP_DIR/${PROJECT_NAME,,}_code_$TIMESTAMP.tar.gz" -C $(dirname $PROJECT_DIR) $(basename $PROJECT_DIR) # Alte Backups löschen (älter als 30 Tage) find $BACKUP_DIR -name "*.sql.gz" -mtime +30 -delete find $BACKUP_DIR -name "*.tar.gz" -mtime +30 -delete print_success "Backup erstellt: $TIMESTAMP" } do_rollback() { print_header "Rollback zu vorheriger Version" print_warning "Rollback wird die letzte Code-Version wiederherstellen" read -p "Fortfahren? (y/N): " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then print_info "Rollback abgebrochen" return fi # Finde letztes Code-Backup LAST_BACKUP=$(ls -t $BACKUP_DIR/${PROJECT_NAME,,}_code_*.tar.gz 2>/dev/null | head -n1) if [ -z "$LAST_BACKUP" ]; then print_error "Kein Backup gefunden!" exit 1 fi print_info "Stelle wieder her: $LAST_BACKUP" # Aktuellen Code sichern mv $PROJECT_DIR "${PROJECT_DIR}_rollback_backup_$(date +%Y%m%d_%H%M%S)" # Backup wiederherstellen tar -xzf "$LAST_BACKUP" -C $(dirname $PROJECT_DIR) # Services neu starten restart_backend print_success "Rollback abgeschlossen!" } # ============================================================================= # Service Management # ============================================================================= restart_backend() { print_info "Starte Backend neu..." if [ "$USE_PM2" = true ]; then pm2 restart $SERVICE_NAME else sudo systemctl restart $SERVICE_NAME fi print_success "Backend neu gestartet!" } show_status() { print_header "Service Status" if [ "$USE_PM2" = true ]; then pm2 status $SERVICE_NAME echo "" pm2 info $SERVICE_NAME else sudo systemctl status $SERVICE_NAME fi echo "" if [ "$WEBSERVER" = "nginx" ]; then print_info "Nginx Status:" sudo systemctl status nginx --no-pager | head -n 10 else print_info "Apache2 Status:" sudo systemctl status apache2 --no-pager | head -n 10 fi echo "" print_info "SSL-Zertifikat:" sudo certbot certificates | grep -A 5 $DOMAIN || print_warning "Kein Zertifikat gefunden" } show_logs() { print_header "Logs" echo "1) Backend-Logs" echo "2) Webserver Access-Logs" echo "3) Webserver Error-Logs" echo "4) Alle Logs (follow)" read -p "Auswahl (1-4): " choice if [ "$WEBSERVER" = "nginx" ]; then ACCESS_LOG="/var/log/nginx/stechuhr3.access.log" ERROR_LOG="/var/log/nginx/stechuhr3.error.log" else ACCESS_LOG="/var/log/apache2/stechuhr3-access.log" ERROR_LOG="/var/log/apache2/stechuhr3-error.log" fi case $choice in 1) if [ "$USE_PM2" = true ]; then pm2 logs $SERVICE_NAME else sudo journalctl -u $SERVICE_NAME -f fi ;; 2) sudo tail -f "$ACCESS_LOG" ;; 3) sudo tail -f "$ERROR_LOG" ;; 4) if [ "$USE_PM2" = true ]; then pm2 logs $SERVICE_NAME & PM2_PID=$! else sudo journalctl -u $SERVICE_NAME -f & JOURNAL_PID=$! fi if [ "$WEBSERVER" = "nginx" ]; then sudo tail -f /var/log/nginx/stechuhr3.*.log else sudo tail -f /var/log/apache2/stechuhr3-*.log fi ;; *) print_error "Ungültige Auswahl" ;; esac } # ============================================================================= # Vollständige Installation # ============================================================================= do_install() { print_header "$PROJECT_NAME - Vollständige Installation" check_root print_warning "Diese Installation wird folgendes tun:" echo " - System-Abhängigkeiten installieren" echo " - Projekt nach $PROJECT_DIR kopieren" echo " - Backend und Frontend einrichten" echo " - Nginx konfigurieren" echo " - SSL-Zertifikat erstellen" echo " - Backend-Service starten" echo " - Firewall konfigurieren" echo "" read -p "Fortfahren? (y/N): " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then print_info "Installation abgebrochen" exit 0 fi install_dependencies setup_directories copy_project_files setup_backend setup_frontend setup_webserver setup_ssl setup_backend_service setup_firewall print_header "Installation abgeschlossen! 🎉" print_success "Deine TimeClock-App läuft jetzt auf https://$DOMAIN" echo "" print_info "Nützliche Befehle:" echo " ./deploy.sh status - Zeige Service-Status" echo " ./deploy.sh logs - Zeige Logs" echo " ./deploy.sh update - Update durchführen" echo " ./deploy.sh backup - Backup erstellen" echo "" } # ============================================================================= # Hauptprogramm # ============================================================================= show_help() { cat << EOF $PROJECT_NAME v3 - Deployment Script Verwendung: $0 [OPTION] Optionen: install Erste Installation (inkl. System-Setup) update Update einer bestehenden Installation rollback Rollback zur vorherigen Version backup Erstelle Backup der Datenbank status Zeige Status der Services logs Zeige Logs help Zeige diese Hilfe Beispiele: $0 install # Erste Installation $0 update # Update durchführen $0 backup # Backup erstellen $0 status # Status anzeigen EOF } # Hauptlogik case "${1:-help}" in install) do_install ;; update) do_update ;; rollback) do_rollback ;; backup) do_backup ;; status) show_status ;; logs) show_logs ;; help|--help|-h) show_help ;; *) print_error "Unbekannte Option: $1" echo "" show_help exit 1 ;; esac exit 0