Skip to content

Network and domains

PerfShop runs on a single Docker bridge network named perfshop-network, which isolates communications between the 31 services and enables DNS resolution by container name. On the host side, each application service exposes its port through the standard "<host_port>:<container_port>" mapping configured by environment variables.

This page describes:

  1. The internal Docker network topology
  2. The complete table of ports exposed to the host
  3. The inter-service communication conventions
  4. The external reverse proxy pattern for exposing the stack over HTTPS

Source of truth

The ports, service names and environment variables on this page are extracted from the docker-compose.desktop.yml and docker-compose.build.yml files. The public DNS conventions are derived from the PUBLIC_*_URL variables of .env.example.

Docker network topology

flowchart TB
  subgraph host["Host (machine or VPS)"]
    rp{"External reverse proxy<br/>(optional)<br/>nginx, Caddy, Traefik..."}

    subgraph engine["Docker Engine"]
      subgraph bridge["perfshop-network (bridge)"]
        direction TB

        subgraph c["Core"]
          DB[("perfshop-db<br/>:3306")]
          APP["perfshop-app<br/>:8080 + :9090"]
          FE["perfshop-frontend<br/>:80"]
          MON["perfshop-monitoring<br/>:3001"]
          ADM["perfshop-admin<br/>:80"]
          CADM["perfshop-chaos-admin<br/>:80"]
          WC["perfshop-welcome<br/>:3011"]
          DOC["docs<br/>:8000"]
        end

        subgraph o["Observability"]
          PROM["prometheus<br/>:9090"]
          GRAF["grafana<br/>:3000"]
          LOKI["perfshop-loki<br/>:3100"]
          PT["perfshop-promtail"]
          TEMPO["perfshop-tempo<br/>:3200/:4317/:4318"]
          PYRO["perfshop-pyroscope<br/>:4040"]
          OS["perfshop-opensearch<br/>:9200"]
          OSD["perfshop-opensearch-dashboards<br/>:5601"]
          VEC["perfshop-vector"]
        end

        subgraph q["QA"]
          JM["perfshop-jmeter"]
          JMUI["perfshop-jmeter-ui<br/>:3005"]
          SQDB[("perfshop-squash-db<br/>:5432")]
          STM["perfshop-testmgmt<br/>:8080"]
          ORCH["perfshop-orchestrator"]
          SEL["perfshop-selenium<br/>:4444 + :7900"]
          TR["perfshop-test-runner<br/>:22"]
          FG["perfshop-forgejo<br/>:3000"]
          SUI["perfshop-scripts-ui<br/>:3008"]
        end
      end
    end

    rp -->|HTTPS terminating| bridge
  end

  user["Browser"] -->|HTTPS| rp
  user -.-|Direct HTTP if no reverse proxy| bridge

Characteristics:

  • Standard bridge driver: no host or macvlan mode. All containers have a private IP allocated by Docker in the bridge subnet.
  • Automatic internal DNS: each container is reachable from the others via its container_name (which matches the Compose service name). Example: perfshop-app can reach the DB via jdbc:mysql://perfshop-db:3306/perfshop.
  • No direct cross-host communication: the stack is single-host. No cluster discovery, no service mesh.
  • All services share the same network: this is intentional. No internal network segmentation between groups (core / obs / QA) — the separation is logical, not network-level.

Ports exposed to the host

All ports are configurable via environment variables listed in the right column. The default values are those of the .env.example file.

Service Host port Container port Env variable Usage
perfshop-db 19306 3306 DB_PORT External MySQL connection (debug) — ⚠️ port 3306 reserved by Hyper-V/WSL2 on Windows
perfshop-app 8080 8080 BACKEND_HTTP_PORT Main REST API
perfshop-app 9090 9090 BACKEND_METRICS_PORT Spring Boot Actuator (/actuator/prometheus, /actuator/health, /actuator/heapdump)
perfshop-frontend 8090 80 FRONTEND_HTTP_PORT React shop (nginx)
perfshop-monitoring 3001 3001 MONITORING_HTTP_PORT Real-time HTML dashboard
perfshop-admin 3004 80 ADMIN_HTTP_PORT Products/orders backoffice
perfshop-chaos-admin 3003 80 CHAOS_HTTP_PORT Chaos control panel
perfshop-welcome 3011 3011 (fixed) Directory page
docs 8087 8000 DOCS_HTTP_PORT MkDocs documentation
prometheus 9091 9090 PROMETHEUS_HTTP_PORT Prometheus UI + scrape endpoint
grafana 3002 3000 GRAFANA_HTTP_PORT Grafana UI
perfshop-loki 19100 3100 LOKI_HTTP_PORT Loki API (push + query) — ⚠️ port 3100 reserved by Hyper-V/WSL2 on Windows
perfshop-tempo 19200 3200 TEMPO_HTTP_PORT Tempo HTTP query API — ⚠️ port 3200 reserved by Hyper-V/WSL2 on Windows
perfshop-tempo 4317 4317 TEMPO_OTLP_GRPC_PORT OTLP gRPC (traces export from the Java agent)
perfshop-tempo 4318 4318 TEMPO_OTLP_HTTP_PORT OTLP HTTP (alternative)
perfshop-pyroscope 4040 4040 PYROSCOPE_HTTP_PORT Pyroscope API (push + query)
perfshop-opensearch 9201 9200 OPENSEARCH_API_PORT OpenSearch REST API
perfshop-opensearch-dashboards 5601 5601 OPENSEARCH_HTTP_PORT OpenSearch Dashboards UI
perfshop-jmeter-ui 3005 3005 JMETER_UI_PORT Custom JMeter UI
perfshop-squash-db 5433 5432 SQUASH_DB_PORT Squash PostgreSQL (debug)
perfshop-testmgmt 8088 8080 SQUASH_HTTP_PORT Squash TM
perfshop-selenium 4444 4444 SELENIUM_HTTP_PORT Selenium WebDriver
perfshop-selenium 7900 7900 SELENIUM_VNC_PORT noVNC (visual debug for Chrome sessions)
perfshop-forgejo 3009 3000 FORGEJO_HTTP_PORT Forgejo UI + REST API
perfshop-scripts-ui 3008 3008 SCRIPTS_UI_PORT Scripts UI

Port conflicts

If one of these default ports is already in use on the host machine (for example 3306 for another MySQL instance), simply override the corresponding variable in the .env. All internal references use Docker DNS names, so these variables only impact the host-side mapping.

Inter-service communication (Docker DNS)

Services talk to each other by container name, never by IP. Here are the main communication chains.

Backend → DB and observability

flowchart LR
  APP["perfshop-app"]
  DB[("perfshop-db:3306")]
  TEMPO["perfshop-tempo:4317<br/>(OTLP gRPC)"]
  PYRO["perfshop-pyroscope:4040<br/>(JFR push)"]

  APP -->|jdbc:mysql://perfshop-db:3306/perfshop| DB
  APP -->|OTLP gRPC| TEMPO
  APP -->|JFR HTTP push| PYRO

Prometheus → scraped targets

flowchart LR
  PROM["prometheus"]
  APP["perfshop-app:9090<br/>/actuator/prometheus"]
  MON["perfshop-monitoring:3001<br/>/metrics"]
  JM["perfshop-jmeter:9270<br/>(active during a test run)"]

  PROM -->|scrape 5s| APP
  PROM -->|scrape 5s| MON
  PROM -->|scrape 5s| JM

Grafana → datasources

flowchart LR
  GRAF["grafana"]
  PROM["http://prometheus:9090"]
  LOKI["http://perfshop-loki:3100"]
  TEMPO["http://perfshop-tempo:3200"]
  PYRO["http://perfshop-pyroscope:4040"]

  GRAF --> PROM
  GRAF --> LOKI
  GRAF --> TEMPO
  GRAF --> PYRO

Frontend / admin / chaos-admin → backend

All HTML/JS UIs call the backend from the browser (cross-domain origins), so:

  • The URL called is the public URL of the backend (PUBLIC_API_URL variable, e.g. http://localhost:8080 or https://perfshop-api.example.com).
  • The backend allows these origins via CORS_ALLOWED_ORIGINS, computed dynamically from the PUBLIC_*_URL variables.
  • The frontend nginx does not act as a proxy to the backend: it only serves static files. API calls go directly from the browser to perfshop-app.
flowchart LR
  B["Student<br/>browser"]
  FE["perfshop-frontend<br/>(static HTML/JS)"]
  BE["perfshop-app<br/>(REST API)"]

  B -->|GET /index.html| FE
  FE -->|loads JS bundle| B
  B -->|fetch '/api/products'| BE

  style FE fill:#fff,stroke:#888
  style BE fill:#fff,stroke:#888

QA stack — control chains

flowchart LR
  SUI["perfshop-scripts-ui"]
  FG["perfshop-forgejo:3000"]
  TR["perfshop-test-runner:22<br/>(SSH)"]
  SEL["perfshop-selenium:4444"]
  STM["perfshop-testmgmt:8080"]
  ORCH["perfshop-orchestrator"]

  SUI -->|Git push API| FG
  SUI -->|docker exec via socket| TR
  TR -->|WebDriver| SEL

  STM -->|SCM polling| FG
  ORCH -->|REST| STM
  ORCH -->|SSH with public key| TR
  TR -->|WebDriver| SEL

JMeter UI → JMeter container

perfshop-jmeter-ui does not use DNS resolution to drive JMeter: it mounts the Docker socket (/var/run/docker.sock) and runs docker exec perfshop-jmeter .... This is an out-of-network control channel that lets test runs be launched without having to expose a JMeter control server.

flowchart LR
  JMUI["perfshop-jmeter-ui<br/>(node.js)"]
  SOCK["/var/run/docker.sock"]
  JM["perfshop-jmeter<br/>(idle, tail -f /dev/null)"]

  JMUI -->|mounts| SOCK
  SOCK -.->|docker exec<br/>jmeter -n -t ...| JM

External reverse proxy (public HTTPS)

To expose PerfShop publicly (hosted training, customer demo), an external reverse proxy is placed in front of the stack. PerfShop does not ship this reverse proxy: it is up to the operator to install and configure nginx, Caddy, Traefik, HAProxy, or any other.

flowchart LR
  internet["Students<br/>(browser)"]
  rp["Reverse proxy<br/>(nginx / Caddy / Traefik)<br/>:443 HTTPS"]
  net["perfshop-network<br/>(local HTTP ports)"]

  internet -->|"shop.example.com"| rp
  internet -->|"api.example.com"| rp
  internet -->|"grafana.example.com"| rp

  rp -->|"http://127.0.0.1:8090"| net
  rp -->|"http://127.0.0.1:8080"| net
  rp -->|"http://127.0.0.1:3002"| net

Subdomain conventions

If you want to follow a consistent convention, the naming used by the PUBLIC_*_URL variables of .env.example proposes:

Variable Typical subdomain Internal target
PUBLIC_FRONTEND_URL shop.<domain> perfshop-frontend:80 (host port 8090)
PUBLIC_API_URL api.<domain> perfshop-app:8080 (host port 8080)
PUBLIC_MONITORING_URL monitoring.<domain> perfshop-monitoring:3001
PUBLIC_GRAFANA_URL grafana.<domain> grafana:3000 (host port 3002)
PUBLIC_CHAOS_URL chaos.<domain> perfshop-chaos-admin:80 (host port 3003)
PUBLIC_ADMIN_URL admin.<domain> perfshop-admin:80 (host port 3004)
PUBLIC_DOCS_URL docs.<domain> docs:8000 (host port 8087)
PUBLIC_JMETER_URL jmeter.<domain> perfshop-jmeter-ui:3005
PUBLIC_SCRIPTS_URL scripts.<domain> perfshop-scripts-ui:3008
PUBLIC_FORGEJO_URL git.<domain> perfshop-forgejo:3000 (host port 3009)
PUBLIC_OPENSEARCH_URL logs.<domain> perfshop-opensearch-dashboards:5601
PUBLIC_SQUASH_URL squash.<domain> perfshop-testmgmt:8080 (host port 8088)
PUBLIC_SELENIUM_VNC_URL vnc.<domain> perfshop-selenium:7900

WebSockets and SSE

Several services use WebSockets or Server-Sent Events (Squash TM, noVNC, OpenSearch Dashboards, Forgejo when editing files). The reverse proxy must explicitly support Upgrade: websocket and disable buffering for these routes.

Propagation to the services

Once the PUBLIC_*_URL are set in the .env, they are propagated automatically:

  • Backend: CORS_ALLOWED_ORIGINS is composed from PUBLIC_FRONTEND_URL, PUBLIC_MONITORING_URL, PUBLIC_CHAOS_URL, PUBLIC_ADMIN_URL. That's what allows the frontend hosted on shop.example.com to call api.example.com.
  • React frontend: VITE_API_URL, VITE_MONITORING_URL are substituted into the bundled JS files at nginx container startup via the env-inject.sh script.
  • Monitoring Node: receives all PUBLIC_*_URL and injects them into window.__CONFIG__ on the browser side (see monitoring/src/server.js).
  • Welcome page: all PUBLIC_*_URL are consumed to generate the directory of links.
  • Grafana: GF_SERVER_ROOT_URL is set to PUBLIC_GRAFANA_URL so absolute links in emails and the API are correct.

Internal and external addresses — recap

Context URL used Example
Container → container (internal to the bridge) http://<service>:<container_port> http://perfshop-app:8080, http://perfshop-loki:3100
Browser → service (without reverse proxy) http://<host>:<host_port> http://localhost:8080, http://192.168.1.10:3002
Browser → service (with reverse proxy) https://<subdomain>.<domain> https://api.example.com, https://grafana.example.com
Backend → other backend always container name jdbc:mysql://perfshop-db:3306/perfshop
Java agent → Tempo / Pyroscope container name http://perfshop-tempo:4317, http://perfshop-pyroscope:4040

It is the explicit separation between public URLs and internal URLs that makes the stack portable: the same configuration can be migrated between Docker Desktop, a Linux VPS and any external reverse proxy without touching the application code, simply by adjusting the PUBLIC_*_URL variables.

To go further