Server safety: no duplicate long-running processes
A preflight gate before launching dev servers, application processes, and container stacks. Prevents the most common operational failure mode: a second instance fighting the first for the port, mid-state, or shared volumes.
Rule
Before launching a long-running process (dev / start / serve, container up), run preflight first. If an instance is already running, use it — do not start a second. If a restart is explicitly requested, stop the current instance gracefully, then launch once.
Preflight — native processes
Applies to: dev servers like vite, next dev, nodemon, uvicorn, gunicorn, flask run, rails s, custom application servers.
- Check the port if known.
- macOS / Linux:
lsof -nP -iTCP:<PORT> -sTCP:LISTEN - One line per listener with PID and command name. Empty output → port free.
- macOS / Linux:
- Check the process by command (fallback or in addition).
- macOS / Linux:
ps -ef | grep -E 'vite|next dev|nodemon|uvicorn|gunicorn|flask run|rails s' | grep -v grep - If multiple projects on the host could host similar processes, disambiguate by working directory:
ps -o pid,command -p <PID>andlsof -p <PID> | grep cwd.
- macOS / Linux:
PID files are not used here — modern dev tools rarely write them, and the twelve-factor app (§VIII Concurrency) explicitly recommends against manual PID-file management.
Preflight — Docker Compose
Applies to any docker compose up invocation.
- Check existing containers for this compose project.
docker compose ps --status running- Non-empty → containers already up for this project.
- If running and recreate was not requested — do not call
docker compose upagain. Use the existing stack (logs, healthchecks, URLs). - If launching anyway — prefer
docker compose up --no-recreateto avoid rebuilds when configuration is unchanged.
Do not preflight a Docker stack by host port. Compose orchestrates a network of containers; a port conflict may be unrelated to this stack. docker compose ps is the correct probe.
When already running
- Do not start a second instance.
- Report what is running:
<command> @ pid <PID>on port<PORT>(native), or<service-name> @ <container-id>(compose). - If the task can be completed against the existing instance (open a URL, hit a healthcheck, read logs) — use it.
When a restart is explicitly requested
- Stop gracefully first — send SIGTERM, not SIGKILL.
- Native:
kill <PID>(default SIGTERM). - Compose:
docker compose down(sends SIGTERM to containers, escalates to SIGKILL after its own timeout).
- Native:
- Wait briefly — give the process up to ~10 seconds to shut down. Re-check with the same preflight commands.
- Escalate only on timeout —
kill -9 <PID>ordocker compose killonly if SIGTERM did not work in the allotted window. - Launch once. Do not loop.
The graceful-stop discipline follows twelve-factor app §IX Disposability: processes should shut down on SIGTERM. SIGKILL skips cleanup hooks and may leave state corrupted (open sockets, half-written files, dangling locks).
After launch
Record for the rest of the session:
- The exact command used and its working directory (
cwd). - The port or URL the process serves.
- Where logs are written (stdout, file, container log).
Subsequent steps (open a URL, run a test, query the API) need this context; re-discovering it from scratch wastes turns. Record once.
Stop conditions
- If port / command / service is unclear from context, ask one clarifying question before running anything. Do not guess.
- If a specific process was named but preflight finds nothing related, surface that ("no
viteprocess or listener on :5173 found") before starting — it may be a different project or environment.
Known limitation
Between preflight and start there is a race window: a third process can grab the port in the milliseconds between lsof returning empty and the dev server binding. Pure-shell defense against this is platform-specific — flock exists on Linux but not natively on macOS, and open(2) with O_EXCL is below the shell level. This skill does not cover that race. In practice: dev tools fail loudly with EADDRINUSE if the race materializes, so the failure is visible and recoverable by re-running.
Sources
- The Twelve-Factor App, §VI Processes / §VIII Concurrency / §IX Disposability — https://12factor.net/
- Docker Compose CLI reference (
up,ps,down) — https://docs.docker.com/reference/cli/docker/compose/up/ lsof(8)man page — https://man7.org/linux/man-pages/man8/lsof.8.html
Internal: §E source check by sub-agent on 2026-05-28.
Source: CATWILLgh/MAINFRAME — distributed by TomeVault.