Splits the two hostnames across one deployment. No files move: the Next app still serves every route on both hosts, and the middleware decides per host which paths it owns and 301s the rest. /login and /signup stay on www - all 82 marketing CTAs point at /signup, which carries a hard canonical to www plus ad traffic. src/lib/hosts.ts is the single source of truth for the boundary (APP_PATH_PREFIXES, isAppPath, wwwUrl, appUrl, urlForPath). The middleware and every absolute-URL builder read from it so they cannot drift apart. - Split the overloaded NEXT_PUBLIC_APP_URL into a www and an app origin. It previously fed both public URLs and in-app URLs, so any single value was wrong somewhere. Most important: QRCodeCard encodes this origin into the QR code the user downloads and prints, so it must stay on www. - Route Stripe return URLs, email links and OAuth redirects per path rather than against one origin, so /dashboard lands on app and /pricing on www. - Cross the host boundary once, after a successful login: the router cannot push across origins, so that jump needs a full load. The user arrives signed in because the session cookie is scoped to COOKIE_DOMAIN. - Keep the app host out of search indexes: X-Robots-Tag on every response plus a Disallow-all robots.txt via rewrite, and /sitemap.xml redirects to www. - Point the TikTok callback fallback at www explicitly. It used to read NEXT_PUBLIC_APP_URL, whose meaning changed here, and only the apex domain is verified with TikTok. Host splitting is inert while both origins are equal, so development is unaffected. Verified: tsc clean, production build succeeds including the Edge middleware bundle, and the path-to-host mapping is unit-checked (prefix traps like /created and /settings-guide stay on www, query strings do not break matching). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
141 lines
4.9 KiB
YAML
141 lines
4.9 KiB
YAML
services:
|
|
# PostgreSQL Database
|
|
db:
|
|
image: postgres:16-alpine
|
|
container_name: qrmaster-db
|
|
restart: unless-stopped
|
|
environment:
|
|
POSTGRES_USER: ${POSTGRES_USER}
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
|
POSTGRES_DB: ${POSTGRES_DB}
|
|
POSTGRES_INITDB_ARGS: "-E UTF8 --locale=en_US.utf8"
|
|
ports:
|
|
- "5435:5432"
|
|
volumes:
|
|
- dbdata:/var/lib/postgresql/data
|
|
- ./docker/init-db.sh:/docker-entrypoint-initdb.d/init-db.sh
|
|
healthcheck:
|
|
test: [ "CMD-SHELL", "pg_isready -U postgres -d qrmaster" ]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 10
|
|
networks:
|
|
- qrmaster-network
|
|
|
|
# Redis Cache
|
|
redis:
|
|
image: redis:7-alpine
|
|
container_name: qrmaster-redis
|
|
restart: unless-stopped
|
|
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
|
|
volumes:
|
|
- redisdata:/data
|
|
healthcheck:
|
|
test: [ "CMD", "redis-cli", "ping" ]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 5
|
|
networks:
|
|
- qrmaster-network
|
|
|
|
# Next.js Application
|
|
web:
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile
|
|
args:
|
|
NEXT_PUBLIC_UMAMI_SRC: ${NEXT_PUBLIC_UMAMI_SRC:-}
|
|
NEXT_PUBLIC_UMAMI_ID: ${NEXT_PUBLIC_UMAMI_ID:-}
|
|
COOKIE_DOMAIN: ${COOKIE_DOMAIN:-}
|
|
container_name: qrmaster-web
|
|
restart: unless-stopped
|
|
environment:
|
|
NODE_ENV: production
|
|
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?schema=public
|
|
DIRECT_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?schema=public
|
|
REDIS_URL: redis://redis:6379
|
|
NEXTAUTH_URL: ${NEXTAUTH_URL}
|
|
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
|
|
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-http://localhost:3050}
|
|
NEXT_PUBLIC_WWW_URL: ${NEXT_PUBLIC_WWW_URL:-http://localhost:3050}
|
|
COOKIE_DOMAIN: ${COOKIE_DOMAIN:-}
|
|
INTERNAL_API_SECRET: ${INTERNAL_API_SECRET}
|
|
TIKTOK_CLIENT_KEY: ${TIKTOK_CLIENT_KEY:-}
|
|
TIKTOK_CLIENT_SECRET: ${TIKTOK_CLIENT_SECRET:-}
|
|
TIKTOK_REDIRECT_URI: ${TIKTOK_REDIRECT_URI:-https://qrmaster.net/api/tiktok/callback}
|
|
TIKTOK_ADMIN_KEY: ${TIKTOK_ADMIN_KEY:-}
|
|
TIKTOK_EXPECTED_OPEN_ID: ${TIKTOK_EXPECTED_OPEN_ID:-}
|
|
IP_SALT: ${IP_SALT:-your-salt-change-in-production}
|
|
ENABLE_DEMO: ${ENABLE_DEMO:-false}
|
|
NEXT_PUBLIC_INDEXABLE: ${NEXT_PUBLIC_INDEXABLE:-true}
|
|
# Google OAuth
|
|
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
|
|
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
|
|
# Stripe
|
|
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY:-}
|
|
STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET:-}
|
|
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: ${NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY:-}
|
|
STRIPE_PRICE_ID_PRO_MONTHLY: ${STRIPE_PRICE_ID_PRO_MONTHLY:-}
|
|
STRIPE_PRICE_ID_PRO_YEARLY: ${STRIPE_PRICE_ID_PRO_YEARLY:-}
|
|
STRIPE_PRICE_ID_BUSINESS_MONTHLY: ${STRIPE_PRICE_ID_BUSINESS_MONTHLY:-}
|
|
STRIPE_PRICE_ID_BUSINESS_YEARLY: ${STRIPE_PRICE_ID_BUSINESS_YEARLY:-}
|
|
# Email & Analytics
|
|
RESEND_API_KEY: ${RESEND_API_KEY:-}
|
|
SMTP_HOST: ${SMTP_HOST:-smtp.qrmaster.net}
|
|
SMTP_PORT: ${SMTP_PORT:-465}
|
|
SMTP_USER: ${SMTP_USER:-timo@qrmaster.net}
|
|
SMTP_PASS: ${SMTP_PASS:-}
|
|
NEWSLETTER_ADMIN_EMAIL: ${NEWSLETTER_ADMIN_EMAIL:-}
|
|
NEWSLETTER_ADMIN_PASSWORD: ${NEWSLETTER_ADMIN_PASSWORD:-}
|
|
NEWSLETTER_TEST_EMAIL: ${NEWSLETTER_TEST_EMAIL:-}
|
|
EMAIL_UNSUBSCRIBE_SECRET: ${EMAIL_UNSUBSCRIBE_SECRET:-}
|
|
NEXT_PUBLIC_POSTHOG_KEY: ${NEXT_PUBLIC_POSTHOG_KEY:-}
|
|
NEXT_PUBLIC_POSTHOG_HOST: ${NEXT_PUBLIC_POSTHOG_HOST:-https://us.i.posthog.com}
|
|
# Cloudflare R2 Storage
|
|
R2_ACCOUNT_ID: ${R2_ACCOUNT_ID:-}
|
|
R2_ACCESS_KEY_ID: ${R2_ACCESS_KEY_ID:-}
|
|
R2_SECRET_ACCESS_KEY: ${R2_SECRET_ACCESS_KEY:-}
|
|
R2_BUCKET_NAME: ${R2_BUCKET_NAME:-qrmaster-menus}
|
|
R2_PUBLIC_URL: ${R2_PUBLIC_URL:-}
|
|
NEXT_PUBLIC_UMAMI_SRC: ${NEXT_PUBLIC_UMAMI_SRC:-}
|
|
NEXT_PUBLIC_UMAMI_ID: ${NEXT_PUBLIC_UMAMI_ID:-}
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
redis:
|
|
condition: service_healthy
|
|
healthcheck:
|
|
test: [ "CMD", "node", "-e", "require('http').get('http://localhost:3000',()=>process.exit(0)).on('error',()=>process.exit(1))" ]
|
|
interval: 10s
|
|
timeout: 3s
|
|
retries: 10
|
|
networks:
|
|
- qrmaster-network
|
|
|
|
|
|
# Adminer - Database Management UI (Optional)
|
|
adminer:
|
|
image: adminer:latest
|
|
container_name: qrmaster-adminer
|
|
restart: unless-stopped
|
|
ports:
|
|
- "8080:8080"
|
|
environment:
|
|
ADMINER_DEFAULT_SERVER: db
|
|
depends_on:
|
|
- db
|
|
networks:
|
|
- qrmaster-network
|
|
profiles:
|
|
- dev
|
|
|
|
volumes:
|
|
dbdata:
|
|
driver: local
|
|
redisdata:
|
|
driver: local
|
|
|
|
networks:
|
|
qrmaster-network:
|
|
external: true
|