# `3pm runtime` cheatsheet — containers, PID 1, auto-exit

One foreground process owns the apps, streams their output to the
container's stdout/stderr, drains on SIGTERM, and **exits when there is
nothing left to supervise** so the orchestrator's restart policy is the
outer loop. Same idea as `pm2-runtime`; unlike `oxmgr runtime` the apps
live in a real daemon, so `3pm ls | logs | events | restart` work from
`docker exec` while it runs. Long-form rationale: `FEATURES.md` →
*Containers*.

## Invocations

```console
$ 3pm runtime ecosystem.toml                     # every app in the file
$ 3pm runtime ecosystem.toml --env production    # profile, same as `start`
$ 3pm runtime ecosystem.toml --only api,worker   # subset
$ 3pm runtime server.js --name api -i 4          # single script, any `start` flag
$ 3pm runtime server.js -- --port 3000           # app args after `--`

$ 3pm runtime app.py --no-auto-exit              # stay up after every app is down
$ 3pm runtime app.py --delay 5                   # wait 5 s before starting
$ 3pm runtime app.py --log-files                 # also write ~/.3pm/logs files
$ 3pm runtime app.py --raw                       # bare lines (pm2-runtime default)
$ 3pm runtime app.py --format                    # logfmt: ts= app= id= type= message=
$ 3pm runtime app.py --timestamp                 # "2026-08-28 10:27:39 0|app | line"
$ 3pm runtime app.py --timestamp=%H:%M:%S        # custom strftime
$ 3pm runtime app.py --json                      # NDJSON wire events (log_*, process, daemon_shutdown)
```

Every `3pm start` flag is accepted (`--name`, `-i`, `--cwd`, `--env`,
`--max-memory-restart`, `--restart-policy`, `--watch`, …); `3pm runtime
--help` is the authoritative list.

## Exit codes

| Code | When | What the orchestrator should do |
|---|---|---|
| `0` | SIGTERM / SIGINT → every app drained through its policy | normal stop |
| `2` | nothing left to supervise: zero apps selected, or every app is `stopped` / `errored` — a crash loop that exhausted `max_restarts` counts | restart the container (`restart: on-failure`) |
| `1` | the daemon died without announcing, or a start failed | restart the container; read the log |
| `130` | a second SIGTERM/SIGINT forced the daemon down (128 + SIGINT) | — |

`--no-auto-exit` turns `2` off: the daemon stays up with nothing running,
`docker exec … 3pm start` can add apps later.

## What you see

```
[3PM] 3pm runtime: daemon pid 7, home /root/.3pm
[3PM] App [api] launched
0|api | listening on :3000              ← stdout lines, prefixed id|name
0|api | warn: slow query                ← stderr lines go to stderr, red on a tty
[3PM] App [api] exit code=1 signal=SIGSEGV
[3PM] App [api] errored
[3PM] nothing left to supervise: stopping the daemon (exit 2)
[3PM] daemon shutting down (kill)
```

Daemon `tracing` lines (`INFO threepm_daemon::…`) share stderr;
`THREEPM_LOG=warn` quiets them. `[3PM]` chrome goes to stdout in text
mode, stderr in `--json` mode (stdout stays pure NDJSON).

## Dockerfile shape

```dockerfile
FROM node:22-slim
COPY --from=build /src/target/release/3pm /usr/local/bin/3pm   # see examples/Dockerfile
WORKDIR /app
COPY . .
ENV THREEPM_LOG=warn
CMD ["3pm", "runtime", "ecosystem.toml", "--env", "production"]
```

```console
$ docker run --rm -p 3000:3000 img                # streams
$ docker exec ctr 3pm ls                          # live table
$ docker exec ctr 3pm logs api --lines 50         # history needs --log-files
$ docker exec ctr 3pm events -f 'process:*'       # lifecycle stream
$ docker stop ctr; echo $?                        # SIGTERM → 0
```

```yaml
# compose / k8s
restart: on-failure          # exit 2 = crash-looped app → let the platform restart the pod
stop_grace_period: 20s       # ≥ the longest kill_timeout in the file
```

## Rules of thumb

- **Exec form only** (`CMD ["3pm", "runtime", …]`): a shell in front
  would be PID 1 and swallow SIGTERM.
- Log **files** are off by default (`out_file`/`error_file` = `NULL`) —
  stdout/stderr is what `docker logs` collects. `--log-files`, or an
  explicit `-o`/`-e`/`-l` (or the same fields in the file), turns them on
  for that stream. Avoid `--disable-logs` here: it also drops the bus
  stream, so nothing reaches stdout.
- `runtime` **must own the daemon**: if one already runs for
  `THREEPM_HOME` it exits `1` with "already owns this home". Use a
  separate `THREEPM_HOME` or `3pm kill` first.
- A **hosted home** is never one of its own: pointed at a home a root
  daemon hosts for a user (`[users] allow`, marked by a `hosted` file),
  `runtime` exits `1` with "is hosted by the root 3pm daemon: `3pm
  runtime` cannot own it" — that daemon owns the home, the apps and the
  exit code. Give the container its own `THREEPM_HOME`.
- `errored` is down on purpose: 3pm already gave the app `max_restarts`
  tries; the container restart is the next escalation.
- `WaitingRestart`, `Launching`, `Stopping` are *not* down — a crash
  that still has retries left never triggers exit `2`.
- Signals: the first SIGTERM/SIGINT is a graceful `3pm kill` (each app
  gets its `kill_signal`, then SIGKILL after `kill_timeout`); a second
  one SIGKILLs the daemon (`130`).
- Orphans re-parented to PID 1 are reaped (`waitpid(-1)`), so a
  double-forking app never leaves zombies.
- No `save`/`resurrect` in this mode; the ecosystem file *is* the state.
- An app with **`tty = true`** needs a pseudo-terminal: `/dev/ptmx` and
  devpts on `/dev/pts` (Docker and Kubernetes mount them by default; a
  minimal sandbox may not). Without them the spawn fails with `tty:
  cannot open a terminal for the app (is devpts mounted on /dev/pts?)`
  in `last_error` — the app never falls back to pipes. Its stripped
  output reaches stdout like any app's; `docker exec -it ctr 3pm attach
  <app>` connects to it (`Ctrl-Z` detaches).
- A `tty` app **dies with a daemon that crashes**: the kernel hangs up
  its terminal (`SIGHUP`) when the last master closes. Here that is the
  container's end anyway; on a host, `3pm update` keeps it running, a
  crash does not.

## Where it comes from

`crates/threepm-cli/src/commands/runtime.rs` (+ `runtime/reaper.rs`),
`threepm_protocol::spawn_foreground` / `SpawnMode::Foreground`,
`3pm __daemon --foreground` (`threepm-daemon/src/bootstrap.rs`).
E2E scenarios: `crates/threepm-cli/tests/e2e.rs`, `runtime_*`.
