Problem
prj--personal-portfolio--v3 ships four separate frontends - Astro portfolio, Astro blog, Astro news-feed, and a Vite/React quiz PWA - deployed in production as their own subdomains: paulserban.eu, blog.paulserban.eu, news-feed.paulserban.eu, and quiz.paulserban.eu. Run locally the plain way, each app instead gets its own dev port. That breaks in three concrete ways: the shared SiteSwitcher header links point at real production URLs even in dev, nothing exercises HTTPS the way the deployed sites do, and cross-domain behaviour - cookies, CORS, mixed content - simply doesn't exist when everything lives on localhost:PORT. Bugs that only show up once sites are split across real domains were invisible until deploy.
Approach
I treated local development like a miniature version of the production domain layout: Traefik terminates TLS on ports 80 and 443, redirects HTTP to HTTPS, and routes by Host header to the right container - so the local topology matches the production one subdomain-for-subdomain instead of port-for-port.
- One local subdomain per production subdomain -
local.paulserban.eu,local.blog.paulserban.eu,local.quiz.paulserban.eu, andlocal.news-feed.paulserban.eumap 1:1 to their real counterparts, so the app never has to special-case "am I in dev." - mkcert issues browser-trusted certificates for those local subdomains - mapped via
/etc/hosts, not a real DNS server - so HTTPS looks and behaves like production instead of throwing certificate warnings or falling back to plain HTTP. - Per-app
local.Dockerfile, all built from a sharedlocal.base.Dockerfile, with monorepo-root build context so pnpm workspace dependencies resolve cleanly. - Docker Compose wires Traefik, the four dev servers, and a shared bridge network; the Astro sites bind-mount
content.dbfor build-time queries. - Cross-site navigation via
PUBLIC_*/VITE_*URL env vars so the sharedSiteSwitcherlinks to local subdomains instead of production while running locally. - HMR over WSS through Traefik - Vite and Astro dev servers configured with
allowedHostsand explicit HMR host/port/protocol, since secure-context WebSocket upgrades behave differently than plain-HTTP HMR.
Browser ──► Traefik (:443, TLS) ──► portfolio (:4321)
│ ──► blog (:4321)
│ ──► quiz (:5180)
│ ──► news (:4321)
└── HTTP :80 -> HTTPS redirect
Why mimic production domains instead of just using ports
Running four apps on localhost:4321, :4322, :5180, etc. hides an entire class of bugs that only exist once sites are split across real, independent subdomains:
- Cross-site navigation - the header
SiteSwitcherlinks to real subdomains; on plain ports those links are either hardcoded to production (wrong in dev) or silently untested. - Cookie and storage scoping - anything scoped per-origin (cookies,
localStorage, service workers for the quiz PWA) behaves differently across sibling subdomains than acrosslocalhostports. - Mixed content and CSP - production is HTTPS-only; testing over plain HTTP on
localhostmasks mixed-content and content-security-policy failures until they surface in production. - CORS - requests between
blog.paulserban.euandquiz.paulserban.euare genuinely cross-origin;localhost:PORTtolocalhost:PORTis a different (and more permissive) browser security boundary. - HMR reliability - Vite/Astro's HMR websocket has to survive a TLS-terminating reverse proxy, which is a meaningfully different code path than a bare local dev server.
Matching the subdomain shape, not just the port numbers, means these issues get caught while iterating locally instead of after a deploy.
Outcome
A single docker compose up --build boots the full system. Developers hit real domain names on standard ports, cross-site navigation and HMR work the same way they will in production, and HTTPS behaviour matches production closely enough to catch mixed-content, cookie-scope, and CORS issues early. Setup guides for macOS and Debian live in _docs/infrastructure/.
Part of Personal Portfolio v3
This Traefik stack is the local-dev counterpart to the real domain layout of Personal Portfolio v3, a TypeScript pnpm monorepo that ships four frontends from one SQLite artifact. Related pieces of the platform:
- The content pipeline that feeds all four surfaces from a single SQLite database
- The spaced-repetition quiz PWA (quiz.paulserban.eu), one of the apps this stack serves locally
- The shared newspaper design system UI kit used across every surface
- CI/CD that ingests content, builds all four surfaces in parallel, and deploys a merged GitHub Pages preview