From 1e9daa885889cb21eba52dcab2c1d41f3d7a34f2 Mon Sep 17 00:00:00 2001 From: DanielS Date: Wed, 11 Nov 2026 00:45:15 +0100 Subject: [PATCH] Add Windows test-server setup behind nginx - docker-compose.server.yml: app on APP_BIND for an existing nginx proxy, QLO_SSL passed to the container - docker/setup-testserver.ps1: one-shot setup for Docker Desktop/WSL (random .env secrets, CLI install that never reinstalls over an existing shop, German texts, optional demo content, hardening) - docker/harden-test-server.php: delete demo customer pub@qloapps.com, disable webservice API keys, set SSL mode, deny module log folders - docker/nginx-qloapps.conf: HTTPS + basic auth + X-Forwarded-Proto - apache: deny /modules/*/logs/ (qlocmconnector logs API secrets) - docker/TESTSERVER.md: Windows Server guide (German) Co-Authored-By: Claude Opus 5.5 --- .env.example | 9 +++ docker-compose.server.yml | 16 +++++ docker/TESTSERVER.md | 87 +++++++++++++++++++++++++ docker/apache-hardening.conf | 2 + docker/harden-test-server.php | 67 ++++++++++++++++++++ docker/nginx-qloapps.conf | 37 +++++++++++ docker/setup-testserver.ps1 | 115 ++++++++++++++++++++++++++++++++++ 7 files changed, 333 insertions(+) create mode 100644 docker-compose.server.yml create mode 100644 docker/TESTSERVER.md create mode 100644 docker/harden-test-server.php create mode 100644 docker/nginx-qloapps.conf create mode 100644 docker/setup-testserver.ps1 diff --git a/.env.example b/.env.example index 8941526f2..ff72190c9 100644 --- a/.env.example +++ b/.env.example @@ -8,3 +8,12 @@ DB_ROOT_PASSWORD=qlo_local_root # Local admin created by the CLI installer (test values only) ADMIN_EMAIL=admin@hotel.test ADMIN_PASSWORD=change-me + +# Test server only (docker-compose.server.yml) +# Public host name as nginx serves it, without https:// (QloApps stores it in the DB) +SHOP_DOMAIN=test.example.de +# Address:port the app listens on for nginx. Use the LAN IP of the Windows server +# if nginx runs on another machine, or 127.0.0.1:8080 if nginx runs on the same host. +APP_BIND=0.0.0.0:8080 +# 1 when nginx serves HTTPS +QLO_SSL=1 diff --git a/docker-compose.server.yml b/docker-compose.server.yml new file mode 100644 index 000000000..04d731297 --- /dev/null +++ b/docker-compose.server.yml @@ -0,0 +1,16 @@ +# Test-server overlay (Windows Server + Docker Desktop, or Docker in WSL). +# Your existing nginx reverse proxy terminates HTTPS and asks for a password; +# the app only listens on APP_BIND (default: all interfaces, port 8080). +# docker compose -f docker-compose.yml -f docker-compose.server.yml up -d --build +# See docker/TESTSERVER.md and docker/nginx-qloapps.conf. + +services: + app: + ports: !override + - "${APP_BIND:-0.0.0.0:8080}:80" + environment: + QLO_SSL: ${QLO_SSL:-1} + restart: unless-stopped + + db: + restart: unless-stopped diff --git a/docker/TESTSERVER.md b/docker/TESTSERVER.md new file mode 100644 index 000000000..75348990c --- /dev/null +++ b/docker/TESTSERVER.md @@ -0,0 +1,87 @@ +# Testserver auf Windows Server + +Shop mit Wasserrad-Design auf einem Windows Server, erreichbar über den vorhandenen nginx-Reverse-Proxy. +nginx übernimmt HTTPS und schützt die ganze Seite mit einem Passwort. + +``` +Browser ──HTTPS──> nginx (Zertifikat, Passwort) ──HTTP──> Windows Server :8080 ──> Docker: app (Apache/PHP) + db (MariaDB) +``` + +## 1. Docker auf dem Windows Server + +**Variante A: Docker Desktop** (bevorzugt) + +1. WSL 2 aktivieren (PowerShell als Administrator), danach neu starten: + ```powershell + wsl --install --no-distribution + ``` +2. Docker Desktop installieren, Einstellung „Use the WSL 2 based engine“ aktiv lassen. +3. Unter *Settings → General* „Start Docker Desktop when you sign in“ aktivieren. + +Hinweis: Docker Desktop ist offiziell für Windows 10/11 gedacht; auf Windows Server 2022/2025 läuft es mit WSL 2 in der Regel, +wird aber von Docker nicht unterstützt. Docker Desktop startet erst, wenn sich ein Benutzer anmeldet. +Ab 250 Mitarbeitenden oder 10 Mio. $ Umsatz braucht Docker Desktop eine kostenpflichtige Lizenz. + +**Variante B: Docker Engine direkt in WSL** (falls Docker Desktop nicht startet) + +```powershell +wsl --install -d Ubuntu-24.04 +``` +In Ubuntu: `curl -fsSL https://get.docker.com | sh`, dann die Schritte unten in Ubuntu mit `docker compose …` ausführen +(statt des PowerShell-Skripts die Befehle aus `docker-compose.yml` nutzen). + +## 2. Code holen + +```powershell +git clone -b hotel/no-telemetry-docker https://gitea.hephex.de/Daniel/hotel.git C:\hotel +cd C:\hotel +``` + +## 3. Einrichten (ein Befehl) + +```powershell +powershell -ExecutionPolicy Bypass -File docker\setup-testserver.ps1 -ShopDomain test.example.de +``` + +Das Skript +- legt `.env` mit zufälligen Passwörtern an (Admin-Login steht dort: `ADMIN_EMAIL` / `ADMIN_PASSWORD`), +- baut und startet die Container, +- installiert QloApps auf Deutsch (überspringt das, wenn schon installiert, also kein versehentliches Löschen), +- ergänzt fehlende deutsche Texte, lädt die Wasserrad-Demo (`-NoDemo` zum Weglassen), +- räumt auf: Demo-Kunde gelöscht, API-Schlüssel abgeschaltet, Log-Ordner gesperrt. + +Ohne Zertifikat im LAN: `-NoSsl`. + +Port und Adresse der App stehen in `.env` unter `APP_BIND` (Standard `0.0.0.0:8080`). +Läuft nginx auf demselben Server: `APP_BIND=127.0.0.1:8080`. Sonst die Windows-Firewall für Port 8080 nur für die IP des nginx-Servers öffnen: + +```powershell +New-NetFirewallRule -DisplayName "QloApps from nginx" -Direction Inbound -Protocol TCP -LocalPort 8080 -RemoteAddress -Action Allow +``` + +## 4. nginx + +Vorlage: [`nginx-qloapps.conf`](nginx-qloapps.conf). Domain, Zertifikat und `APP_HOST` anpassen, Passwortdatei anlegen: + +```bash +htpasswd -c /etc/nginx/.htpasswd-hotel tester +nginx -t && systemctl reload nginx +``` + +Wichtig ist `proxy_set_header X-Forwarded-Proto $scheme;` – daran erkennt QloApps HTTPS. Ohne die Zeile gibt es Umleitungsschleifen. + +## Betrieb + +| Aufgabe | Befehl (im Repo-Ordner) | +|---|---| +| Status | `docker compose -f docker-compose.yml -f docker-compose.server.yml ps` | +| Logs | `docker compose -f docker-compose.yml -f docker-compose.server.yml logs -f app` | +| Update | `git pull`, dann das Setup-Skript erneut ausführen | +| Stoppen | `docker compose -f docker-compose.yml -f docker-compose.server.yml down` (Daten bleiben im Volume `db_data`) | +| Backup DB | `docker compose exec db mariadb-dump -uroot -p qloapps > backup.sql` | + +## Vor echtem Betrieb + +Der Testserver ist für Design- und Ablauftests gedacht. Für echte Buchungen fehlen noch: eigene Rechtstexte +(Impressum, Datenschutz, AGB), E-Mail-Versand (SMTP), Zahlungsart, regelmäßige Backups und QloApps-Updates +(der automatische Update-Hinweis ist abgeschaltet). diff --git a/docker/apache-hardening.conf b/docker/apache-hardening.conf index 7ed04551c..d526279c7 100644 --- a/docker/apache-hardening.conf +++ b/docker/apache-hardening.conf @@ -1,6 +1,8 @@ # The repo is bind-mounted as web root: never serve dotfiles (.env, .git, ...) or docker files. RedirectMatch 404 "/\.(?!well-known/)" RedirectMatch 404 "^/(docker/|docker-compose\.yml)" +# Module log folders (qlocmconnector logs full API requests incl. client_secret) +RedirectMatch 404 "^/modules/[^/]+/logs/" # Installation is done via CLI (see docker-compose.yml); a web re-install would wipe the database. RedirectMatch 404 "^/install-dev" ServerTokens Prod diff --git a/docker/harden-test-server.php b/docker/harden-test-server.php new file mode 100644 index 000000000..e624872ce --- /dev/null +++ b/docker/harden-test-server.php @@ -0,0 +1,67 @@ +executeS('SELECT id_customer FROM '._DB_PREFIX_."customer WHERE email = 'pub@qloapps.com'"); +foreach ($ids as $row) { + $c = new Customer((int)$row['id_customer']); + $c->delete(); +} +$ok(count($ids) ? 'pub@qloapps.com deleted' : 'no demo customer present'); + +echo "Webservice API\n"; +Configuration::updateValue('PS_WEBSERVICE', 0); +Db::getInstance()->update('webservice_account', array('active' => 0)); +$ok('API disabled, all keys deactivated'); + +echo "Developer mode\n"; +if (_PS_MODE_DEV_) { + $warn('_PS_MODE_DEV_ is on: set it to false in config/defines.inc.php'); +} else { + $ok('off'); +} + +echo "SSL\n"; +$ssl = getenv('QLO_SSL') === '1'; +Configuration::updateValue('PS_SSL_ENABLED', (int)$ssl); +Configuration::updateValue('PS_SSL_ENABLED_EVERYWHERE', (int)$ssl); +$ok($ssl ? 'HTTPS everywhere' : 'HTTP (LAN test, no certificate)'); + +echo "Module log folders\n"; +$deny = "\n Require all denied\n\n"; +foreach (glob(_PS_MODULE_DIR_.'*', GLOB_ONLYDIR) as $dir) { + $logs = $dir.'/logs'; + if (basename($dir) === 'qlocmconnector' && !is_dir($logs)) { + @mkdir($logs, 0755, true); + } + if (is_dir($logs) && !is_file($logs.'/.htaccess')) { + file_put_contents($logs.'/.htaccess', $deny); + $ok(basename($dir).'/logs protected'); + } +} + +echo "Admin accounts\n"; +foreach (Db::getInstance()->executeS('SELECT email, active FROM '._DB_PREFIX_.'employee') as $e) { + echo ' - '.$e['email'].($e['active'] ? '' : ' (inactive)').PHP_EOL; +} + +Tools::clearSmartyCache(); +echo "Done.\n"; diff --git a/docker/nginx-qloapps.conf b/docker/nginx-qloapps.conf new file mode 100644 index 000000000..e086d77d6 --- /dev/null +++ b/docker/nginx-qloapps.conf @@ -0,0 +1,37 @@ +# nginx server block for the QloApps test server (HTTPS + password for the whole site). +# Replace test.example.de, the certificate paths and APP_HOST:8080 (the Windows server running Docker). +# Password file: htpasswd -c /etc/nginx/.htpasswd-hotel +# (or: printf "user:$(openssl passwd -apr1)\n" > /etc/nginx/.htpasswd-hotel) + +server { + listen 80; + server_name test.example.de; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + http2 on; + server_name test.example.de; + + ssl_certificate /etc/letsencrypt/live/test.example.de/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/test.example.de/privkey.pem; + + # whole site (shop + /admin-dev) behind a password while testing + auth_basic "Hotel Test"; + auth_basic_user_file /etc/nginx/.htpasswd-hotel; + + client_max_body_size 20m; # image uploads in the back office + add_header X-Robots-Tag "noindex, nofollow" always; + add_header X-Content-Type-Options nosniff always; + add_header Referrer-Policy strict-origin-when-cross-origin always; + + location / { + proxy_pass http://APP_HOST:8080; + 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; # QloApps uses this to detect HTTPS + proxy_read_timeout 300s; # installer / image regeneration + } +} diff --git a/docker/setup-testserver.ps1 b/docker/setup-testserver.ps1 new file mode 100644 index 000000000..250574961 --- /dev/null +++ b/docker/setup-testserver.ps1 @@ -0,0 +1,115 @@ +<# +.SYNOPSIS + Sets up the QloApps test server on Windows (Docker Desktop or Docker in WSL). + +.DESCRIPTION + Run from the repository root in PowerShell: + powershell -ExecutionPolicy Bypass -File docker\setup-testserver.ps1 -ShopDomain test.example.de + + - creates .env with random passwords (only if .env does not exist yet) + - builds and starts the containers (docker-compose.yml + docker-compose.server.yml) + - installs QloApps in German via CLI (skipped if config/settings.inc.php exists: no accidental wipe) + - fills German shop texts, optionally loads the Wasserrad demo content, then hardens the install + Re-running is safe: it never reinstalls over an existing shop. + +.PARAMETER ShopDomain + Host name nginx serves the shop under, without https:// (e.g. test.example.de). +.PARAMETER NoSsl + nginx serves plain HTTP (LAN test without certificate). +.PARAMETER NoDemo + Skip the "Zum alten Wasserrad" demo content. +#> +param( + [Parameter(Mandatory = $true)][string]$ShopDomain, + [switch]$NoSsl, + [switch]$NoDemo +) + +$ErrorActionPreference = 'Stop' +$root = Split-Path -Parent $PSScriptRoot +Set-Location $root +$compose = @('compose', '-f', 'docker-compose.yml', '-f', 'docker-compose.server.yml') + +function Step($text) { Write-Host "`n==> $text" -ForegroundColor Cyan } +function New-Secret([int]$bytes = 18) { + $b = New-Object byte[] $bytes + [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($b) + return ([Convert]::ToBase64String($b) -replace '[^A-Za-z0-9]', '') +} +function Invoke-Docker { + & docker @args + if ($LASTEXITCODE -ne 0) { throw "docker $($args -join ' ') failed ($LASTEXITCODE)" } +} +function Read-EnvFile { + $h = @{} + foreach ($line in Get-Content .env) { + if ($line -match '^\s*([A-Z_]+)=(.*)$') { $h[$Matches[1]] = $Matches[2].Trim() } + } + return $h +} + +Step 'Docker' +docker version --format '{{.Server.Version}}' | Out-Null +if ($LASTEXITCODE -ne 0) { throw 'Docker is not running (start Docker Desktop or the Docker engine in WSL).' } + +Step '.env' +$ssl = if ($NoSsl) { '0' } else { '1' } +if (-not (Test-Path .env)) { + $content = (Get-Content .env.example -Raw) + $content = $content -replace '(?m)^DB_PASSWORD=.*$', "DB_PASSWORD=$(New-Secret)" + $content = $content -replace '(?m)^DB_ROOT_PASSWORD=.*$', "DB_ROOT_PASSWORD=$(New-Secret)" + $content = $content -replace '(?m)^ADMIN_EMAIL=.*$', "ADMIN_EMAIL=admin@$ShopDomain" + $content = $content -replace '(?m)^ADMIN_PASSWORD=.*$', "ADMIN_PASSWORD=$(New-Secret 15)" + $content = $content -replace '(?m)^SHOP_DOMAIN=.*$', "SHOP_DOMAIN=$ShopDomain" + $content = $content -replace '(?m)^QLO_SSL=.*$', "QLO_SSL=$ssl" + [System.IO.File]::WriteAllText((Join-Path $root '.env'), $content) + Write-Host '.env created with random passwords (keep it private, it is git-ignored).' +} else { + Write-Host '.env exists, keeping its values.' + # older .env files lack the server keys: add them from the parameters + $existing = Get-Content .env -Raw + $add = @{ 'SHOP_DOMAIN' = $ShopDomain; 'APP_BIND' = '0.0.0.0:8080'; 'QLO_SSL' = $ssl } + foreach ($k in $add.Keys) { + if ($existing -notmatch "(?m)^$k=") { + Add-Content -Path .env -Value "$k=$($add[$k])" + Write-Host " added $k=$($add[$k])" + } + } +} +$envVars = Read-EnvFile + +Step 'Build and start containers' +Invoke-Docker @compose up -d --build + +Step 'File permissions for www-data' +Invoke-Docker @compose exec -T app sh -c 'cd /var/www/html && chown -R www-data:www-data img mails modules translations cache config upload download themes override log' + +if (Test-Path config/settings.inc.php) { + Step 'QloApps already installed (config/settings.inc.php exists) - skipping install' +} else { + Step 'Install QloApps (German, newsletter off)' + Invoke-Docker @compose exec -T -u www-data app php install-dev/index_cli.php ` + --language=de --country=de --timezone=Europe/Berlin ` + "--domain=$($envVars['SHOP_DOMAIN'])" "--ssl=$($envVars['QLO_SSL'])" ` + --db_server=db "--db_user=$($envVars['DB_USER'])" "--db_password=$($envVars['DB_PASSWORD'])" "--db_name=$($envVars['DB_NAME'])" ` + "--name=Zum alten Wasserrad" --firstname=Admin --lastname=Hotel ` + "--email=$($envVars['ADMIN_EMAIL'])" "--password=$($envVars['ADMIN_PASSWORD'])" ` + --newsletter=0 +} + +Step 'German shop texts' +Invoke-Docker @compose exec -T -u www-data app php docker/apply-translations.php de + +if (-not $NoDemo) { + Step 'Wasserrad demo content' + Invoke-Docker @compose exec -T -u www-data app php docker/seed-wasserrad-demo.php +} + +Step 'Harden' +Invoke-Docker @compose exec -T -u www-data app php docker/harden-test-server.php + +$scheme = if ($envVars['QLO_SSL'] -eq '1') { 'https' } else { 'http' } +Write-Host "`nDone." -ForegroundColor Green +Write-Host "Shop: $scheme`://$($envVars['SHOP_DOMAIN'])/" +Write-Host "Admin: $scheme`://$($envVars['SHOP_DOMAIN'])/admin-dev/ (login: ADMIN_EMAIL / ADMIN_PASSWORD in .env)" +Write-Host "App listens on $($envVars['APP_BIND']) - point nginx there (docker/nginx-qloapps.conf)."