Skip to content

feat: probe host ports before starting the dev environment - #1323

Draft
Soner (shyim) wants to merge 6 commits into
mainfrom
feat/dev-port-conflicts
Draft

feat: probe host ports before starting the dev environment#1323
Soner (shyim) wants to merge 6 commits into
mainfrom
feat/dev-port-conflicts

Conversation

@shyim

@shyim Soner (shyim) commented Aug 3, 2026

Copy link
Copy Markdown
Member

What changed?

project dev now checks whether the host ports the generated compose.yaml will publish are actually free before running docker compose up -d (ports already held by the project's own containers are excluded, so restarts never false-alarm).

On conflict:

  • TUI (project dev): a modal lists the busy ports and offers Use random free ports / Quit.
  • Headless (project dev start, or project dev without a TTY): new --on-port-conflict=fail|random flag. fail (default) aborts with a descriptive error, random remaps only the conflicting ports.

Chosen ports are persisted per-key into .shopware-project.local.yml (or .local.yaml) and reused on every later run. All published host ports are also directly configurable now, and false disables publishing a port entirely:

docker:
  ports:
    web: 8005      # remap
    adminer: false # don't publish at all

CLI output with port 8000 occupied (default fail mode):

ERROR  cannot start the development environment, host ports are already in use:
  Shop (Caddy) (web): port 8000 is already in use
rerun with --on-port-conflict=random to switch them to free ports, or set docker.ports in .shopware-project.yml

With --on-port-conflict=random:

  Shop (Caddy): port 8000 is in use, switched to 60636
  Mailpit UI: port 8025 is in use, switched to 60637
  Saved the new ports to .shopware-project.local.yml

Implementation notes:

  • New internal/docker/ports.go holds the single source of truth for all 12 published ports (web 8000/8080/9999/9998/5173/5773, adminer, mailpit, conditional lavinmq/opensearch), the availability probe, and the random allocator (all listeners held open until every port is picked, so no duplicates).
  • Persistence is a surgical yaml.Node read-modify-write (internal/shop/config_local.go) that preserves comments, ${VAR} references, and unrelated keys. The TUI profiler-secret save was migrated onto it; the old full-file-rewrite WriteLocalConfig (which would have clobbered the ports) is removed. WriteConfig strips docker.ports so machine-local ports never leak into the committed config.
  • Container-side ports never change, so service discovery keeps working; the hardcoded watcher URLs in the overview tab now come from the config.
  • Fixes two pre-existing config bugs this feature would immediately trigger: ReadConfig ignored .shopware-project.local.yml entirely when the base config file is missing, and the local-override merge corrupted unquoted compatibility_date values into RFC3339 timestamps (yaml.v3 !!timestamp resolution during the map[string]any round-trip).

Why?

The dev environment binds a lot of host ports and anything already listening (a second project, a stray php -S, another mailpit) made docker compose up fail with a raw error. Now conflicts are detected up front and can be resolved automatically, with the resolution remembered per machine without touching the committed config.

How was this tested?

  • go test ./... and golangci-lint run ./... are green; internal/shop/config_schema.json regenerated.
  • New unit tests: probe core (busy / own-container-excluded / override-probed / disabled-skipped / conditional services), random allocation (distinct + bindable), compose output (remapped, disabled, and default bindings; ports: key dropped when a service has none left), local-overlay persistence (creates 0600 file, preserves comments/${VAR}/secrets, .yaml naming, ReadConfig round-trip, unquoted-date regression), TUI lifecycle (conflict → prompt, quit, fix → start, fix error).
  • Manual end-to-end against a project with ports 8000/8025 actually occupied: fail mode error above, random mode remap + persisted overlay + regenerated compose (60636:8000), rerun with 8000 still busy correctly finds no conflict (override is probed instead), and adminer: false removes the ports: key from the service.

Related issue or discussion

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added configurable Docker host ports, including the ability to disable selected ports.
    • Added automatic detection of port conflicts when starting development services.
    • Added fail and random conflict-handling modes.
    • Randomly remapped conflicting ports and persisted overrides locally.
    • Updated development screens and watcher links to reflect configured ports.
  • Bug Fixes

    • Improved local configuration merging, validation, permissions, and date handling.
    • Prevented stale or unavailable service links from being displayed.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
📝 Walkthrough

Walkthrough

The change adds Docker host-port configuration, local override persistence, conflict detection, random port allocation, Compose regeneration, and CLI/TUI handling before development and migration startup.

Changes

Docker port management

Layer / File(s) Summary
Port configuration and local persistence
internal/shop/config.go, internal/shop/config_docker_ports.go, internal/shop/config_local.go, internal/shop/config_override.go, internal/shop/config_schema.json, internal/shop/config_local_test.go
Docker ports accept host-port numbers or false. Local overrides are merged, validated, written with mode 0600, and excluded from committed configuration.
Compose port bindings
internal/docker/compose.go, internal/docker/compose_test.go
Compose generation applies configured host ports to supported services and omits disabled bindings.
Conflict detection and random allocation
internal/docker/ports.go, internal/docker/ports_test.go
Port scanning identifies active service conflicts, excludes project-owned bindings, probes TCP availability, and allocates unique replacement ports.
Startup conflict resolution
cmd/project/project_dev.go, internal/tui/dev/*
Development and migration startup checks ports before launching containers. The CLI supports fail and random modes. The TUI displays conflicts, persists selected replacements, regenerates Compose configuration, and restarts startup. Watcher URLs use configured Docker ports.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Developer
  participant DevelopmentCLI
  participant DockerPortScanner
  participant PortAllocator
  participant ComposeGenerator
  participant LocalConfig
  Developer->>DevelopmentCLI: start development environment
  DevelopmentCLI->>DockerPortScanner: find port conflicts
  DockerPortScanner-->>DevelopmentCLI: return conflicts
  DevelopmentCLI->>PortAllocator: allocate random replacements
  PortAllocator-->>DevelopmentCLI: return port map
  DevelopmentCLI->>ComposeGenerator: regenerate compose.yaml
  DevelopmentCLI->>LocalConfig: persist Docker port overrides
  DevelopmentCLI-->>Developer: start containers or report conflicts
Loading

Possibly related issues

  • Issue 939: The change implements Docker port overrides, collision detection, alternative-port allocation, and user-facing handling for parallel Shopware projects.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.92% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the main change: probing host ports before starting the development environment.
Description check ✅ Passed The description covers all required sections and provides detailed behavior, examples, testing results, and related issue information.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/dev-port-conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 5

🧹 Nitpick comments (6)
internal/shop/config_local_test.go (1)

79-118: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use t.Context() instead of context.Background().

The coding guidelines require t.Context() for test contexts. The sibling file internal/shop/config_test.go already follows this. This file uses context.Background() at lines 86, 98, 115, 202, and 234.

After replacing all five call sites, remove the now-unused context import at line 4.

♻️ Proposed change for the two call sites in this range
-	cfg, err := ReadConfig(context.Background(), configPath, false)
+	cfg, err := ReadConfig(t.Context(), configPath, false)
 	require.NoError(t, err)
 	require.NotNil(t, cfg.Docker)
 	assert.Equal(t, ConfigDockerPort(52341), cfg.Docker.Ports[DockerPortWeb])
-	cfg, err := ReadConfig(context.Background(), configPath, true)
+	cfg, err := ReadConfig(t.Context(), configPath, true)

As per coding guidelines: "use t.Context() for test contexts".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/shop/config_local_test.go` around lines 79 - 118, Replace every
context.Background() call in the tests of this file, including the calls in
TestUpdateLocalDockerPortsRoundTripsThroughReadConfig and
TestReadConfigMergesLocalFileWithoutBaseConfig, with the corresponding test’s
t.Context(). Apply the same change to the other three call sites, then remove
the unused context import.

Source: Coding guidelines

internal/docker/compose_test.go (1)

164-169: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider parsing the YAML instead of slicing the compose text.

strings.Cut(compose, "adminer:") matches the first occurrence of the literal adminer:. If a future compose template emits adminer: earlier, for example as a mapping key inside a depends_on block on the web service, the slice would cover the wrong region and the assertion would pass without testing the adminer service.

Unmarshaling the generated file and reading services.adminer.ports keeps the assertion tied to the intended node.

♻️ Proposed change
-		// A service whose ports are all disabled loses its ports key entirely.
-		_, adminerSection, found := strings.Cut(compose, "adminer:")
-		assert.True(t, found)
-		adminerSection, _, found = strings.Cut(adminerSection, "mailer:")
-		assert.True(t, found)
-		assert.NotContains(t, adminerSection, "ports:")
+		// A service whose ports are all disabled loses its ports key entirely.
+		var parsed struct {
+			Services map[string]struct {
+				Ports []string `yaml:"ports"`
+			} `yaml:"services"`
+		}
+		assert.NoError(t, yaml.Unmarshal(result, &parsed))
+		assert.Empty(t, parsed.Services["adminer"].Ports)

This also removes the need for the strings import if no other assertion uses it.

Run the following script to confirm the current compose layout and whether adminer: can appear before the service definition:

#!/bin/bash
# Description: Inspect the compose generation template for depends_on usage and service ordering.
set -euo pipefail

fd -t f 'compose' internal/docker

# Map the compose generator.
ast-grep outline internal/docker/compose.go --items all

# Look for depends_on rendering and the adminer service block.
rg -n -C 6 'depends_on|adminer' internal/docker --type=go -g '!*_test.go'

# Check for an embedded compose template file.
fd -e tmpl -e yaml -e yml . internal/docker --exec rg -n -C 4 'adminer|depends_on' {}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/docker/compose_test.go` around lines 164 - 169, Replace the text
slicing around the generated compose output in the relevant test with YAML
unmarshaling, then inspect the parsed services.adminer node and assert that its
ports field is absent. Remove the strings import if it is no longer used, while
preserving the existing validation that the adminer service exists.
cmd/project/project_dev.go (1)

222-228: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use e.configPath for consistency.

Line 227 reads the package-level projectConfigPath, while lines 243 and 250 in the same method read e.configPath. Both hold the same value today, because line 194 assigns projectConfigPath to the field. Reading one source in the whole method prevents the error message from pointing at a different file than the one the method writes.

♻️ Proposed change
-		return fmt.Errorf("cannot start the development environment, host ports are already in use:\n%s\nrerun with --on-port-conflict=random to switch them to free ports, or set docker.ports in %s", strings.Join(lines, "\n"), projectConfigPath)
+		return fmt.Errorf("cannot start the development environment, host ports are already in use:\n%s\nrerun with --on-port-conflict=random to switch them to free ports, or set docker.ports in %s", strings.Join(lines, "\n"), e.configPath)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/project/project_dev.go` around lines 222 - 228, Update the error message
in the port-conflict handling branch of the enclosing method to use the method’s
existing e.configPath field instead of the package-level projectConfigPath
variable, matching the references used by the later branches.
internal/tui/dev/lifecycle_test.go (1)

358-363: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use require.NoError in the setup helper.

writeMinimalComposeProject establishes a precondition. If the composer.lock write fails, assert.NoError records the failure but lets the test continue. fixPortConflicts then fails for a second, unrelated reason, which hides the root cause.

♻️ Proposed change
 func writeMinimalComposeProject(t *testing.T, dir string) {
 	t.Helper()
 	lock := `{"packages": [{"name": "shopware/core", "version": "6.6.0.0"}], "packages-dev": []}`
-	assert.NoError(t, os.WriteFile(filepath.Join(dir, "composer.lock"), []byte(lock), 0o644))
+	require.NoError(t, os.WriteFile(filepath.Join(dir, "composer.lock"), []byte(lock), 0o644))
 }

Add "github.com/stretchr/testify/require" to the import block.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/dev/lifecycle_test.go` around lines 358 - 363, Update
writeMinimalComposeProject to use require.NoError for the os.WriteFile result,
and add the testify/require import so setup failures stop the test immediately
before dependent logic runs.
internal/shop/config_local.go (1)

109-142: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Derive the credential key list from one source.

The credential keys appear twice: in the secrets map literal and in the loop slice. A future key must be added in both places, otherwise it is written but never cleared.

♻️ Proposed dedupe
-	secrets := map[string]string{
-		"blackfire_server_id":    "",
-		"blackfire_server_token": "",
-		"tideways_api_key":       "",
-	}
-	if php != nil {
-		secrets["blackfire_server_id"] = php.BlackfireServerID
-		secrets["blackfire_server_token"] = php.BlackfireServerToken
-		secrets["tideways_api_key"] = php.TidewaysAPIKey
-	}
+	type credential struct {
+		key   string
+		value string
+	}
+	credentials := []credential{
+		{key: "blackfire_server_id"},
+		{key: "blackfire_server_token"},
+		{key: "tideways_api_key"},
+	}
+	if php != nil {
+		credentials[0].value = php.BlackfireServerID
+		credentials[1].value = php.BlackfireServerToken
+		credentials[2].value = php.TidewaysAPIKey
+	}

Then iterate credentials in the mutate function.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/shop/config_local.go` around lines 109 - 142, Define a single
credential key list, such as credentials, and use it to initialize or populate
the secrets map and to drive the mutation loop in updateLocalConfig. Remove the
duplicated inline key slice while preserving the existing set-or-delete behavior
for each credential.
internal/docker/ports_test.go (1)

63-72: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename busy to match what it returns.

busy returns an isFree predicate. The call busy(8000) reads as "8000 is busy", but the returned function reports false for 8000. The inversion inside the closure makes the helper harder to follow.

♻️ Proposed rename
-	busy := func(ports ...int) func(int) bool {
+	// freeExcept returns an isFree predicate that reports the given ports as busy.
+	freeExcept := func(ports ...int) func(int) bool {

Update the call sites accordingly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/docker/ports_test.go` around lines 63 - 72, Rename the test helper
busy to isFree to reflect that its returned predicate reports whether a port is
available, and update every call site accordingly. Preserve the existing
predicate behavior and port-set logic while removing the misleading busy naming.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/docker/ports.go`:
- Around line 156-163: Update ownPublishedPorts to determine the Compose project
name from the inherited COMPOSE_PROJECT_NAME or
shop.ReadComposeProjectName(projectFolder) and pass it to docker compose ps via
-p. Also update the full FindPortConflicts probe, including ownPublishedPorts
and port checks, to run under a bounded timeout even when called with
context.Background().

In `@internal/shop/config_local.go`:
- Around line 53-63: Update the local configuration write flow around
os.WriteFile to write the secret content to a same-directory temporary file with
mode 0600, then atomically rename it over localFile. Add the required filepath
handling, ensure temporary files are cleaned up on failure, and preserve the
existing wrapped errors for write and rename failures.

In `@internal/shop/config_override.go`:
- Around line 220-238: Update normalizeTimestampValue for time.Time values to
determine date-only status from the value’s local wall-clock fields, replacing
the v.Truncate comparison. Only format as time.DateOnly when the local hour,
minute, second, and nanosecond are all zero; otherwise preserve the RFC3339
formatting.

In `@internal/tui/dev/lifecycle_test.go`:
- Around line 341-344: Update the persistence assertion near the local override
file read in the lifecycle test to verify the allocated web port value from
result.overrides[shop.DockerPortWeb], rather than checking only for the
ambiguous "web:" key. Keep the existing file-read and error assertion, and
ensure the persisted content must contain the expected port value.

In `@internal/tui/dev/lifecycle.go`:
- Around line 128-131: Guard m.config before calling SetDockerPortOverrides in
the update path around startContainers: ensure a nil config is initialized or
handled safely before applying non-empty msg.overrides, matching the nil-config
behavior permitted by fixPortConflicts. Preserve the existing override
application and container-start flow when m.config is available.

---

Nitpick comments:
In `@cmd/project/project_dev.go`:
- Around line 222-228: Update the error message in the port-conflict handling
branch of the enclosing method to use the method’s existing e.configPath field
instead of the package-level projectConfigPath variable, matching the references
used by the later branches.

In `@internal/docker/compose_test.go`:
- Around line 164-169: Replace the text slicing around the generated compose
output in the relevant test with YAML unmarshaling, then inspect the parsed
services.adminer node and assert that its ports field is absent. Remove the
strings import if it is no longer used, while preserving the existing validation
that the adminer service exists.

In `@internal/docker/ports_test.go`:
- Around line 63-72: Rename the test helper busy to isFree to reflect that its
returned predicate reports whether a port is available, and update every call
site accordingly. Preserve the existing predicate behavior and port-set logic
while removing the misleading busy naming.

In `@internal/shop/config_local_test.go`:
- Around line 79-118: Replace every context.Background() call in the tests of
this file, including the calls in
TestUpdateLocalDockerPortsRoundTripsThroughReadConfig and
TestReadConfigMergesLocalFileWithoutBaseConfig, with the corresponding test’s
t.Context(). Apply the same change to the other three call sites, then remove
the unused context import.

In `@internal/shop/config_local.go`:
- Around line 109-142: Define a single credential key list, such as credentials,
and use it to initialize or populate the secrets map and to drive the mutation
loop in updateLocalConfig. Remove the duplicated inline key slice while
preserving the existing set-or-delete behavior for each credential.

In `@internal/tui/dev/lifecycle_test.go`:
- Around line 358-363: Update writeMinimalComposeProject to use require.NoError
for the os.WriteFile result, and add the testify/require import so setup
failures stop the test immediately before dependent logic runs.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 58406773-933a-4217-8b67-08e4eaf65d94

📥 Commits

Reviewing files that changed from the base of the PR and between 297dc86 and 6ef77d0.

📒 Files selected for processing (21)
  • cmd/project/project_dev.go
  • internal/docker/compose.go
  • internal/docker/compose_test.go
  • internal/docker/ports.go
  • internal/docker/ports_test.go
  • internal/shop/config.go
  • internal/shop/config_docker_ports.go
  • internal/shop/config_local.go
  • internal/shop/config_local_test.go
  • internal/shop/config_override.go
  • internal/shop/config_schema.json
  • internal/tui/dev/lifecycle.go
  • internal/tui/dev/lifecycle_test.go
  • internal/tui/dev/model.go
  • internal/tui/dev/model_commands.go
  • internal/tui/dev/model_test.go
  • internal/tui/dev/model_update.go
  • internal/tui/dev/model_view.go
  • internal/tui/dev/overlay_port_conflict.go
  • internal/tui/dev/tab_overview.go
  • internal/tui/dev/tab_overview_test.go

Comment thread internal/docker/ports.go
Comment thread internal/shop/config_local.go Outdated
Comment thread internal/shop/config_override.go
Comment thread internal/tui/dev/lifecycle_test.go Outdated
Comment thread internal/tui/dev/lifecycle.go Outdated
@lasomethingsomething

Copy link
Copy Markdown
Contributor

Waiting until the local proxy is merged, then it needs a rebase

Base automatically changed from next to main August 20, 2026 11:34
@shyim
Soner (shyim) force-pushed the feat/dev-port-conflicts branch from 6ef77d0 to eadc678 Compare August 21, 2026 04:34
@codecov-commenter

Codecov Comments Bot (codecov-commenter) commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.46975% with 242 lines in your changes missing coverage. Please review.
✅ Project coverage is 63.31%. Comparing base (9010046) to head (051aecd).

Files with missing lines Patch % Lines
cmd/project/project_dev.go 16.66% 40 Missing ⚠️
internal/shop/config_local.go 78.94% 28 Missing ⚠️
internal/docker/port.go 3.70% 26 Missing ⚠️
internal/shop/config_docker_services.go 72.60% 20 Missing ⚠️
cmd/project/project_proxy.go 0.00% 19 Missing ⚠️
internal/tui/dev/model_commands.go 40.00% 15 Missing ⚠️
internal/docker/discover.go 74.07% 14 Missing ⚠️
internal/tui/dev/model_update.go 20.00% 12 Missing ⚠️
internal/shop/config.go 74.28% 9 Missing ⚠️
internal/tui/dev/model.go 59.09% 9 Missing ⚠️
... and 18 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1323      +/-   ##
==========================================
+ Coverage   62.72%   63.31%   +0.58%     
==========================================
  Files         435      455      +20     
  Lines       29476    30087     +611     
==========================================
+ Hits        18490    19049     +559     
- Misses      10986    11038      +52     
Flag Coverage Δ
go-test 63.31% <78.46%> (+0.58%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@shyim
Soner (shyim) force-pushed the feat/dev-port-conflicts branch 2 times, most recently from b075c23 to b716a7e Compare August 21, 2026 08:15
@shyim

Copy link
Copy Markdown
Member Author

i would wait on #1427

@shyim
Soner (shyim) force-pushed the feat/dev-port-conflicts branch from 1477488 to da070ba Compare August 31, 2026 08:08
Soner (shyim) and others added 6 commits September 2, 2026 13:19
- Add docker.ports config (int|false) with machine-local overrides
  persisted to .shopware-project.local.yml (stripped from the committed
  config by WriteConfig)
- Probe published host ports before starting, excluding own containers;
  new --on-port-conflict flag (fail|random) on project dev
- Add Bubbletea port-conflict overlay in the dev TUI offering random
  re-allocation or quit
- Skip probing for proxy-mode projects (no host ports published); keep
  probing after proxy fallback and write fixed-port compose in that case
  so the fallback is never silently undone
Service, port, and URL knowledge was scattered across compose generation,
port-conflict detection, proxy listing, and the dev TUI, so every new
service meant touching half a dozen files. Introduce a single service
catalog in internal/docker that defines each service's endpoints, default
ports, proxy subdomains, credentials, and feature gate, and derive port
definitions, proxy routes, access URLs, and the TUI overview from it.

Environment discovery (docker compose ps parsing and container
classification) moves from the TUI into the docker package next to the
catalog, sharing one compose invocation and parser with port-conflict
detection. The duplicated proxyHostname helpers are replaced by
proxy.RegisteredHostname, and rustfs ports now participate in conflict
detection and port overrides like every other service.

Co-authored-by: Cursor <cursoragent@cursor.com>
@shyim
Soner (shyim) force-pushed the feat/dev-port-conflicts branch from f532390 to 051aecd Compare September 2, 2026 11:37
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