---
title: Deployment
description: Containers, Kubernetes, Helm and Terraform — plus the network topology that makes the plane split worth having.
---

The repository ships deployment definitions under `infrastructure/`, validated
by `npm run infra:validate` as a stage of `npm run verify`.

| | |
| --- | --- |
| `infrastructure/docker/Dockerfile` | Multi-stage build for all three Node processes |
| `infrastructure/docker/docker-compose.yml` | Single-node stack with PostgreSQL |
| `infrastructure/kubernetes/base.yaml` | Namespace, config, secrets, service accounts |
| `infrastructure/kubernetes/control-plane.yaml` | Deployment, service, probes, HPA |
| `infrastructure/kubernetes/gateway.yaml` | Deployment, service, probes, HPA |
| `infrastructure/helm/veefore/` | Chart and values |
| `infrastructure/terraform/` | Database, networking, secret references |

## Build

```bash
npm run build                 # backend and console
npm run build:backend
npm run build:console
```

```bash
docker build -f infrastructure/docker/Dockerfile -t veefore:1.0.0 .
```

One image, three entry points. The processes share almost all their code, and a
single image means one thing to scan, sign and promote.

```bash
node dist/control-plane/server.js
node dist/gateway/server.js
node dist/control-plane/worker.js
```

## Network topology

This is the part that makes the plane split worth having.

```text
                    Internet / your VPC
                            │
          ┌─────────────────┴─────────────────┐
          │                                   │
    ingress: console              ingress: gateway
    + control plane                /gateway/v1/* only
    /v1/* only                            │
          │                               │
   people, CLI, CI                     agents
          │                               │
          └───────────────┬───────────────┘
                          │
                   PostgreSQL (private)
```

:::security Separate ingresses. Do not merge them.
The gateway is the only process agents reach, and it must expose **only**
`/gateway/v1/*` and `/.well-known/veefore-platform-key`.

Exposing `/v1/*` on the ingress agents can reach undoes the design. The control
plane is where policies are activated and credentials are registered; it has no
business being reachable from wherever your agents run.

Correspondingly, do not expose `/gateway/v1/*` on the human-facing ingress.
There is no reason for a browser to reach it.
:::

The database is private to both planes. Nothing outside the cluster or VPC
reaches it.

## Kubernetes

```bash
kubectl apply -f infrastructure/kubernetes/base.yaml
kubectl apply -f infrastructure/kubernetes/control-plane.yaml
kubectl apply -f infrastructure/kubernetes/gateway.yaml
```

The probes are the important part of those manifests:

```yaml
livenessProbe:
  httpGet: { path: /health/live, port: http }
  initialDelaySeconds: 10
  periodSeconds: 10
readinessProbe:
  httpGet: { path: /health/ready, port: http }
  initialDelaySeconds: 5
  periodSeconds: 5
```

`/health/ready` genuinely checks the database, so a pod with a broken connection
leaves rotation instead of serving errors.

Both planes are stateless — session state and the job queue are in the database
— so horizontal scaling needs no coordination. Scale them independently: the
gateway sees high-volume, latency-sensitive agent traffic; the control plane
sees low-volume, bursty human and CI traffic. 3:1 is a reasonable starting
ratio.

Run at least one worker replica. Job claiming is tenant-safe and guarded against
overlapping poll ticks, so several can run concurrently. With none, webhooks
never deliver and usage never rolls up, and nothing visibly breaks.

## Secrets

```bash
kubectl create secret generic veefore-keys \
  --from-literal=VEEFORE_DATA_ENCRYPTION_KEY=… \
  --from-literal=VEEFORE_SIGNING_PRIVATE_KEY=… \
  --from-literal=VEEFORE_SIGNING_KEY_ID=… \
  --from-literal=VEEFORE_SESSION_SECRET=… \
  --from-literal=VEEFORE_AUDIT_CHAIN_SECRET=…
```

Better: an external secrets operator pulling from your secret manager, so
rotation does not mean editing a manifest.

:::danger Give each process only the secrets it needs
The gateway does not issue human sessions, so it does not need
`VEEFORE_SESSION_SECRET`. The worker does not sign passports, so it does not
need `VEEFORE_SIGNING_PRIVATE_KEY`.

A secret a process does not need is a secret that process should not have —
it is one more place a compromise can read it from. The
[table in Run the stack](/operate/run-the-stack) lists what each one requires.
:::

## Rollout order

1. **Migrations**, as a job that completes before anything restarts. Forward-only and safe against the previous version.
2. **Control plane**, rolling.
3. **Gateway**, rolling.
4. **Worker**. Any time; in-flight jobs are reclaimed.
5. **Console** static assets.

Gateway after the control plane, so the control plane is already on the new
schema when the gateway starts depending on it.

:::warning Enforcement mode is data, not configuration
Deploying does not change any environment's mode. It persists in the database
and changes only through `POST /v1/environments/{id}/gateway-mode` or
`veefore deploy` — with a reason, step-up authentication, and a `CRITICAL` audit
event if it reduces enforcement.

This is deliberate: a mode that could be changed by a deploy could be changed by
a misconfigured pipeline.
:::

## TLS

Terminate at your ingress. Both planes speak HTTP behind it.

`veefore doctor` reports a control plane or gateway reachable over plaintext
HTTP on a non-loopback host as a finding. Egress has the same rule enforced
rather than reported: a tool endpoint must be HTTPS unless its host is loopback.

## Terraform

`infrastructure/terraform/` provisions the database, the networking and the
secret references. It does not provision the application — that is Helm or raw
manifests, so you can move the application forward without touching
infrastructure state.

```bash
cd infrastructure/terraform
terraform init
terraform plan
```

## Validate before you ship

```bash
npm run infra:validate
```

Checks that the container, Kubernetes, Helm and Terraform definitions are well
formed. Part of `npm run verify`.

## Scaling signals

| Signal | Action |
| --- | --- |
| Gateway p99 decision latency rising | More gateway replicas |
| Database connections near `max_connections` | PgBouncer, or lower `DATABASE_POOL_MAX` |
| Job queue depth growing | More worker replicas |
| Rate limits firing unexpectedly across instances | Set `REDIS_URL` so limits are shared |

That last one is a real trap: without Redis, each instance keeps its own rate
limit and nonce state, so your effective limit is multiplied by your instance
count and nonce replay protection is weaker than you think.

## Related

:::cards

:::card Run the stack | /operate/run-the-stack
Processes, health and scaling.
:::

:::card Configuration | /operate/configuration
What to inject and what not to.
:::

:::card Deployment model | /architecture/adr/0013-deployment-model
Why two planes and one image.
:::

:::card Observability | /operate/observability
What to watch once it is running.
:::

:::
