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

# Docker Compose

> Deploying Nebula Enterprise on a single host with Docker Compose

The Compose deploy is the right path for **air-gapped**, **single-VM**, and **POC / evaluation** deployments — anywhere standing up a Kubernetes cluster is overkill or impossible.

The bundle is identical regardless of which path you choose: same application binaries, same database schema, same release-contract attestations. Migrating from Compose to [EKS](/enterprise/kubernetes/eks) later does not require a data re-import.

## Prereqs

* Linux host with `docker` (24+) and `docker compose` (v2.30+, ideally v2.40+ for healthcheck and profile semantics)
* \~50 GB free disk (graph-engine WAL + compactor scratch + Postgres + Queue)
* 16 GB RAM minimum, 32 GB recommended for production
* OpenAI, Anthropic, or Azure OpenAI credentials. For air-gapped deploys with no cloud LLM, use the `compose.vllm.yaml` overlay (below)

If you're on AWS and want to back the durable state to RDS + S3 instead of running Postgres / MinIO in-stack, follow [Managed AWS resources](/enterprise/managed-resources). `enterprise/bootstrap.sh` auto-detects which in-stack services to skip based on the `*_HOST` values in your `.env.enterprise`.

## Install

```bash theme={null}
tar -xzf nebula-enterprise-<version>.tar.gz
cd nebula-enterprise-<version>/
sha256sum -c checksums.txt

# Preflight: docker/compose present, default published ports free,
# disk + memory headroom.
./scripts/preflight.sh

# Side-load every pinned image; no registry pull required.
docker load -i images.tar

# Generate the secrets section of .env.enterprise (random keys, RSA
# keypair for JWT signing, MinIO root credentials).
./enterprise/generate-secrets.sh

# Edit env/.env.enterprise: populate OpenAI, Anthropic, or Azure credentials and set NEBULA_CONFIG_NAME to match.
# For RDS + S3, see /enterprise/managed-resources.

# Bring everything up. Idempotent on re-run.
./enterprise/bootstrap.sh

# Verify
curl -fsS http://localhost:7272/v1/health
```

For JWT verifier behavior and signing-key rotation, see [Service authentication](/enterprise/security#service-authentication).

The bootstrap script orders the infrastructure (Postgres, MinIO, Orchestration) → migrations → token derivation → application services. Catalog-bootstrap runs Alembic, creates the required Postgres extensions (`vector`, `pg_partman`, `pg_cron`), and applies the catalog contract. End-to-end first boot takes 2-4 minutes; subsequent re-runs are a no-op if everything is already healthy.

## What runs in the stack

| Service                | Role                         | Externalizable? |
| ---------------------- | ---------------------------- | --------------- |
| `nebula`               | API + ingest pipeline        | —               |
| `nebula-worker`        | Orchestration worker pool    | —               |
| `graph-engine`         | Rust graph + vector store    | —               |
| `postgres`             | Nebula application database  | Yes → RDS       |
| `minio` + `minio-init` | S3-compatible object storage | Yes → S3        |

See [Managed AWS resources](/enterprise/managed-resources) for the externalization knobs.

## Air-gapped: local LLM with vLLM

For deployments with no internet egress, stack the `compose.vllm.yaml` overlay on top of `compose.enterprise.yaml` by passing `--overlay` to `bootstrap.sh`:

```bash theme={null}
./enterprise/bootstrap.sh --overlay compose.vllm.yaml
```

Running through `bootstrap.sh` (rather than calling `docker compose -f compose.enterprise.yaml -f compose.vllm.yaml up -d` directly) is required so the in-stack `postgres` and `minio` services activate via `COMPOSE_PROFILES` — those services are profile-gated and won't start under a raw `docker compose up`.

The overlay runs vLLM for completions and TEI for embeddings, then flips every Nebula service to `NEBULA_CONFIG_NAME=onprem_local`. Completion calls route to `http://vllm-instruction:8000/v1`; embedding calls route to `http://tei-embedding:8000/v1`. No cloud LLM API key is required.

<Warning>
  The default `nebula-enterprise-<version>.tar.gz` bundle does **not** include the vLLM or TEI images — the instruction image is \~10 GB and most customers route to OpenAI, Anthropic, or Azure OpenAI instead. Two options to obtain them:

  1. **Request an inference-inclusive bundle from support.** We can produce `nebula-enterprise-<version>-vllm.tar.gz` (built with `bundle.sh --include-vllm`) and ship it via the encrypted-delivery path. Use this for truly air-gapped hosts with no public-registry egress.

  2. **Side-load the inference images yourself.** If you have a separate host that *can* reach public registries, pull and save the images there, transfer the tarball, and load on the air-gapped host:
     ```bash theme={null}
     # On a connected host:
     docker pull vllm/vllm-openai:v0.20.2
     docker pull ghcr.io/huggingface/text-embeddings-inference:89-1.9
     docker save -o vllm.tar \
       vllm/vllm-openai:v0.20.2 \
       ghcr.io/huggingface/text-embeddings-inference:89-1.9
     # Transfer vllm.tar to the air-gapped host via your usual mechanism, then:
     docker load -i vllm.tar
     ```

  The exact inference image tags the overlay expects are pinned in `compose.vllm.yaml`; check that file for the versions to pull.
</Warning>

GPU prereqs: NVIDIA Container Toolkit installed on the host; GPU capacity for the default Qwen3.5 completion model plus Qwen3 embedding model.

## Upgrade

```bash theme={null}
# Extract the new bundle alongside the old one.
tar -xzf nebula-enterprise-<new-version>.tar.gz
cd nebula-enterprise-<new-version>/

# Reuse the existing env file (your secrets carry forward).
cp ../nebula-enterprise-<old-version>/env/.env.enterprise env/
cp -R ../nebula-enterprise-<old-version>/secrets/ ./

# Load new images.
docker load -i images.tar

# Update NEBULA_VERSION in env/.env.enterprise to the new version.
# Re-run bootstrap; it picks up the new image tags.
./enterprise/bootstrap.sh
```

Single-host restart; \~30s downtime on the API tier while pods cycle. Catalog-bootstrap re-runs Alembic to apply any new migrations before the API is back in service.

## Stopping the stack

```bash theme={null}
# Keep state (volumes preserved):
docker compose --env-file env/.env.enterprise -f compose.enterprise.yaml down

# Wipe state (volumes deleted — data loss):
docker compose --env-file env/.env.enterprise -f compose.enterprise.yaml down -v
```

The bundle namespaces containers, networks, and persistent Docker volumes
with `COMPOSE_PROJECT_NAME` from `env/.env.enterprise` (default
`nebula_enterprise`). Use a unique project name for separate deployments on
the same host; keep it stable when upgrading a deployment that should reuse
existing data. If an older bundle created unprefixed volumes such as
`postgres_data`, `bootstrap.sh` fails before starting services. Remove those
volumes for a fresh deployment, or manually copy them into the matching
project-prefixed volumes only after verifying the source deployment and secrets
belong to this upgrade:

```bash theme={null}
set -e

PROJECT=nebula_enterprise
COPY_IMAGE=postgres:16.6

for volume in postgres_data minio_data; do
  docker volume create "${PROJECT}_${volume}"
  docker run --rm \
    -v "${volume}:/from:ro" \
    -v "${PROJECT}_${volume}:/to" \
    "${COPY_IMAGE}" sh -c 'cp -a /from/. /to/'
done

for volume in postgres_data minio_data; do
  docker volume rm "${volume}"
done
```

`postgres:16.6` is included in `images.tar`; if you are running from a source
checkout instead of a bundle, pull it first. Include any other legacy volumes
that bootstrap reports.

## Troubleshooting

<AccordionGroup>
  <Accordion title="bootstrap.sh fails on 'NEBULA_POSTGRES_PASSWORD must be set'">
    `enterprise/generate-secrets.sh` didn't run, or `env/.env.enterprise` is missing the secrets section. Re-run `./enterprise/generate-secrets.sh ./env/.env.enterprise` and try again. The script refuses to overwrite an existing file, so delete it first if you intend to regenerate (warning: this rotates every secret).
  </Accordion>

  <Accordion title="catalog-bootstrap fails with 'pgvector extension not available'">
    The bundled Postgres image carries `vector`, `pg_partman`, and `pg_cron`, so this only happens if you've pointed at an external Postgres missing required extensions. On RDS/Aurora, make all three extensions available to the master user; if your parameter group restricts extension installs with `rds.allowed_extensions`, include them there. Set `shared_preload_libraries=pg_cron` and `cron.database_name` to the Nebula database name before bootstrap.
  </Accordion>

  <Accordion title="bootstrap exits with 'No valid client credentials found'">
    Select a config that matches your provider credentials: `full_openai` with `OPENAI_API_KEY`, `full_anthropic` with `ANTHROPIC_API_KEY`, or `full_azure` with `AZURE_API_KEY` and `AZURE_API_BASE`.
  </Accordion>

  <Accordion title="login succeeds but token issuance fails with 'no RS256 signing key configured'">
    The JWT signing key is missing from the deployment. Re-run `./enterprise/generate-secrets.sh ./env/.env.enterprise` on a clean env directory, or restore the `secrets/enterprise-jwt.pem` file and `NEBULA_JWT_KID` value from your previous deployment. See [Service authentication](/enterprise/security#service-authentication) before rotating JWT keys.
  </Accordion>

  <Accordion title="graph-engine fails to start, logs show 'S3 access denied'">
    For in-stack MinIO: check that `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` in `.env.enterprise` match `MINIO_ROOT_USER` and `MINIO_ROOT_PASSWORD`. `generate-secrets.sh` populates all four to the same value; if you edited any of them manually, restore the match.
  </Accordion>
</AccordionGroup>
