From a61feb1befd8531abdcd2d6b16a8b2ac886912b0 Mon Sep 17 00:00:00 2001 From: boban Date: Fri, 19 Jun 2026 20:57:42 +0200 Subject: [PATCH] fix(deploy): apply Caddyfile changes on update (dir mount + recreate) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A single-file bind mount of the Caddyfile kept the original inode, so a git pull that replaces the file never reached a running caddy, and up -d does not recreate it for a content-only change — Caddy fixes (e.g. the 0.9.18 WebSocket fix) silently never deployed. Mount the docker/caddy directory instead (reflects the live file) and force-recreate the caddy container on every install/update so Caddyfile changes reliably take effect. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 11 +++++++++++ config/clusev.php | 2 +- docker-compose.prod.yml | 6 +++++- install.sh | 3 +++ tests/Feature/ExternalTlsModeTest.php | 14 ++++++++++++++ 5 files changed, 34 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4063277..969ab1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,17 @@ getaggte Releases (Kanal `stable`, optional `beta`) — niemals Entwicklungs-Bui _Keine offenen Änderungen — der nächste Stand wird hier gesammelt und als `vX.Y.Z` getaggt._ +## [0.9.19] - 2026-06-19 + +### Behoben +- **Caddyfile-Änderungen wurden bei einem Update nicht übernommen.** Die Caddy-Config war als + **einzelne Datei** gemountet; ein `git pull` ersetzt die Datei (neue Inode), aber der laufende + Container hängt weiter an der alten — und `docker compose up -d` baut Caddy bei reiner + Inhaltsänderung nicht neu. Dadurch kamen Caddy-Fixes (z. B. der WebSocket-Fix aus 0.9.18) ohne + manuelles Eingreifen nie an. Jetzt wird das **Verzeichnis** `docker/caddy` gemountet (spiegelt die + Datei live) und `install.sh` baut den Caddy-Container bei jedem Deploy gezielt neu + (`up -d --force-recreate caddy`), sodass Caddyfile-Änderungen zuverlässig wirksam werden. + ## [0.9.18] - 2026-06-19 ### Behoben diff --git a/config/clusev.php b/config/clusev.php index c026c52..c721c8a 100644 --- a/config/clusev.php +++ b/config/clusev.php @@ -2,7 +2,7 @@ return [ // First tagged release is v0.1.0 (semantic, not -dev). - 'version' => '0.9.18', + 'version' => '0.9.19', // Deployed commit + branch. install.sh bakes these into .env from the host's .git (the prod // image ships no .git); the Versions page prefers them and falls back to a live .git read in diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 7efa38b..b18fb94 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -86,7 +86,11 @@ services: # its X-Forwarded-Proto. Leave as 127.0.0.1/32 when Caddy terminates TLS itself. TRUSTED_PROXY_CIDR: "${TRUSTED_PROXY_CIDR:-127.0.0.1/32}" volumes: - - ./docker/caddy/Caddyfile:/etc/caddy/Caddyfile:ro + # Mount the DIRECTORY (not the single Caddyfile): a single-file bind mount keeps the + # original inode, so a `git pull` that REPLACES the file would not reach a running + # container. A directory mount always reflects the current file, so `caddy reload` / + # a recreate picks up Caddyfile changes on update. + - ./docker/caddy:/etc/caddy:ro - caddy-data:/data # ACME account + certs — MUST persist across recreate - caddy-config:/config depends_on: diff --git a/install.sh b/install.sh index e51bac1..0abb681 100755 --- a/install.sh +++ b/install.sh @@ -240,6 +240,9 @@ esac # ── [5/9] stack ────────────────────────────────────────────────────── phase 5/9 "Stack starten" $COMPOSE up -d +# Caddyfile changes ship as edits to a bind-mounted file; `up -d` does NOT recreate caddy for a +# content-only change, so force it to re-read the (possibly updated) Caddyfile on every deploy. +$COMPOSE up -d --force-recreate caddy >/dev/null 2>&1 || true info "Container gestartet" # ── host watchers (restart + update sentinels) — idempotent, best-effort ──── diff --git a/tests/Feature/ExternalTlsModeTest.php b/tests/Feature/ExternalTlsModeTest.php index 97852d5..3dac30f 100644 --- a/tests/Feature/ExternalTlsModeTest.php +++ b/tests/Feature/ExternalTlsModeTest.php @@ -3,11 +3,14 @@ namespace Tests\Feature; use App\Http\Middleware\PanelScheme; +use App\Livewire\System\Index as SystemIndex; use App\Models\Setting; +use App\Models\User; use App\Services\DeploymentService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Http\Request; use Illuminate\Http\Response; +use Livewire\Livewire; use Symfony\Component\HttpFoundation\Response as SymfonyResponse; use Symfony\Component\HttpKernel\Exception\HttpException; use Tests\TestCase; @@ -100,6 +103,17 @@ class ExternalTlsModeTest extends TestCase $this->runPanelScheme(Request::create('http://evil.example.com/dashboard')); } + public function test_status_card_does_not_claim_lets_encrypt_in_external_mode(): void + { + $this->actingAs(User::factory()->create(['must_change_password' => false])); + Setting::put('panel_domain', 'panel.example.com'); + app(DeploymentService::class)->setTlsMode('external'); + + Livewire::test(SystemIndex::class) + ->assertSee(__('system.tls_active_external_title')) + ->assertDontSee('Let\'s Encrypt'); + } + public function test_reverb_client_uses_wss_on_443_for_the_active_domain(): void { Setting::put('panel_domain', 'panel.example.com');