# How Dream Server Handles Port Conflicts When Merging Docker Compose Files

> Dream Server actively prevents Docker Compose port conflicts with pre-runtime validation and environment-driven substitution. Learn how Dream Server ensures seamless merges.

- Repository: [Light Heart Labs/DreamServer](https://github.com/Light-Heart-Labs/DreamServer)
- Tags: how-to-guide
- Published: 2026-05-18

---

**Dream Server prevents port conflicts by validating host-port assignments across layered compose fragments before runtime, using environment-driven parameter substitution and a dedicated resolution script that aborts on collision detection.**

Light-Heart-Labs/DreamServer orchestrates complex container stacks by merging a base composition with GPU overlays and extension-provided fragments. To ensure that no two services claim the same host interface, the project implements a deterministic **port contract validation** system that intercepts conflicts during the build phase rather than at runtime.

## The Multi-Layer Compose Architecture

Dream Server constructs its final Docker Compose stack through a hierarchical merge process. The foundation rests on [`docker-compose.base.yml`](https://github.com/Light-Heart-Labs/DreamServer/blob/main/docker-compose.base.yml), which is then overlaid with optional GPU configurations and any extension fragments found in `extensions/services/*/compose.yaml`.

Each service declares its network requirements using parameterized port mappings (e.g., `${WEBUI_PORT:-3000}`) rather than hardcoded integers. These parameters pull values from the generated `.env` file, where defaults are centrally defined in `.env.example`. This architecture ensures that every service’s host binding remains configurable while maintaining a single source of truth for port allocation.

## The Port Contract Validation Pipeline

Before executing any `docker compose` command, the helper script **[`scripts/resolve-compose-stack.sh`](https://github.com/Light-Heart-Labs/DreamServer/blob/main/scripts/resolve-compose-stack.sh)** performs a deterministic validation sweep across all resolved compose files. This pipeline enforces three critical safety guarantees:

### Canonical Port Definitions via Environment Variables

Every extension and base service defines its exposed ports using shell parameter expansion syntax. For example, the dashboard service might declare:

```yaml
ports:
  - "${DASHBOARD_PORT:-3000}:3000"

```

The default values for these variables reside in `.env.example`, allowing the resolution script to substitute values at compose-merge time rather than runtime. This approach ensures that port values are resolved exactly once, creating a static configuration that can be inspected and validated before containers start.

### Conflict Detection and Resolution

The resolution script scans all `ports:` entries across the merged compose configuration and builds a mapping of **host-port → service** pairs. If the same host port appears for two distinct services, the script aborts immediately with a descriptive error indicating the colliding services and the offending port number.

This logic is continuously verified by the test suite **[`dream-server/tests/test-port-contracts.sh`](https://github.com/Light-Heart-Labs/DreamServer/blob/main/dream-server/tests/test-port-contracts.sh)**, which simulates conflicting configurations to ensure the validation layer catches errors before they reach Docker Engine.

### Localhost Binding Enforcement

Beyond port uniqueness, Dream Server forces all services to bind exclusively to `127.0.0.1`. This constraint prevents accidental exposure of development services on external network interfaces and isolates the stack to the host loopback. The binding behavior is validated by **[`dream-server/tests/test-bind-address-sweep.sh`](https://github.com/Light-Heart-Labs/DreamServer/blob/main/dream-server/tests/test-bind-address-sweep.sh)**, which verifies that no service attaches to `0.0.0.0` or specific external IPs.

## Practical Configuration Examples

### Overriding Ports at Install Time

When the default port allocations conflict with existing system services, users can override values via environment variables at install time. The resolution script picks up these changes during the next validation pass:

```bash

# Change the dashboard UI port from default 3000 to 9090

WEBUI_PORT=9090 ./install.sh

```

### Detecting Collisions During Development

To manually trigger the validation layer without running a full install, invoke the resolution script directly:

```bash

# Resolve the final compose file and abort if any host-port collides

scripts/resolve-compose-stack.sh

```

### Inspecting Current Port Assignments

To view the currently configured port values (with secrets masked), use the CLI utility:

```bash

# Show the generated environment configuration

dream config show

```

### Example Conflict Error

If two services request the same host port, the resolution script produces an error similar to:

```

ERROR: Port conflict detected – services "dashboard" and "open-webui" both request host port 3000.
Fix by setting distinct values in .env (e.g. DASHBOARD_PORT=3000, WEBUI_PORT=3001) and re-run the installer.

```

## Summary

- **Deterministic defaults** are stored in `.env.example` and injected via parameter substitution (e.g., `${WEBUI_PORT:-3000}`) across all compose fragments.
- **Pre-runtime validation** occurs in [`scripts/resolve-compose-stack.sh`](https://github.com/Light-Heart-Labs/DreamServer/blob/main/scripts/resolve-compose-stack.sh), which builds a host-port map and aborts on collision before Docker receives the final configuration.
- **Loopback isolation** is enforced by binding all services to `127.0.0.1`, preventing external interface conflicts as verified by [`test-bind-address-sweep.sh`](https://github.com/Light-Heart-Labs/DreamServer/blob/main/test-bind-address-sweep.sh).
- **Environment overrides** allow users to resolve conflicts ad-hoc by setting variables (e.g., `WEBUI_PORT=9090`) before running [`./install.sh`](https://github.com/Light-Heart-Labs/DreamServer/blob/main/./install.sh).
- **Automated testing** via [`test-port-contracts.sh`](https://github.com/Light-Heart-Labs/DreamServer/blob/main/test-port-contracts.sh) ensures the conflict detection logic remains robust against regression.

## Frequently Asked Questions

### How does Dream Server detect port conflicts before containers start?

Dream Server runs [`scripts/resolve-compose-stack.sh`](https://github.com/Light-Heart-Labs/DreamServer/blob/main/scripts/resolve-compose-stack.sh) during the installation process. This script merges all compose fragments (base, GPU overlays, and extensions), substitutes environment variables, and scans the resulting configuration for duplicate host ports. If it finds the same port assigned to two different services, it aborts with an error message before invoking Docker.

### Can I change port numbers after the initial installation?

Yes. Modify the corresponding variable in your `.env` file (e.g., change `WEBUI_PORT=3000` to `WEBUI_PORT=9090`) and re-run [`./install.sh`](https://github.com/Light-Heart-Labs/DreamServer/blob/main/./install.sh). The resolution script will re-validate the entire stack with the new values, ensuring no new conflicts are introduced by your changes.

### Why does Dream Server force all services to bind to 127.0.0.1?

Binding to the loopback interface (`127.0.0.1`) instead of `0.0.0.0` prevents services from accidentally exposing ports on external network interfaces. This security measure, enforced by [`test-bind-address-sweep.sh`](https://github.com/Light-Heart-Labs/DreamServer/blob/main/test-bind-address-sweep.sh), ensures that even if a port conflict occurred on the host's external IP, it would not affect the Dream Server stack or vice versa.

### What files should I check if I encounter a port conflict error?

First, review `.env.example` to see the default port assignments and their variable names. Then check [`scripts/resolve-compose-stack.sh`](https://github.com/Light-Heart-Labs/DreamServer/blob/main/scripts/resolve-compose-stack.sh) to understand how the merge logic processes your specific extensions. Finally, consult `extensions/services/*/manifest.yaml` and [`compose.yaml`](https://github.com/Light-Heart-Labs/DreamServer/blob/main/compose.yaml) to identify which extensions are requesting the conflicting port numbers.