fix(deploy): apply Caddyfile changes on update (dir mount + recreate)

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) <noreply@anthropic.com>
feat/v1-foundation v0.9.19
boban 2026-06-19 20:57:42 +02:00
parent 92969eb0dc
commit a61feb1bef
5 changed files with 34 additions and 2 deletions

View File

@ -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._ _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 ## [0.9.18] - 2026-06-19
### Behoben ### Behoben

View File

@ -2,7 +2,7 @@
return [ return [
// First tagged release is v0.1.0 (semantic, not -dev). // 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 // 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 // image ships no .git); the Versions page prefers them and falls back to a live .git read in

View File

@ -86,7 +86,11 @@ services:
# its X-Forwarded-Proto. Leave as 127.0.0.1/32 when Caddy terminates TLS itself. # 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}" TRUSTED_PROXY_CIDR: "${TRUSTED_PROXY_CIDR:-127.0.0.1/32}"
volumes: 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-data:/data # ACME account + certs — MUST persist across recreate
- caddy-config:/config - caddy-config:/config
depends_on: depends_on:

View File

@ -240,6 +240,9 @@ esac
# ── [5/9] stack ────────────────────────────────────────────────────── # ── [5/9] stack ──────────────────────────────────────────────────────
phase 5/9 "Stack starten" phase 5/9 "Stack starten"
$COMPOSE up -d $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" info "Container gestartet"
# ── host watchers (restart + update sentinels) — idempotent, best-effort ──── # ── host watchers (restart + update sentinels) — idempotent, best-effort ────

View File

@ -3,11 +3,14 @@
namespace Tests\Feature; namespace Tests\Feature;
use App\Http\Middleware\PanelScheme; use App\Http\Middleware\PanelScheme;
use App\Livewire\System\Index as SystemIndex;
use App\Models\Setting; use App\Models\Setting;
use App\Models\User;
use App\Services\DeploymentService; use App\Services\DeploymentService;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Livewire\Livewire;
use Symfony\Component\HttpFoundation\Response as SymfonyResponse; use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\HttpException;
use Tests\TestCase; use Tests\TestCase;
@ -100,6 +103,17 @@ class ExternalTlsModeTest extends TestCase
$this->runPanelScheme(Request::create('http://evil.example.com/dashboard')); $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 public function test_reverb_client_uses_wss_on_443_for_the_active_domain(): void
{ {
Setting::put('panel_domain', 'panel.example.com'); Setting::put('panel_domain', 'panel.example.com');