LPM CLI

Port management

Understand how LPM CLI assigns ports, handles conflicts, and connects development services.

Port management keeps a multi-service development run usable during a preferred-port conflict.

LPM CLI assigns final service ports, saves local replacements, injects peer addresses, and selects the browser-facing endpoint.

Declare the project topology in lpm.json. Use lpm ports to inspect or reset machine-local port state.

Understand the port types

One development run can use four types of ports:

Port typePurpose
Preferred portThe services.<name>.port value in lpm.json
Assigned portThe port that the child service uses after conflict handling
Public portThe browser-facing HTTPS, proxy, or network listener
Readiness targetThe readyPort or readyUrl used during startup

These ports can have the same number in a simple HTTP project. HTTPS and proxy workflows often use separate public and child ports.

Configure services in lpm.json

Define each development service below services:

lpm.json
{
  "$schema": "https://cli.lpm.dev/schemas/lpm.json",
  "services": {
    "db": {
      "command": "docker compose up postgres",
      "readyPort": 5432,
      "readyTimeout": 60
    },
    "api": {
      "command": "node api.js",
      "port": 4000,
      "readyUrl": "http://localhost:4000/health",
      "dependsOn": ["db"],
      "env": {
        "DATABASE_URL": "postgres://localhost:5432/app"
      }
    },
    "web": {
      "command": "vite",
      "port": 5173,
      "dependsOn": ["api"],
      "primary": true
    }
  }
}

Start the complete service graph:

lpm dev

The example starts db, then api, then web. LPM CLI starts independent services in the same dependency level together.

The Docker daemon owns the database listener. readyPort checks that external port, and DATABASE_URL gives the API its connection address.

FieldPurpose
portPreferred listener port for the service
readyPortSeparate TCP readiness target
readyUrlHTTP readiness target that must return a 2xx response
readyTimeoutShared startup deadline, from 1 to 3600 seconds. The default is 30.
dependsOnServices that enter their ready state before this service starts
primaryService that receives browser, HTTPS, network, and tunnel handling
hostFriendly hostname that routes to the final assigned port
restartRestart the service after a non-zero exit
cwdWorking directory relative to the project root

See the lpm.json service reference for every field.

Assign service ports

LPM CLI assigns ports before it starts the service commands. A service gets a managed port in these cases:

  • The service has a port value.
  • The service explicitly sets primary: true.
  • The sole service runs a recognized development server or has a command-level --port option.
  • A primary endpoint is requested through CLI options or proxy.host.
  • The service has a host value.

A numeric command-level port is the fallback for the primary service. CLI overrides, saved replacements, and port take precedence.

If a command hard-codes a numeric port, a different assignment causes a conflict error. Use $PORT or remove the command-level option to permit reassignment.

If no preferred value exists, automatic assignment starts at 3000. A service with host also receives a managed port.

A generic worker with no endpoint configuration stays portless, even if an old saved assignment exists.

LPM CLI passes the final port through PORT. It also adds framework-specific port arguments where the framework requires them.

For example, Vite receives a strict assigned port. Next.js, Nuxt, SvelteKit, Remix, and Astro receive their supported port argument.

A service with only readyPort does not receive a managed service port. The readiness target can belong to an external service.

For a container listener, use readyPort or readyUrl. Use port when the launched process or its descendants own the listener.

Replace a busy port

If a preferred port is busy, LPM CLI selects the next available port. It reports available process-owner information.

For example, a preferred port of 5173 can become 5174:

web → :5174 (port 5173 in use by another process)

LPM CLI does not edit lpm.json and does not stop the existing process. It saves the replacement for this project in:

~/.lpm/ports.toml

With LPM_HOME set, the file is $LPM_HOME/ports.toml.

The next lpm dev run tries the saved replacement before the preferred port. The service can continue to use 5174 after 5173 becomes free.

Concurrent lpm dev runs reserve assigned ports. This reservation prevents two LPM CLI processes from selecting the same replacement.

Return to the preferred port

Inspect the preferred port before you stop its owner:

lpm ports inspect 5173

If the owner can stop safely, stop it and clear the saved replacement:

lpm ports kill 5173
lpm ports reset
lpm dev

lpm ports reset clears only the saved replacements for the project. Like lpm dev, it acts on the nearest directory with a package.json or an lpm.json, so you can run it from a subdirectory. It does not stop processes or edit lpm.json.

If the preferred port remains busy, the next development run selects another replacement.

Choose the primary service

The primary service provides the main development endpoint. It receives these features:

  • Browser-open handling
  • Local HTTPS
  • Local network access
  • Tunnel forwarding
  • Top-level proxy.host routing

A project with one service selects that service for primary features. A generic worker does not receive a managed port automatically.

For a generic server that reads PORT, set primary: true or port. Recognized development-server commands receive a managed port automatically.

If a multi-service project uses a main public endpoint, set primary: true on one service.

These options require one primary service in a multi-service project:

  • --port
  • --https
  • --network
  • --tunnel

More than one primary service causes an error.

Separate public and child ports

Without HTTPS, --port requests the assigned port for the primary child service:

lpm dev --port 4000

With HTTPS, the same value belongs to the browser-facing TLS frontend:

lpm dev --https --port 4000

In the HTTPS example, the browser connects to port 4000. The primary child receives a separate managed port and continues to use plain HTTP.

The local proxy has its own public listener. A tunnel has a public URL that forwards to the verified primary child endpoint.

See Local HTTPS, lpm proxy, and Tunneling for those public frontends.

Connect services with environment variables

LPM CLI gives each service the final ports of its peers:

For service "web":
  API_PORT=4000
  API_URL=http://localhost:4000

For service "api":
  WEB_PORT=5173
  WEB_URL=http://localhost:5173

The variable pattern is {SERVICE_NAME}_PORT and {SERVICE_NAME}_URL. LPM CLI converts the service name to uppercase and changes hyphens to underscores.

For example, a service named my-api becomes MY_API_PORT and MY_API_URL.

Every service receives variables for peers with assigned service ports. This includes workers and other services without their own port.

A service does not receive variables for itself. An external readiness target does not create peer variables.

Use explicit service env values for database URLs and other connections that need a different protocol.

The values use the final assigned ports. A replacement port appears in these variables automatically. These peer values override service env values with the same names.

Peer URLs continue to use http://localhost:<port> during local HTTPS. LPM CLI terminates browser HTTPS in front of the plain HTTP services.

Service-specific env values are also available:

lpm.json
{
  "services": {
    "api": {
      "command": "node api.js",
      "port": 4000,
      "env": {
        "LOG_LEVEL": "debug"
      }
    }
  }
}

Wait for service readiness

For a managed port, LPM CLI waits for a new listener that belongs to the launched service process.

This ownership rule prevents an unrelated process from satisfying the managed-port readiness step.

You can add an application readiness target:

ConfigurationBehavior
Managed port onlyWait for the assigned listener and make sure that the launched process owns it.
readyPortPoll the separate TCP port after managed-listener ownership.
readyUrlPoll the URL until it returns a 2xx response.
No managed port or readiness targetUse a short launch grace period.

If both readyUrl and a distinct readyPort exist, readyUrl is the application readiness target.

LPM CLI also recognizes advertised loopback URLs in service output. It preserves the advertised scheme and base path after it verifies the listener.

Listener ownership and the application probe share one readyTimeout deadline. The default is 30 seconds, and the maximum is 3600 seconds.

readyUrl accepts HTTP URLs for localhost, 127.0.0.1, or [::1]. It does not follow redirects.

If the assigned port changes, LPM CLI updates an explicit readiness port that matches the configured service port. Independent readiness targets remain unchanged, including URLs that use the default HTTP port.

If a service fails a readiness check, LPM CLI stops the initial startup.

LPM CLI terminates each service that it started and returns an error.

If a dependsOn service fails readiness, LPM CLI does not start the dependent service.

Start services in dependency order

dependsOn defines the initial startup order:

lpm.json
{
  "services": {
    "db": { "command": "docker compose up postgres", "readyPort": 5432 },
    "api": { "command": "node api.js", "port": 4000, "dependsOn": ["db"] },
    "web": { "command": "vite", "port": 5173, "dependsOn": ["api"] }
  }
}

LPM CLI reports an error for a missing dependency or a dependency cycle.

Independent branches start together. A dependent service starts only after all its declared dependencies are ready.

During shutdown, LPM CLI stops dependents before their dependencies. This order gives dependent services time to close active connections.

Restart crashed services

Set restart: true for a service that must restart after a non-zero exit:

lpm.json
{
  "services": {
    "worker": {
      "command": "node worker.js",
      "restart": true
    }
  }
}

LPM CLI uses increasing restart delays from 1 second to 30 seconds. It stops after 10 failed restart attempts.

If the service stays ready for more than 60 seconds, the next crash starts a new retry sequence. Startup waits and restart delays do not count.

Before LPM CLI restarts a service, each direct dependsOn service must be ready. Independent services can recover while another restart waits for readiness.

If a dependency restarts, LPM CLI stops its active transitive dependents in reverse dependency order. These dependents wait for recovery.

After the dependency is ready, LPM CLI restarts the waiting dependents in dependency order. Each dependency level must become ready before the next level starts.

If a restarted service fails readiness, LPM CLI stops that process. Then it schedules another attempt with the existing restart delay.

Dashboard restarts use the same dependency rules. A service that you stop from the dashboard remains stopped during dependency recovery.

If a dependency exits or cannot restart, LPM CLI stops its transitive dependents. LPM CLI does not resume them automatically.

Inspect port state

Use the complete command syntax:

lpm ports [action] [target]

Common read-only commands:

lpm ports                 # show ports for this project
lpm ports all             # show visible TCP listeners
lpm ports inspect 5173    # inspect one port

Reset machine-local replacements:

lpm ports reset

lpm ports reports a saved replacement before the configured preferred port. A host-only service can also appear with its saved assigned port.

Use lpm ports all to inspect current listeners. A saved assignment does not prove that the service is running.

See lpm ports for inspection output, process termination, JSON output, and platform differences.

Common problems

lpm dev uses a different port on every restart

Run lpm ports all to find listeners near the requested port. Concurrent development processes can make LPM CLI select another replacement.

A service stays on its replacement after the preferred port is free

Clear the project replacement, then restart the development run:

lpm ports reset
lpm dev

A saved port has no listener

A saved port is a local assignment. Start the project with lpm dev, then inspect the listener with lpm ports inspect <port>.

The browser port is not the child port

HTTPS, proxy, and network frontends can use separate public ports. Read the URL that lpm dev prints.

Startup stops after a readiness failure

An ownership, readyPort, or readyUrl failure stops the initial startup.

Read the service output. Then correct the readiness target or increase readyTimeout.

A peer variable is missing

Make sure that the peer has an assigned service port. A readyPort without port, primary, or host does not create peer variables.

See also

  • lpm ports — inspect listeners and reset saved replacements
  • lpm dev — run the development service graph
  • lpm.json services — configure services and readiness
  • lpm proxy — route friendly hostnames to assigned ports
  • Local HTTPS — separate the browser TLS port from child ports
  • Tunneling — forward a public URL to the primary service