From 48727c80cd232d767914c28f90b90c152fa95b1e Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Wed, 27 May 2026 14:53:46 -0300 Subject: [PATCH] feat(docker): add Caddy reverse proxy and domain-portable Reverb config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an optional Caddy service (proxy profile) to compose.prod.yaml for automatic HTTPS on a custom domain, with a Caddyfile that transparently proxies WebSocket upgrades. Move Echo setup into resources/js/echo.ts and derive the Reverb host from window.location when VITE_REVERB_* is unset, so the published image's frontend connects on any domain without a rebuild. The release workflow bakes a fixed public Reverb key and leaves host/port/scheme empty to enable this; the Herd dev setup keeps its explicit VITE_REVERB_* values, so its behaviour is unchanged. Drop the now-unneeded 8080 host port — the WebSocket rides the main HTTP(S) port via the in-container nginx /app proxy. --- .github/workflows/release-docker.yml | 7 ++++++ Caddyfile | 6 +++++ compose.prod.yaml | 36 ++++++++++++++++++++++++---- resources/js/app.ts | 6 +---- resources/js/echo.ts | 15 ++++++++++++ 5 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 Caddyfile create mode 100644 resources/js/echo.ts diff --git a/.github/workflows/release-docker.yml b/.github/workflows/release-docker.yml index 2bd275ae..dec1474b 100644 --- a/.github/workflows/release-docker.yml +++ b/.github/workflows/release-docker.yml @@ -63,6 +63,13 @@ jobs: target: production platforms: ${{ matrix.platform }} labels: ${{ steps.meta.outputs.labels }} + # Bake a fixed public Reverb key; leave host/port/scheme empty so the + # frontend derives them from window.location (works on any domain). + build-args: | + VITE_REVERB_APP_KEY=trypost-reverb-key + VITE_REVERB_HOST= + VITE_REVERB_PORT= + VITE_REVERB_SCHEME= cache-from: type=gha,scope=${{ env.PLATFORM_PAIR }} cache-to: type=gha,mode=max,scope=${{ env.PLATFORM_PAIR }} outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 00000000..add53be2 --- /dev/null +++ b/Caddyfile @@ -0,0 +1,6 @@ +# Caddy serves TryPost on your domain with automatic HTTPS (Let's Encrypt). +# APP_DOMAIN is provided by the caddy service in compose.prod.yaml. +# reverse_proxy transparently passes through WebSocket upgrades (Reverb on /app). +{$APP_DOMAIN} { + reverse_proxy app:80 +} diff --git a/compose.prod.yaml b/compose.prod.yaml index e407888e..c105ae72 100644 --- a/compose.prod.yaml +++ b/compose.prod.yaml @@ -40,10 +40,13 @@ services: BROADCAST_CONNECTION: reverb # ===== WebSockets (Reverb) ===== + # These are the INTERNAL server->Reverb connection — leave as-is even on a + # domain. The browser derives the WebSocket host from its own URL, and the + # in-container nginx proxies it on /app, so it just works on any domain. REVERB_APP_ID: "1001" - REVERB_APP_KEY: trypost-reverb-key + REVERB_APP_KEY: trypost-reverb-key # matches the key baked into the published image REVERB_APP_SECRET: change-me-reverb-secret # <- change me - REVERB_HOST: localhost # <- for a real domain, set to that host (and PORT 443 / SCHEME https) + REVERB_HOST: localhost REVERB_PORT: "8080" REVERB_SCHEME: http @@ -102,8 +105,7 @@ services: # ANTHROPIC_API_KEY: "" # GEMINI_API_KEY: "" ports: - - "8000:80" # app (nginx) - - "8080:8080" # Reverb WebSocket + - "8000:80" # app (nginx); WebSocket rides this same port via /app volumes: - storage:/var/www/html/storage/app depends_on: @@ -141,7 +143,33 @@ services: timeout: 3s retries: 5 + # Optional reverse proxy with automatic HTTPS (Let's Encrypt). + # To serve on a domain: + # 1. Point the domain's DNS at this host. + # 2. Set APP_DOMAIN below and APP_URL above to https://. + # 3. Start with the proxy profile: + # docker compose -f compose.prod.yaml --profile proxy up -d + # Without the profile, the app is served directly on http://localhost:8000. + caddy: + image: caddy:2-alpine + container_name: trypost-caddy + restart: unless-stopped + profiles: [proxy] + environment: + APP_DOMAIN: post.example.com # <- your domain + ports: + - "80:80" + - "443:443" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy-data:/data + - caddy-config:/config + depends_on: + - app + volumes: pgdata: redisdata: storage: + caddy-data: + caddy-config: diff --git a/resources/js/app.ts b/resources/js/app.ts index c63c45ea..de757e69 100644 --- a/resources/js/app.ts +++ b/resources/js/app.ts @@ -1,7 +1,7 @@ import '../css/app.css'; +import './echo'; import { createInertiaApp, router } from '@inertiajs/vue3'; -import { configureEcho } from '@laravel/echo-vue'; import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; import { i18nVue } from 'laravel-vue-i18n'; import type { DefineComponent } from 'vue'; @@ -12,10 +12,6 @@ import dayjs from './dayjs'; import { capturePageview, initializePostHog, syncPostHogContext } from './posthog'; import type { Auth } from './types'; -configureEcho({ - broadcaster: 'reverb', -}); - const appName = import.meta.env.VITE_APP_NAME || 'TryPost.it'; createInertiaApp({ diff --git a/resources/js/echo.ts b/resources/js/echo.ts new file mode 100644 index 00000000..c378c0cd --- /dev/null +++ b/resources/js/echo.ts @@ -0,0 +1,15 @@ +import { configureEcho } from '@laravel/echo-vue'; + +const scheme = import.meta.env.VITE_REVERB_SCHEME || (window.location.protocol === 'https:' ? 'https' : 'http'); +const forceTLS = scheme === 'https'; +const port = Number(import.meta.env.VITE_REVERB_PORT || window.location.port || (forceTLS ? 443 : 80)); + +configureEcho({ + broadcaster: 'reverb', + key: import.meta.env.VITE_REVERB_APP_KEY, + wsHost: import.meta.env.VITE_REVERB_HOST || window.location.hostname, + wsPort: port, + wssPort: port, + forceTLS, + enabledTransports: ['ws', 'wss'], +});