# Native Deployment

> Applies to: ChengOS v0.2.0+ ｜ Last verified: 2026-09-25 ｜ Source: `deploy/hybrid/start.sh`, `deploy/hybrid/status.sh`, `deploy/hybrid/stop.sh`, `deploy/chengos.sh`, `deploy/.env.example`

Native — "hybrid" in the scripts — runs the compiled `cheng-api` binary directly on the host,
with no container runtime. It is the mode to choose when you want the lowest overhead, or when
you have **no root on the machine at all**.

[Deployment Modes](/docs/deploy-modes) compares it with Docker and distributed; this page is how
to run it.

## Install

```bash
./chengos.sh install --mode native --db-install-mode managed-process --with api,ui,app,cli
./chengos.sh start
./chengos.sh status
```

`chengos.sh` also runs as an interactive menu with no arguments, in English or Chinese. The
non-interactive commands are `install`, `start`, `stop`, `restart`, `status`, `version`,
`update`, `rollback`, `doctor`, `cloudflare`, and `uninstall`.

Modules are selected with `--with`: `api`, `ui`, `app`, `cli`, plus `redis` and `qdrant` for the
optional infrastructure. The installer offers full, minimal (Postgres + API + UI), or custom.

`status` **exits non-zero when the API is not serving**, deliberately, so it works as a health
probe in a monitoring script.

## The database decision

`DB_INSTALL_MODE` is the choice that shapes everything else:

| Mode | Databases run as | Needs `sudo` |
|---|---|---|
| `managed-process` (default) | Unprivileged user processes inside the install directory | **No** |
| `system-service` | System packages plus systemd services | Yes |

**`managed-process`** creates a PostgreSQL data directory with `initdb` inside the install tree
and starts it with `pg_ctl` on port 5432, as your own user. Nothing touches the system package
manager and nothing needs elevation. This is what makes ChengOS installable on a shared host or
a locked-down VM.

**`system-service`** installs the packages and enables them with
`systemctl enable --now postgresql` (and `valkey-server` when Redis is enabled), so the databases
start with the machine. `start.sh` checks for sudo up front rather than failing halfway.

Switching from `system-service` to `managed-process` stops and disables the system services
first, so the two do not both try to hold port 5432.

If `initdb` and `postgres` are not on the host at all, the script fails with an explicit message
rather than guessing — install the Postgres client tools and rerun.

Override per run:

```bash
./chengos.sh start --db-install-mode system-service
```

## Configuration

Everything reads `deploy/.env`. `start.sh` copies `.env.example` to `.env` on the first run and
then **stops**, telling you to edit it — it will not start with placeholder secrets.

For native mode the essentials are:

```bash
DATABASE_URL=postgres://tianai_db:…@127.0.0.1:5432/master_router
REDIS_URL=redis://:…@127.0.0.1:6379
QDRANT_URL=http://127.0.0.1:6334

DB_INSTALL_MODE=managed-process
ENABLE_REDIS=false
ENABLE_QDRANT=false

PORT=19225
API_PORT=19225
UI_PORT=8080
APP_PORT=5055
BIND_ADDRESS=127.0.0.1    # the API: this host only
UI_BIND=0.0.0.0           # the UI server: narrow this before going public
APP_BIND=0.0.0.0          # the app server: likewise

CREDENTIAL_MASTER_KEY_1=<64 hex chars>   # openssl rand -hex 32
JWT_SECRET=<64 hex chars>
```

**Hosts are `127.0.0.1` in native mode**, not the compose service names used in Docker. That is
the most common copy-paste error between modes. See
[Storage, Redis & Vector Configuration](/docs/config-storage-redis).

`ENABLE_REDIS` and `ENABLE_QDRANT` default to `false`: a minimal install runs on PostgreSQL
alone. Turn on Redis when you need [scheduling](/docs/exec-scheduling-triggers) or agent memory,
and Qdrant when you need [RAG](/docs/data-rag-pipeline).

`deploy/generate-env.sh` produces a `.env` with generated secrets instead of placeholders.

## The hardened topology

A native host runs three services that are **not** equally public:

| Service | Listens on | Reached by |
|---|---|---|
| `cheng-api` | `BIND_ADDRESS` = `127.0.0.1` | The UI and app servers, which proxy `/api` and `/ws`; the CLI and health checks on this host |
| `ui-server` | `UI_BIND` | Your reverse proxy or `cloudflared` |
| `app-server` | `APP_BIND` | Your reverse proxy or `cloudflared` |

The API never needs another address: even a remote CLI or desktop client should reach it
through the proxy's 443, where `/api` is forwarded. `UI_BIND` and `APP_BIND` **default to
`0.0.0.0`**, so a fresh install serves the frontends on every interface until you change them:

- **Proxy or `cloudflared` on this host** — set both to `127.0.0.1`.
- **Proxy on another machine** — set both to *this host's* private or VPN address (another
  machine's address cannot be bound), list the proxy in `TRUSTED_PROXY_IPS` unless it is on an
  RFC 1918 address, and make sure the proxy-to-origin hop is private or encrypted.

`.env` is never overwritten by an upgrade, so an older installation keeps `BIND_ADDRESS=0.0.0.0`
and may lack `UI_BIND` / `APP_BIND` entirely until you add them. The frontends print their
effective proxy trust set at startup. The full procedure, for both the tunnel and the proxy
routes, is in [Hardening a Public Deployment](/docs/deploy-hardening).

**Optional: a local decision model.** With `LAYA_ENABLED=true` and `LAYA_BIN` plus
`LAYA_MODEL_PATH` or `LAYA_MODELS_DIR` set, `start.sh` also launches `laya serve` on
`LAYA_PORT` (1215), verifies each model file against its pinned SHA-256, and warms it up;
`status` and `stop` include it. See
[Decision Providers & TypeSafe Jev](/docs/decision-providers-and-jev).

## Layout

`start.sh` creates and uses:

```text
<install-root>/
  bin/cheng-api          the binary
  .env
  logs/                  cheng-api.log, ui-server.log, app-server.log
  runtime/               pid files, database data directory
  skills/                skill packages
  ui/  app/              served frontend assets
```

Log paths can be overridden with `CHENG_API_LOG_FILE`, `CHENG_UI_LOG_FILE`, and
`CHENG_APP_LOG_FILE`.

## Day-to-day operation

```bash
./chengos.sh start                 # start selected modules
./chengos.sh status                # process status and version, non-zero if the API is down
./chengos.sh restart               # stop then start with the recorded module set
./chengos.sh stop                  # stop services
./chengos.sh version
./chengos.sh update                # scripts and package
./chengos.sh update --scripts-only # scripts only
./chengos.sh doctor                # read-only exposure report; non-zero on a problem
./chengos.sh uninstall
```

`stop` takes an option controlling whether the databases stop with the application — leave them
running if something else uses them.

`status` reports the installed version and whether a newer release is available; the update flow
is covered in [Upgrades & Rollback](/docs/deploy-upgrade-rollback).

The scripts underneath live in `deploy/hybrid/` (`start.sh`, `stop.sh`, `status.sh`,
`generate-env.sh`) and can be called directly, for example
`bash hybrid/start.sh --with api,ui`. Prefer `chengos.sh`: it records the installed mode and
module set so `restart` and `update` do the right thing without repeating the flags.

## Installing from source

If the install root contains `chengflow/` and `chengflow-ui/`, the installer detects a developer
workspace and compiles from source instead of unpacking a release archive. On a server you
almost certainly want the release package; on a development machine this is the convenience.

## Troubleshooting

| Symptom | Cause |
|---|---|
| `No .env file found` | First run created it; edit it and rerun |
| Postgres will not start | Port 5432 already held — often a leftover `system-service` install |
| `Postgres tools are not installed` | `initdb`/`postgres` missing; install the client tools |
| `status` says not serving, no log | Check `logs/cheng-api.log`; usually a bad `DATABASE_URL` or a missing secret |
| Scheduling does nothing | `ENABLE_REDIS=false` |
| RAG nodes fail to connect | `ENABLE_QDRANT=false`, or `QDRANT_URL` pointing at 6333 instead of 6334 |
| `system-service` install fails early | It requires sudo and checks up front |
| Remote CLI cannot connect to port 19225 | Expected: the API is loopback-only. Connect through the reverse proxy on 443 |
| `status` reports UI NOT RESPONDING | It prints the address it probed; if that differs from `UI_BIND`, `.env` changed without a restart |
| `EADDRNOTAVAIL` on start | `UI_BIND` / `APP_BIND` names an address that is not on this host |

## Next steps

- [Deployment Modes](/docs/deploy-modes) — how this compares with Docker.
- [Environment Variable Reference](/docs/config-env-reference) — every variable.
- [Upgrades & Rollback](/docs/deploy-upgrade-rollback) — keeping it current.
- [Hardening a Public Deployment](/docs/deploy-hardening) — putting it on the internet.
