Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions deploy/control/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# meridian-control image (the node control plane). Separate deployable from the
# gateway image at the repo root. Runs the control-plane HTTP server; apply
# Alembic migrations first (`meridian-control migrate`) as an init step.
#
# docker build -f deploy/control/Dockerfile -t meridian-control .
# docker run -e MERIDIAN_CONTROL_DB_URL=postgresql+psycopg://... \
# -e MERIDIAN_CONTROL_REQUIRE_MTLS=1 \
# -v meridian-ca:/var/lib/meridian-control/ca \
# meridian-control migrate
# docker run ... meridian-control run --host 0.0.0.0 --port 8443
FROM python:3.12-slim-trixie AS builder

WORKDIR /build
RUN apt-get update && apt-get upgrade -y --no-install-recommends && rm -rf /var/lib/apt/lists/*

COPY pyproject.toml README.md ./
COPY meridian/ meridian/
COPY meridian_control/ meridian_control/

RUN python -m venv /opt/venv \
&& /opt/venv/bin/pip install --no-cache-dir --upgrade "pip>=26.1.2" \
&& /opt/venv/bin/pip install --no-cache-dir ".[control]"


FROM python:3.12-slim-trixie AS runtime
RUN apt-get update && apt-get upgrade -y --no-install-recommends && rm -rf /var/lib/apt/lists/* \
&& useradd --system --uid 10002 --create-home meridian-control

COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

# CA material and (for SQLite dev) the DB live here; mount a volume in production.
RUN mkdir -p /var/lib/meridian-control/ca && chown -R meridian-control /var/lib/meridian-control
ENV MERIDIAN_CONTROL_CA_DIR=/var/lib/meridian-control/ca
WORKDIR /var/lib/meridian-control

USER meridian-control
EXPOSE 8443
ENTRYPOINT ["meridian-control"]
CMD ["run", "--host", "0.0.0.0", "--port", "8443"]
90 changes: 90 additions & 0 deletions docs/CONTROL_PLANE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Control plane & GPU node agents

Meridian has two planes that work together:

- **The gateway** (this repo, the `meridian` package) — the request/traffic plane.
It decides *whether* a backend may receive traffic and routes to it.
- **The control plane** (`meridian-control`, in this repo) + **node agents**
([`meridian-node`](https://github.com/IMV-IN/meridian-node)) — the fleet plane.
Central `meridian-control` decides *what* should run on each GPU host; the
`meridian-node` agent decides *how* to run it safely on that host.

> Central Meridian decides **what** should run and **whether** it may receive
> traffic. `meridian-node` decides **how** to make it run safely on one GPU host.

```
meridian-node agent ──enroll / mTLS / heartbeat / desired-state / observations──▶ meridian-control
(GPU host) (this repo)
gateway ◀──── GET /admin/projection (routable managed engines) ────────────────────────────┘
```

## meridian-control

A **separate deployable** from the gateway (it does not import or modify gateway
code). It implements the node control protocol (`contracts/v1` in the
meridian-node repo): approval-gated enrollment with a built-in Ed25519 CA,
epoch-fenced sessions with restore-safe fencing, leases, desired-state
generations, observations, certificate rotation and revocation/CRL, and a
capacity-aware placement selector. State is durable via SQLAlchemy — **SQLite by
default, Postgres via a URL** — so control replicas are stateless.

See [`meridian_control/README.md`](https://github.com/IMV-IN/Meridian/blob/main/meridian_control/README.md)
for the full endpoint list and the meridian-node `DESIGN.md` for the protocol.

## Deploy

`meridian-control` ships its own image (`deploy/control/Dockerfile`), separate
from the gateway image.

```bash
docker build -f deploy/control/Dockerfile -t meridian-control .

# 1. Apply the schema (Alembic) — run once per DB / upgrade.
docker run --rm \
-e MERIDIAN_CONTROL_DB_URL=postgresql+psycopg://user:pass@db/meridian_control \
meridian-control migrate

# 2. Serve. Persist the CA directory; enable mTLS in production.
docker run -d --name meridian-control -p 8443:8443 \
-e MERIDIAN_CONTROL_DB_URL=postgresql+psycopg://user:pass@db/meridian_control \
-e MERIDIAN_CONTROL_REQUIRE_MTLS=1 \
-v meridian-ca:/var/lib/meridian-control/ca \
meridian-control run --host 0.0.0.0 --port 8443

# 3. Mint a one-time enrollment token for a node.
docker exec meridian-control meridian-control mint-token --auto-approve
```

The gateway consumes `GET /admin/projection` read-only (set `control_plane.url`
in the gateway config; see the projection-sync notes). Node agents are installed
from the [meridian-node](https://github.com/IMV-IN/meridian-node) repo
(systemd units + Dockerfile + example configs in its `deploy/`).

## Production readiness checklist

The system is feature-complete and tested (gateway 540, control 41, node 95 unit
tests + real-GPU host tests on an RTX 4060). Before a production rollout:

- [ ] **Enable mTLS.** `MERIDIAN_CONTROL_REQUIRE_MTLS=1`, terminate mTLS at the
edge, and forward the verified client cert in `x-client-cert`. The app must
be reachable **only** through that edge. (Default is off for dev.)
- [ ] **Use Postgres** (`MERIDIAN_CONTROL_DB_URL`), run `meridian-control migrate`
on deploy, and tune the pool (`MERIDIAN_CONTROL_DB_POOL_SIZE` /
`_MAX_OVERFLOW`).
- [ ] **Persist and back up the CA directory** (`/var/lib/meridian-control/ca`).
Follow the restore runbook (`POST /admin/restore`) after any DB restore so
fencing epochs cannot regress.
- [ ] **Node hosts:** deploy the root helper, pin allowed image digests + trusted
signing keys + per-device capacity in the root-owned policy, and start in
`observe-only` before enabling `managed-host`.
- [ ] **Validate the newer engine drivers** (TGI, SGLang, llama.cpp, LMDeploy)
against real engine images on a host — their launch flags are typed and
unit-tested but not yet run against live engines.

## What's left (deliberately deferred)

**Load-driven batched observation writes** beyond ~500 nodes (meridian-node
DESIGN §10.7). Batching trades projection/placement read-freshness for write
throughput, so it should follow a measured load profile rather than a guess.
Connection pooling is the correct first lever and is in place.
4 changes: 4 additions & 0 deletions meridian_control/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ Production manages the schema with **Alembic** (`meridian-control migrate`, or
`alembic -c meridian_control/alembic.ini upgrade head`). `create_all` remains the
zero-config default for local dev and tests.

Container image (separate from the gateway image): `deploy/control/Dockerfile`.
See [docs/CONTROL_PLANE.md](../docs/CONTROL_PLANE.md) for the full deploy flow and
the production-readiness checklist.

## Endpoints

| Method + path | Purpose |
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ nav:
- Known issues: KNOWN_ISSUES.md
- Understand:
- Architecture and scope: FULL.md
- Control plane & node agents: CONTROL_PLANE.md
- Milestones: MILESTONES.md
- What's shipped: ship.md
- Roadmap: ROADMAP.md
Expand Down
Loading