> ## Documentation Index
> Fetch the complete documentation index at: https://docs.manthan.systems/llms.txt
> Use this file to discover all available pages before exploring further.

# Local Deployment

> Complete docker-compose.yml reference for running Parmana locally

## Overview

The Parmana stack consists of four services:

| Service     | Image                                                 | Port (host) | Role                    |
| ----------- | ----------------------------------------------------- | ----------- | ----------------------- |
| `postgres`  | `postgres:16-alpine`                                  | 5433        | Audit database          |
| `redis`     | `redis:7`                                             | 6380        | Replay protection store |
| `server`    | Built from `packages/server/Dockerfile`               | 3000        | Governance runtime      |
| `dashboard` | `ghcr.io/pavancharak/parmanasystems/dashboard:latest` | 8081        | Audit UI                |

<Note>
  Redis is required. The server throws `[SYS-REPLAY-001]` on startup if `REDIS_URL` is not set or Redis is unreachable. Replay protection cannot be disabled.
</Note>

***

## The docker-compose.yml

```yaml theme={null}
services:

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: Parmana_audit
      POSTGRES_USER: Parmana
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5433:5432"
    restart: unless-stopped
    networks:
      - parmana-net
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U Parmana -d Parmana_audit"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 10s

  redis:
    image: redis:7
    command:
      - redis-server
      - --appendonly
      - yes
    ports:
      - "6380:6379"
    restart: unless-stopped
    networks:
      - parmana-net

  server:
    build:
      context: .
      dockerfile: packages/server/Dockerfile
    volumes:
      - /secure/parmana:/secure/parmana:ro
    environment:
      PORT: 3000
      HOST: 0.0.0.0
      CORS_ORIGIN: http://localhost:8081
      REDIS_URL: redis://redis:6379
      AUDIT_DATABASE_URL: postgresql://Parmana:${POSTGRES_PASSWORD}@postgres:5432/Parmana_audit
      PARMANA_API_KEY: ${PARMANA_API_KEY}
      PARMANA_SIGNING_PROVIDER: ${PARMANA_SIGNING_PROVIDER}
      PARMANA_SIGNING_PRIVATE_KEY_PATH: ${PARMANA_SIGNING_PRIVATE_KEY_PATH}
      PARMANA_SIGNING_PUBLIC_KEY_PATH: ${PARMANA_SIGNING_PUBLIC_KEY_PATH}
      PARMANA_POLICIES_ROOT: /app/policies
      PARMANA_TRUST_ROOT: /app/trust/trust-root.json
      PARMANA_TRUST_PUBLIC_KEY: /app/trust/root.pub
      PARMANA_RELEASE_MANIFEST: /app/artifacts/release-manifest.json
      PARMANA_RELEASE_SIGNATURE: /app/artifacts/release-manifest.sig
    ports:
      - "3000:3000"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
    restart: unless-stopped
    networks:
      - parmana-net

  dashboard:
    image: ghcr.io/pavancharak/parmanasystems/dashboard:latest
    ports:
      - "8081:80"
    depends_on:
      server:
        condition: service_started
    restart: unless-stopped
    networks:
      - parmana-net

networks:
  parmana-net:
    driver: bridge

volumes:
  postgres_data:
```

***

## Environment variables

Create a `.env` file at the repository root. The compose file reads from it automatically.

### Required

| Variable            | Description                               |
| ------------------- | ----------------------------------------- |
| `POSTGRES_PASSWORD` | Password for the `Parmana` database user  |
| `PARMANA_API_KEY`   | Bearer token required on all API requests |

### Signing key — choose one option

**Option A — Key files on the host:**

```bash theme={null}
PARMANA_SIGNING_PROVIDER=disk
PARMANA_SIGNING_PRIVATE_KEY_PATH=/secure/parmana/private.pem
PARMANA_SIGNING_PUBLIC_KEY_PATH=/secure/parmana/public.pem
```

The host path `/secure/parmana` is mounted read-only into the container at `/secure/parmana`. Adjust the volume path in `docker-compose.yml` to match your actual key location.

**Option B — Keys in environment:**

```bash theme={null}
PARMANA_SIGNING_PROVIDER=env
PARMANA_SIGNING_PRIVATE_KEY=-----BEGIN PRIVATE KEY-----\n...
PARMANA_SIGNING_PUBLIC_KEY=-----BEGIN PUBLIC KEY-----\n...
```

<Warning>
  Do not put private key material in `.env` files committed to version control. For production, use Docker secrets or a secrets manager. See [Production Deployment](/docker/production).
</Warning>

### Optional with defaults

| Variable                    | Default                                | Description                     |
| --------------------------- | -------------------------------------- | ------------------------------- |
| `PORT`                      | `3000`                                 | Server listen port              |
| `HOST`                      | `0.0.0.0`                              | Server bind address             |
| `CORS_ORIGIN`               | `http://localhost:8081`                | Allowed CORS origin             |
| `PARMANA_POLICIES_ROOT`     | `/app/policies`                        | Path to compiled policy bundles |
| `PARMANA_TRUST_ROOT`        | `/app/trust/trust-root.json`           | Trust root JSON                 |
| `PARMANA_TRUST_PUBLIC_KEY`  | `/app/trust/root.pub`                  | Trust root public key (PEM)     |
| `PARMANA_RELEASE_MANIFEST`  | `/app/artifacts/release-manifest.json` | Release manifest                |
| `PARMANA_RELEASE_SIGNATURE` | `/app/artifacts/release-manifest.sig`  | Release manifest signature      |

***

## Required directory structure

The server verifies these paths exist on startup (relative to the container's working directory `/app`):

```
/app/
  policies/                     ← compiled policy bundles
  trust/
    root.pub                    ← trust root public key (PEM)
    trust-root.json             ← trust root metadata
  artifacts/
    release-manifest.json       ← release manifest
```

If any of these are missing, the server exits immediately with an error listing the missing path.

***

## Commands

```bash theme={null}
# Start all services in the background
docker compose up -d

# View server logs
docker compose logs server -f

# View all logs
docker compose logs -f

# Stop all services (preserve volumes)
docker compose down

# Stop and remove all volumes (deletes all audit data)
docker compose down -v

# Restart only the server
docker compose restart server

# Rebuild the server image after code changes
docker compose up -d --build server
```

***

## Verifying the stack

```bash theme={null}
# Runtime health
curl http://localhost:3000/health | jq .

# Runtime manifest (version, capabilities)
curl http://localhost:3000/runtime/manifest | jq .

# Audit statistics
curl http://localhost:3000/audit/stats \
  -H "Authorization: Bearer $PARMANA_API_KEY" | jq .
```

***

## Troubleshooting

**Server exits immediately on startup**

```bash theme={null}
docker compose logs server --tail=50
```

Common causes:

* `[SYS-REPLAY-001] REDIS_URL is required` — Redis container not up yet, or `REDIS_URL` not set
* `Parmana server must be started from repository root. Missing: /app/policies` — policy directory not present in the built image
* `[SYS-KEY-001]` — signing key not found at the configured path

**Postgres not accepting connections**

```bash theme={null}
docker compose logs postgres --tail=20
docker compose exec postgres pg_isready -U Parmana -d Parmana_audit
```

If `pg_isready` fails, `POSTGRES_PASSWORD` in `.env` may not match the password used when the volume was first initialized. Remove the volume and restart:

```bash theme={null}
docker compose down -v
docker compose up -d
```

**Dashboard shows "Cannot connect to server"**

The dashboard connects to the server from the browser, not from within Docker. The browser must be able to reach `http://localhost:3000`. Verify the server port is exposed and `CORS_ORIGIN` is configured for `http://localhost:8081`.

**Port already in use**

Modify the host port in `docker-compose.yml`. The container ports stay the same. For example, to move the server to port 3100:

```yaml theme={null}
ports:
  - "3100:3000"
```
