Skip to main content
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 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 + RabbitMQ)
  • 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/bootstrap.sh auto-detects which in-stack services to skip based on the *_HOST values in your .env.enterprise.

Install

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. The bootstrap script orders the infrastructure (Postgres, MinIO, Hatchet) → 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

ServiceRoleExternalizable?
nebulaAPI + ingest pipeline
nebula-workerHatchet worker pool
graph-engineRust graph + vector store
postgresNebula application databaseYes → RDS
hatchet-postgresHatchet workflow databaseYes → RDS (same instance, separate DB)
minio + minio-initS3-compatible object storageYes → S3
hatchet-engine + hatchet-dashboardHatchet control planeNo
hatchet-rabbitmqHatchet task queueNo
See Managed AWS 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:
./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, hatchet-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 OPENAI_API_KEY required.
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:
    # 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.
GPU prereqs: NVIDIA Container Toolkit installed on the host; GPU capacity for the default Qwen3.5 completion model plus Qwen3 embedding model.

Upgrade

# 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

# 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:
set -e

PROJECT=nebula_enterprise
COPY_IMAGE=postgres:16.6

for volume in postgres_data minio_data hatchet_postgres_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 hatchet_postgres_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

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).
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.
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.
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 before rotating JWT keys.
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.
The Hatchet engine takes 30-60s to fully start after migrations. Wait for docker compose -f compose.enterprise.yaml logs -f hatchet-engine to show successfully booted, then refresh.