feat(docker): add Caddy reverse proxy and domain-portable Reverb config

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.
This commit is contained in:
Paulo Castellano 2026-05-27 14:53:46 -03:00
parent 5b9226569f
commit 48727c80cd
5 changed files with 61 additions and 9 deletions

View file

@ -63,6 +63,13 @@ jobs:
target: production target: production
platforms: ${{ matrix.platform }} platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }} 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-from: type=gha,scope=${{ env.PLATFORM_PAIR }}
cache-to: type=gha,mode=max,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 outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true

6
Caddyfile Normal file
View file

@ -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
}

View file

@ -40,10 +40,13 @@ services:
BROADCAST_CONNECTION: reverb BROADCAST_CONNECTION: reverb
# ===== WebSockets (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_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_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_PORT: "8080"
REVERB_SCHEME: http REVERB_SCHEME: http
@ -102,8 +105,7 @@ services:
# ANTHROPIC_API_KEY: "" # ANTHROPIC_API_KEY: ""
# GEMINI_API_KEY: "" # GEMINI_API_KEY: ""
ports: ports:
- "8000:80" # app (nginx) - "8000:80" # app (nginx); WebSocket rides this same port via /app
- "8080:8080" # Reverb WebSocket
volumes: volumes:
- storage:/var/www/html/storage/app - storage:/var/www/html/storage/app
depends_on: depends_on:
@ -141,7 +143,33 @@ services:
timeout: 3s timeout: 3s
retries: 5 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://<that domain>.
# 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: volumes:
pgdata: pgdata:
redisdata: redisdata:
storage: storage:
caddy-data:
caddy-config:

View file

@ -1,7 +1,7 @@
import '../css/app.css'; import '../css/app.css';
import './echo';
import { createInertiaApp, router } from '@inertiajs/vue3'; import { createInertiaApp, router } from '@inertiajs/vue3';
import { configureEcho } from '@laravel/echo-vue';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { i18nVue } from 'laravel-vue-i18n'; import { i18nVue } from 'laravel-vue-i18n';
import type { DefineComponent } from 'vue'; import type { DefineComponent } from 'vue';
@ -12,10 +12,6 @@ import dayjs from './dayjs';
import { capturePageview, initializePostHog, syncPostHogContext } from './posthog'; import { capturePageview, initializePostHog, syncPostHogContext } from './posthog';
import type { Auth } from './types'; import type { Auth } from './types';
configureEcho({
broadcaster: 'reverb',
});
const appName = import.meta.env.VITE_APP_NAME || 'TryPost.it'; const appName = import.meta.env.VITE_APP_NAME || 'TryPost.it';
createInertiaApp({ createInertiaApp({

15
resources/js/echo.ts Normal file
View file

@ -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'],
});