Skip to content

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

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

feat(proxy): run multiple local shops in parallel behind a shared proxy#1208
Tomasz Turkowski (tturkowski) wants to merge 22 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

@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

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>
@tturkowski
Tomasz Turkowski (tturkowski) force-pushed the feat/local-proxy-multiple-shops branch from 54c9305 to cae4216 Compare July 30, 2026 11:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new shopware-cli project proxy command group and supporting infrastructure to run multiple local Shopware projects in parallel behind a shared Traefik reverse proxy, using stable per-project hostnames with local DNS resolution and trusted HTTPS.

Changes:

  • Adds a new internal/proxy subsystem (DNS daemon, resolver configuration, Traefik management, verification, trust store integration, registry/settings state).
  • Implements proxy-mode Docker Compose overrides (marker-guarded compose.override.yaml) to remove fixed host ports and route by hostname via Traefik.
  • Integrates proxy awareness into project create, project dev, the dev TUI overview, and storefront watcher routing.

Reviewed changes

Copilot reviewed 69 out of 70 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
internal/tui/dev/tab_overview_health.go Adds proxy-related setup health checks to the TUI overview health panel.
internal/tui/dev/tab_overview_format_test.go Adds tests for overview formatting helpers used by the TUI.
internal/tui/dev/model.go Adds proxy fallback handling and an interactive proxy setup execution path in the TUI.
internal/system/dockername.go Introduces Docker Compose project-name validation helper.
internal/proxy/wsl_resolver_test.go Adds unit tests for WSL DNS resolver guidance text.
internal/proxy/windows_access.go Adds WSL/Windows browser access guidance and hostname list generation.
internal/proxy/windows_access_test.go Adds tests for Windows access guidance and hostname generation.
internal/proxy/verify.go Implements proxy verify bottom-up checks (Docker → DNS → OS resolver → Traefik → trusted HTTPS).
internal/proxy/verify_windows.go Windows stub for OS-level resolution check.
internal/proxy/verify_test.go Adds tests for verification hints and probe-hostname generation.
internal/proxy/verify_linux.go Linux OS-resolution check via getent hosts.
internal/proxy/verify_darwin.go macOS OS-resolution check via dscacheutil.
internal/proxy/trust.go Implements CA trust installation flow and user guidance (mkcert/truststore).
internal/proxy/trust_test.go Adds tests for trust-blocked guidance content.
internal/proxy/traefik.go Adds Traefik container/network lifecycle management and hostname alias reconciliation.
internal/proxy/traefik_test.go Adds tests for Traefik dynamic config writing and alias helpers.
internal/proxy/stats.go Adds proxy instance stats collection for the TUI (shop count + memory sum).
internal/proxy/stats_test.go Adds tests for parsing Docker memory usage strings.
internal/proxy/statedir.go Adds a shared state directory helper for proxy state files.
internal/proxy/settings.go Adds machine-wide proxy settings (base domain) with validation and persistence.
internal/proxy/settings_test.go Adds tests for settings validation and persistence round-trips.
internal/proxy/resolver.go Adds resolver status types and Linux-without-systemd-resolved guidance text.
internal/proxy/resolver_windows.go Windows resolver stubs reporting unsupported behavior.
internal/proxy/resolver_linux.go Linux systemd-resolved split-DNS configuration (configure/unconfigure + guidance).
internal/proxy/resolver_linux_test.go Adds tests for Linux resolver blocked guidance content.
internal/proxy/resolver_darwin.go macOS /etc/resolver configuration (configure/unconfigure + guidance).
internal/proxy/resolver_darwin_test.go Adds tests for macOS resolver blocked guidance content.
internal/proxy/registry.go Adds registry state for registered projects and restore metadata.
internal/proxy/registry_test.go Adds registry behavior tests (upsert/remove/find/round-trip).
internal/proxy/projectconfig.go Adds comment-preserving YAML URL rewrite/restore logic for .shopware-project.yml.
internal/proxy/projectconfig_test.go Adds tests for URL rewrite/restore semantics and missing-file behavior.
internal/proxy/hostname.go Adds proxy hostname derivation (from config URL or directory name).
internal/proxy/hostname_test.go Adds tests for hostname derivation edge cases.
internal/proxy/docker.go Adds Docker Compose version check for !reset support and docker runner helper.
internal/proxy/dns.go Adds embedded DNS server implementation and direct-query helper for verification/tests.
internal/proxy/dns_test.go Adds tests for DNS zone behavior and garbage packet handling.
internal/proxy/dns_daemon.go Adds non-Windows DNS daemon spawning/management via self re-exec and PID/state files.
internal/proxy/dns_daemon_windows.go Adds Windows stubs and shared “not supported” error for DNS daemon operations.
internal/proxy/cert.go Adds mkcert-compatible CA/cert management and SAN host list generation.
internal/proxy/cert_test.go Adds tests for certificate creation, idempotency, and regeneration triggers.
internal/proxy/canonical.go Adds canonical project-root resolution (symlink normalization).
internal/mkcert/mkcert.go Adds BSD-licensed mkcert-derived CA/certificate implementation as an internal library.
internal/mkcert/mkcert_test.go Adds tests for CAROOT behavior, CA creation/reuse, cert issuance, and keyless mode.
internal/mkcert/LICENSE Adds the mkcert BSD license text for the adapted code.
internal/extension/storefront_watch.go Adds proxy-mode support for the deprecated storefront hot-proxy watcher via env + Node preload.
internal/extension/storefront_hmr_patch.cjs Adds a managed Node preload patch to rewrite webpack-dev-server websocket target behind the proxy.
internal/executor/docker.go Improves watcher shutdown by SIGINTing the full in-container process tree (not just the wrapper process).
internal/envfile/upsert.go Adds an env-file “upsert var” helper for surgical .env updates.
internal/envfile/upsert_test.go Adds tests for env var upsert and read behavior.
internal/docker/compose.go Adds a YAML boolean-node helper used by proxy compose override generation.
internal/docker/compose_test.go Adds a regression test ensuring base compose output remains non-proxy (ports/labels absent).
internal/docker/compose_override.go Adds generation + write/remove for marker-guarded proxy compose overrides with Traefik routes.
internal/docker/compose_override_test.go Adds extensive tests for override content and safety checks (refuse user override files).
go.mod Adds github.com/smallstep/truststore and an indirect plist dependency for trust installation.
go.sum Adds checksums for new module dependencies.
docs/proxy.md Adds end-to-end architecture/design documentation for the shared proxy feature set.
cmd/root.go Treats ErrProxyNotRegistered as a user-facing error (exit 1 without extra logging).
cmd/project/project_storefront_watch.go Routes storefront watcher through proxy hostname when the project is proxied.
cmd/project/project_proxy_verify.go Adds project proxy verify command and shared output printer for verification steps.
cmd/project/project_proxy_test.go Adds tests for local-domain choice resolution and Shopware command availability detection.
cmd/project/project_proxy_setup.go Adds project proxy setup and teardown, including DNS/trust installation and verification.
cmd/project/project_proxy_list.go Adds project proxy list and status commands with running-instance detection and links.
cmd/project/project_proxy_dns_serve.go Adds hidden internal subcommand used as the DNS daemon re-exec target.
cmd/project/project_dev.go Bootstraps proxy infra for proxy-mode projects with a non-blocking fallback to port mode.
cmd/project/project_dev_test.go Adds tests for proxy-project detection and local-domain hostname normalization.
cmd/project/project_create.go Adds --local-domain support, inline (prompted) one-time setup option, and base-domain lookup.
cmd/project/project_create_install.go Writes proxy hostname URLs into the created project config and updates create summary output.
cmd/project/project_create_form.go Extends interactive create form to prompt for local domains and optional one-time machine setup.
Suppressed comments (1)

internal/proxy/hostname.go:33

  • ProjectHostname can produce invalid DNS hostnames when the project directory contains underscores (Docker Compose allows them, DNS labels do not). Sanitizing underscores to dashes here keeps hostnames valid and matches the behavior in project create (localDomainHostname).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/proxy/hostname.go
Comment thread internal/proxy/hostname_test.go
Comment thread internal/system/dockername.go
Comment thread internal/proxy/verify.go
@tturkowski

Copy link
Copy Markdown
Contributor Author
* [x]  How does this work on WSL2, seperate network, DNS?

Works same way as it works on mac or windows, we do serve dns and in wsl we redirect domain traffic to our dns that resolves to 127.0.0.1 and then traefik takes care about the rest. WSL users who want to access WSL-running shops from Windows browser would need to one time setup Windows's hosts file.

* [x]  What about the watchers (thats the most hard part)

Made them work, but it was a hard part. See comment above with details.

* [x]  SSL Certificate injection into containers and basic reachability, how does container A reach Container B over SSL and that domain

Made that work, containers will resolve from domain to the container, no need of container names usage.

@lasomethingsomething

somethings (lasomethingsomething) commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Follow-up from our chat:

  • In the right-side column, under "User Action," change Domains => "Local domains enabled"; Domains (default) => "Default domains configured"; Trust cert (t) => "Local certificate trusted"
  • Move the memory indicator to the bottom of the Overview tab and provide a scroll in prep for many-instance scenarios (see image)
36265b74-19ec-41aa-ba48-7d6fbf93eeb4-1

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.

5 participants