Process Lifecycle Management

Operators can reconfigure a running server — locally or inside a Docker container — without recreating the container. Four CLI subcommands cover the full lifecycle:

Command

Signal

What it does

zscaler-mcp reload

SIGHUP

Soft reload. Re-reads .env and re-applies env-driven toggles. MCP sessions and the listening socket survive.

zscaler-mcp restart

SIGUSR2

Hard restart. Re-reads .env, then os.execvp’s a fresh Python interpreter with the original argv. Same PID, fresh memory, fresh env, fresh module imports.

zscaler-mcp status

(read-only)

Print PID, uptime, transport, port, and .env path of the running server (or report none running).

zscaler-mcp stop

SIGTERM

Clean shutdown. Same signal Docker uses. No respawn.

Lifecycle subcommands are mutually exclusive with the serve path — running zscaler-mcp with no subcommand starts the server as before.

When to reload vs restart

  • Reload (SIGHUP) when you only flipped a runtime toggle: ZSCALER_MCP_LOG_TOOL_CALLS, log level, or a non-credential env var. The Zscaler SDK client has no module-level cache — it’s created on every tool call — so credential rotations land naturally on the next call. The auth middleware token cache is keyed by credential hash, so old entries miss the cache and re-validate against new values. MCP sessions survive.

  • Restart (SIGUSR2) when you changed something read once at startup: rotated credentials, changed --toolsets selection, flipped --enable-write-tools, swapped vanity domain, or anything that resolves the entitlement filter. Sessions die — clients reconnect.

The state of the server (sessions, sockets, in-flight requests) is preserved across reload and discarded across restart. Choose by what you changed, not by what’s convenient.

The PID file

When the server starts (after parse_args() succeeds, before server.run()), it writes a JSON PID file containing the running PID, start time, transport, host:port, the resolved .env path, the original argv, and the Python interpreter path. The PID file is the source of truth for status, reload, restart, and stop.

Location priority:

  1. --pid-file <path> / ZSCALER_MCP_PID_FILE

  2. /var/run/zscaler-mcp.pid (typical inside containers with write access)

  3. /tmp/zscaler-mcp.pid

  4. ~/.zscaler-mcp/server.pid (auto-created)

For multiple instances on the same host (e.g. one per port), set ZSCALER_MCP_PID_FILE=/tmp/zscaler-mcp-<port>.pid per instance.

Env-source classification

The status / reload / restart commands need to be honest about whether re-reading .env will actually change anything. The CLI classifies the env source on every invocation:

Label

What it means

live

.env is at the recorded path and exists. reload / restart will re-read it and pick up host-side edits.

live (bind-mounted)

Same as above but the path is /app/.env — i.e. you bind-mounted the file into the container.

fresh-discovery

The originally-recorded .env is gone but a new one is present at a default search path. restart will pick it up; reload will no-op.

missing

The originally-recorded .env is gone and no replacement exists. reload / restart will still run but won’t change credentials. Typical of containers started with --env-file only.

none

No .env was ever discovered. Typical of AgentCore deployments using Secrets Manager.

The classifier surfaces this in the status output so operators don’t have to guess.

The docker cp workflow

For a container already running without a bind mount, you can drop a .env in at any time:

docker cp ./.env <container>:/app/.env
docker exec <container> zscaler-mcp restart

The classifier reports fresh-discovery and restart execs a fresh process that picks up the newly-placed file. No container recreate needed.

Why --env-file alone doesn’t work for live edits

docker run --env-file=./.env reads the host file once at docker run time and copies the values into the container’s Config.Env metadata. After that moment, the container has no link to the host file — you could delete it on the host and the container still holds the values. docker stop && docker start <ctr> reuses Config.Env from metadata; it does not re-read the host file.

The only ways to change a running container’s environment are:

  1. docker rm + docker run (full recreate)

  2. Bind-mount the file inside so PID 1 can re-read it on reload/restart

  3. docker cp the file in, then zscaler-mcp restart

The lifecycle subcommands work with (2) and (3). (1) is always available but disrupts sessions.

Cross-platform notes

  • Linux / macOS: full support.

  • Windows: SIGHUP and SIGUSR2 don’t exist. reload / restart print a clear error and exit 2. status reads the PID file. stop falls back to SIGTERM. Native Windows operators restart their supervisor (Docker Desktop, NSSM, etc.) directly. Container deployments are unaffected — the container OS is Linux regardless of host.

Signal summary

Signal

Handler

Behaviour

SIGHUP

Custom

Soft reload. Re-reads .env, re-applies env-driven toggles.

SIGUSR2

Custom

Hard restart via os.execvp. Same PID, fresh process.

SIGTERM

Default (uvicorn)

Clean shutdown. Used by docker stop and zscaler-mcp stop.

SIGINT

Default (uvicorn)

Same as SIGTERM. Used by Ctrl+C.

Environment summary

Setting

Effect

--pid-file / ZSCALER_MCP_PID_FILE

Override the PID file location. Set per-instance when running multiple servers on the same host.

--dotenv-path / ZSCALER_MCP_DOTENV_PATH

Explicit path to the .env file. Recorded in the PID file so reload / restart re-read the same source.

See also