Skip to content

feat(proxy): run multiple local shops in parallel behind a shared proxy#1208

Draft
Tomasz Turkowski (tturkowski) wants to merge 5 commits into
nextfrom
feat/local-proxy-multiple-shops
Draft

feat(proxy): run multiple local shops in parallel behind a shared proxy#1208
Tomasz Turkowski (tturkowski) wants to merge 5 commits into
nextfrom
feat/local-proxy-multiple-shops

Conversation

@tturkowski

@tturkowski Tomasz Turkowski (tturkowski) commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

What changed?

New command group shopware-cli project proxy — run any number of local shops in parallel under stable hostnames, instead of everyone fighting over 127.0.0.1:8000.

Command Purpose
proxy setup one-time machine setup: wildcard DNS + HTTPS trust (single sudo ceremony; --domain, --skip-trust)
proxy up / down register/deregister the current project — fully reversible
proxy list / status overview of registered shops with their URLs
proxy verify bottom-up health check of the whole chain, with actionable hints
proxy teardown deregister everything and stop the shared infrastructure
$ shopware-cli project proxy list

  shop1.shopware.local  running  ~/shops/shop1
    Shop      https://shop1.shopware.local
    Admin     https://shop1.shopware.local/admin
    Adminer   https://adminer.shop1.shopware.local
    Mailpit   https://mailer.shop1.shopware.local

  shop2.shopware.local  running  ~/shops/shop2
    ...

Under the hood: one shared Traefik container routes by hostname (shops publish no host ports at all), a tiny DNS server embedded in the binary answers *.shopware.local → 127.0.0.1, and an mkcert-compatible local CA provides trusted HTTPS. Proxy mode is a marker-guarded compose.override.yaml — the base compose.yaml stays untouched, so project dev and manual docker compose keep working. up points APP_URL, the sales-channel domain and the project config at the proxy; down restores every value exactly.

➡️ Architecture, design decisions and trade-offs: docs/proxy.md

Why?

The dev environment publishes fixed host ports, so a second shop can't start — anyone working on multiple projects juggles ports or stops shops. Routing by hostname removes the conflict by construction, and trusted HTTPS matters for testing payment providers locally.

How was this tested?

  • go test ./... green, golangci-lint run ./... — 0 issues
  • unit tests for the pure logic: DNS wire format (incl. zone-spoofing edge cases), compose override generation, YAML/env surgery with exact-restore semantics, registry/settings round-trips, all user-facing guidance texts
  • manually end-to-end on macOS: three shops in parallel over trusted HTTPS, repeated up/down/teardown cycles with byte-identical restore of .shopware-project.yml, .env.local and the sales-channel domain; verify ladder validated against a real corporate sudo-block scenario

Related issue or discussion

Closes #1094, related: #939

@tturkowski
Tomasz Turkowski (tturkowski) force-pushed the feat/local-proxy-multiple-shops branch from 4f04d9f to 4b3b422 Compare July 17, 2026 12:32
Adds `shopware-cli project proxy` (setup/up/down/list/status/verify/
teardown): a shared Traefik container routes stable hostnames like
https://shop1.shopware.local to local projects, so shops publish no host
ports and any number can run at once.

- embedded wildcard DNS server (x/net/dns) on 127.0.0.1:53535, wired via
  /etc/resolver (macOS) or systemd-resolved (Linux) by a one-time
  `proxy setup` with a single sudo ceremony (--domain, --skip-trust)
- trusted HTTPS out of the box: mkcert-compatible local CA, per-project
  wildcard SANs, trust-store install via smallstep/truststore
- proxy mode is a marker-guarded compose.override.yaml (ports cleared with
  !reset, requires Compose >= 2.24); the base compose.yaml stays untouched,
  so `project dev` and manual docker compose keep working in both modes
- `up` points APP_URL, the sales channel domain and the url keys in
  .shopware-project.yml at the proxy; `down` restores everything exactly
- `verify` checks the whole chain bottom-up with actionable hints,
  including guidance when sudo is blocked or systemd-resolved is missing
- docs/proxy.md explains the architecture, decisions and trade-offs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@shyim

Soner (shyim) commented Jul 20, 2026

Copy link
Copy Markdown
Member

Whats definitively missing here is:

  • How does this work on WSL2, seperate network, DNS?
  • What about the watchers (thats the most hard part)
  • SSL Certificate injection into containers and basic reachability, how does container A reach Container B over SSL and that domain

@tturkowski

Copy link
Copy Markdown
Contributor Author

Dev watchers through the shared proxy

A quick summary of how the admin/storefront watchers can work behind the shared reverse proxy.

Admin watcher — works as-is, no code changes

The admin is Vite-only. Vite works out its own HMR connection from the page it's loaded on, so all that was needed:

  • route the admin-watch.<shop> hostname through Traefik to the Vite dev server, and
  • show that URL in the TUI.

You open https://admin-watch.shop1.shopware.local directly and HMR just works.

Storefront watcher (now) — webpack + a small runtime patch

The storefront's classic watcher (HMR + webpack, @deprecated, to be removed in 6.9) exposes two fixed ports (9998 + 9999). This is a blocker for our proxy: the browser's hot-reload websocket target (hostname + port) is baked into the vendor code (webpack-dev-server's client.webSocketURL, hardcoded to 0.0.0.0) and can't be set from any project file or env var. That's what stops it from routing through our single-port proxy.

Rather than patching vendor file, we inject a tiny preload script when launching the watcher (Node --require) that overrides webSocketURL at runtime, pointing it at storefront-watch.<shop> through the proxy. The vendor code runs untouched; we just correct one value on the way through.

Result: the storefront watcher runs fully through the proxy - multiple shops in parallel, clean port-free hostnames, no exposed ports, and no change to Shopware or the shop. You browse https://storefront-watch.shop1.shopware.local.

Tradeoff: it's a runtime patch — clever but hidden, and it leans on the internals of shopware/shopware code. If that internal behavior ever changes (I don't think it will, but it feels worth mentioning), hot-reload could quietly stop working with no obvious error. In my opinion it's acceptable for a bridge on a code path that's going away.

Storefront watcher (future) — Vite, the clean path

From 6.7.11 the storefront also ships a Vite dev server. To make that work behind a reverse proxy we need a small, fully backward-compatible contribution to shopware/shopware (Storefront bundle):

  • the dev import-map plugin should use Vite's server.origin instead of hardcoding http://localhost:<port>, and
  • the vite config should set origin / allowedHosts / host from an env var when it's set

With that, the storefront watcher - once enabled, works at the shop's own URL (https://shop1.shopware.local) - no separate hostname.

Plan proposal:

  • implement the webpack + runtime-patch path now (it covers every shop, since webpack is on everything until 6.9)
  • file the Vite contribution in parallel, and once it's done we can offer Vite as the watcher for newer shops - gradually migrating off the runtime patch, which we can track via telemetry. Webpack won't be removed until 6.9, so there's plenty of runway.

@shyim

Copy link
Copy Markdown
Member

btw because of excactly those REASONS I DONT WANT TO have those watchers directly inside Shopware. we're like now screwed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants