Getting Started with Nebula Pulse

Nebula Pulse is a Docker Swarm management platform that gives you a visual control plane for your container infrastructure. Deploy stacks, monitor services, auto-scale workloads, and integrate with your Git workflows — all from a single interface.

What is Nebula Pulse?

At its core, Nebula Pulse connects to the Docker Swarm manager on your host and exposes a rich UI and API for managing your cluster. A lightweight Nebula Agent runs as a global service on every node, collecting real-time CPU and memory metrics that the platform uses for autoscaling and observability.

🆓 Free / Community

  • Deploy & destroy stacks
  • View logs & service state
  • Manual scaling
  • Up to 5 users
  • Audit logging

⚡ Pro

  • Real-time CPU & memory metrics
  • CPU-based autoscaling
  • Multi-node management via SSH
  • Node label management
  • Backup & restore
  • Full RBAC, unlimited users

🏢 Enterprise

  • AI Assistant (YAML generation)
  • GitOps auto-deploy
  • SSO (Google, Azure AD, LDAP)
  • Full REST API access
  • Audit & compliance exports

Architecture Overview

Nebula Pulse is designed to run on a single manager node or across a multi-node swarm. The following services make up the platform:

ServicePurposeRequired
nebula_appNode.js API + backend logicYes
nebula_webNginx serving the frontend SPAYes
nebula_dbPostgreSQL — users, audit logs, GitOps configYes
nebula_agentRust telemetry agent (global service on all nodes)Pro+
ai_sidecarPython RAG assistant + Chroma vector DBEnterprise
ai_ollamaLocal LLM inference (Ollama)Enterprise

Installation

Run the automated installer on your Docker Swarm manager node:

bash
curl -fsSL https://get.nebula-pulse.io/install.sh | sudo bash

The installer will:

  1. Initialize Docker Swarm (if not already active)
  2. Generate a random admin password and JWT secret
  3. Deploy the full Nebula Pulse stack
  4. Print the access URL and admin credentials

First Login

  1. Open the URL printed by the installer (default port 80)
  2. Sign in with username admin and the generated password
  3. You will be prompted to change your password on first login
  4. After changing your password, the default credential is cleared from the environment for security

Security note: After you change your password, Nebula removes the ADMIN_PASSWORD environment variable from the Docker Swarm service spec so it cannot be reinjected on restart.

User Roles

RoleCapabilities
AdminFull access — users, nodes, stacks, audit logs, license management
DevOpsDeploy/destroy stacks, scale services, manage nodes
DeveloperView stacks and logs, trigger GitOps syncs
UserRead-only — view service status and metrics

Deployment Modes

Single Node

All services run on one machine. Ideal for development, staging, or small production workloads. Docker Swarm runs in single-node mode — you can still use the full Nebula Pulse feature set.

Multi-Node Cluster

Add worker nodes from the Swarm Topology tab using the SSH join feature (Pro+). The Nebula Agent deploys automatically to every node, enabling per-node metrics and cross-node visibility.

Air-Gapped / Offline

An offline installer package is available. License activation works via file upload — no external network access required.

Upgrades

Nebula Pulse ships an upgrade.sh script that updates an existing installation to a new version without data loss. It backs up your installation, deploys new application files, merges any new environment variables, and redeploys the Swarm stack as a rolling update — all in a single command.

Before You Upgrade

Confirm the following before running the upgrade:

  • You are running the command as root (or with sudo).
  • An existing Nebula installation exists at /opt/nebula (the script checks for /opt/nebula/.env).
  • Docker is running and Docker Swarm is active.
  • The upgrade package directory (containing the new VERSION file and application files) is accessible on the host.

The upgrade script displays your current installed version and the new package version before proceeding, and prompts for confirmation. Use --yes to skip the prompt for automated upgrades.

Running the Upgrade

Extract the new package on the host, then run:

bash
cd /path/to/nebula-pulse-X.Y.Z
sudo ./upgrade.sh

# Or skip the confirmation prompt:
sudo ./upgrade.sh --yes

The script runs 7 steps:

  1. Pre-upgrade backup — saves .env, license.key, nginx.conf, stacks/, and a Postgres dump to /opt/nebula_backups/pre_upgrade_<version>_<timestamp>.tar.gz.
  2. Stash preserved data — copies credentials, stacks, certs, and AI documents to a temp directory.
  3. Deploy new application files — copies backend, frontend, compose files, and agent binaries from the package into /opt/nebula/.
  4. Restore preserved data — writes the stashed .env back (your credentials are never overwritten). Merges any new environment variables added in this release — existing values are never touched.
  5. Update Docker configs & images — recreates the nebula_agent_js and nebula_package_json Docker configs, loads the new agent image, and rebuilds the AI sidecar if present.
  6. Rolling redeploy — runs docker stack deploy to apply the updated images and configs with a rolling update (no downtime for replicated services).
  7. Health check — waits 20 seconds, then verifies all Swarm services report the correct replica count.

Database schema migrations (new tables, columns) are applied automatically on the first login after the upgrade via the initDB() function.

What's Preserved

The following are always preserved across an upgrade — they are never overwritten by new package files:

ItemLocationNotes
Environment & credentials/opt/nebula/.envJWT secret, admin password, DB credentials, SMTP config, etc.
License key/opt/nebula/license.keyRestored verbatim if present
TLS certificates/opt/nebula/certs/Custom certs restored from stash
Stack configs/opt/nebula/stacks/All user-created stack YAML files and config files
AI documents/opt/nebula/ai/documents/User-uploaded RAG knowledge base documents
Database dataDocker volume nebula_db_dataPostgres data volume is never touched — only a dump is taken for reference

New environment variables

Each release may introduce new env vars. The upgrade script automatically appends any missing variables to your .env with safe default values — it never modifies variables that already exist. Review the appended values after upgrading and update them as needed.

Rollback

If something goes wrong, the pre-upgrade backup at /opt/nebula_backups/ contains everything needed to restore your previous state:

bash
# List available backups
ls /opt/nebula_backups/

# Extract the backup you want to restore
cd /opt/nebula_backups
tar -xzf pre_upgrade_4.2.0_20260430_120000.tar.gz

# Restore .env and redeploy the previous version's compose
cp pre_upgrade_4.2.0_20260430_120000/.env /opt/nebula/.env
# Then re-run install or redeploy with the previous package

The Postgres data lives in a Docker named volume (nebula_db_data) and is never deleted by the upgrade script. The pre-upgrade SQL dump in the backup archive is an additional safety net — you can import it with psql if the volume is ever lost.

After any upgrade, check Admin → Audit Log to confirm that the platform started cleanly and that schema migrations completed (look for the login events and any system-init entries).

Features & Tiers

Nebula Pulse ships with three licensing tiers. All tiers include core stack management and audit logging. Advanced metrics, autoscaling, GitOps, and AI capabilities are unlocked progressively.

Free / Community

No license key required. The Free tier is fully functional for individuals and small teams getting started with Docker Swarm management.

Included features

  • Deploy and destroy stacks from YAML
  • Service state monitoring (Running / Degraded / Down)
  • Replica count (actual vs desired)
  • Node placement visibility
  • Manual scaling with +/− buttons
  • Container log viewer
  • Stack configuration viewer
  • Audit logging
  • Up to 5 user accounts

Limitations

  • No real-time CPU / memory metrics (Docker Swarm API only)
  • No autoscaling
  • No multi-node SSH join/remove
  • No backup & restore
  • No SSO or identity provider integration

Pro

Unlocks deep metrics from the Nebula Agent running on each node, autoscaling, and multi-node cluster management.

Everything in Free, plus:

Deep Metrics (Nebula Agent)

MetricSourceExample
vCPU %Kernel cgroups via agent45.2%
RAMMemory cgroups via agent256 MiB

Autoscaling Labels

yaml
deploy:
  labels:
    nebula.scale.cpu: "70"   # Scale up when CPU exceeds 70%
    nebula.scale.min: "2"    # Always keep at least 2 replicas
    nebula.scale.max: "10"   # Never exceed 10 replicas
  • Node management — add nodes via SSH, drain and remove nodes
  • Node label management for placement constraints
  • File browser for container filesystem access
  • One-click backup download (ZIP: stacks, configs, license)
  • Full Role-Based Access Control (RBAC) with unlimited users
  • SSO — Google Workspace, Azure AD / Entra ID, LDAP / Active Directory via Dex OIDC

Enterprise

Full platform capabilities, including AI-powered assistance, GitOps integration, and priority support.

Everything in Pro, plus:

  • AI Assistant — Natural language Docker Compose generation, RAG-powered documentation queries, troubleshooting help
  • GitOps — Auto-deploy on git push from GitHub, GitLab, or Bitbucket
  • REST API — Full programmatic access for external integrations
  • Audit exports — CSV and JSON export for compliance reporting

Scaling Protection

Nebula prevents accidental scaling of stateful services. If a service name matches a known stateful pattern, a warning is shown before scaling proceeds:

Protected Patterns
postgres, mysql, mariadb, mongodb
redis, elasticsearch, cassandra
rabbitmq, kafka, zookeeper
Any service with _db or -db in its name

License Enforcement

Days RemainingUI IndicatorFeature Access
> 30 daysNoneFull
10–30 daysYellow warningFull
1–10 daysOrange warningFull
0–7 days (Grace)Red bannerFull (grace period)
ExpiredLockout bannerReverts to Free

Warning thresholds differ by billing period: 30 days for yearly subscriptions, 10 days for monthly.

Stack Management

Nebula Pulse manages Docker Swarm stacks with automatic directory organisation, config injection, network creation, and bind-mount volume setup. Each stack gets its own directory under /opt/nebula/stacks/.

Directory Structure

text
stacks/
  rabbitmq/
    docker-compose.yml       # Stack YAML
    rabbitmq-config          # Config file saved to disk
    rabbitmq-definitions     # Config file saved to disk
  nginx/
    docker-compose.yml
    html/                    # Auto-created bind mount directory
  kafka/
    docker-compose.yml

Legacy flat stack files (stacks/nginx.yml) are automatically migrated to the directory structure on platform startup.

Deploying a Stack

Via the UI

  1. Click STACKS+ Deploy Stack
  2. Enter a stack name — alphanumeric, dashes, and underscores only. Names cannot start with nebula.
  3. Paste or type your Docker Compose YAML. Enterprise users can use AI Autocraft to generate YAML from a plain-English description.
  4. If the YAML references Docker configs, input fields appear automatically for each config file.
  5. Click EXECUTE to deploy.

What happens during deployment

  1. Config creation — Docker configs are saved to disk and registered in Swarm
  2. Network creation — External overlay networks are pre-created
  3. Volume directories — Local bind-mount paths under /opt/nebula/stacks/ are auto-created
  4. YAML processing — The configs: top-level block is injected with external: true
  5. Stack deploydocker stack deploy -c <yaml> <name> is executed
  6. Audit log — Success or failure is recorded with full deployment details

Via the API

http
POST /api/stack/deploy
Content-Type: application/json

{
  "name": "mystack",
  "yaml": "version: '3.8'\nservices:\n  web:\n    image: nginx:alpine",
  "configs": {
    "config-name": "file content here..."
  }
}

Destroying a Stack

  1. Click STACKSDestroy Stack
  2. Select the stack from the dropdown
  3. Confirm the deletion

On destroy, Nebula:

  • Removes the stack via docker stack rm
  • Waits 3 seconds for services to drain, then removes Docker configs
  • Deletes the stack directory from disk
  • Writes an audit log entry

Placement Constraints

Pin services to specific nodes using label constraints:

yaml
deploy:
  placement:
    constraints:
      - node.labels.rabbitmq == 1

Use Node Management to assign labels to nodes before deploying stacks with placement constraints.

Stack APIs

EndpointMethodDescription
/api/stack/deployPOSTDeploy a stack with optional configs
/api/stack/destroyPOSTDestroy a stack and clean up configs
/api/stacks/listGETList all deployed stacks
/api/stack/:name/yamlGETGet the YAML for a stack
/api/stack/:name/configsGETGet config references for a stack
/api/stack/configs/parsePOSTParse YAML to detect config references

Troubleshooting

"No suitable node" error

The stack has placement constraints that no nodes satisfy. Use Node Label Management to add the required labels to your nodes.

Config files not appearing in deploy modal

Make sure the YAML has a configs: section at the service level. Config detection is automatic when you type or paste YAML.

Stack failing after config changes

Docker Swarm configs are immutable. When redeploying, Nebula removes the old config and creates a new one. The 3-second drain delay handles in-flight requests.

Bind mount directory not created

Only directories under /opt/nebula/stacks/ are auto-created. For other paths, create the directory manually before deploying.

Autoscaling

Nebula Pulse automatically scales Docker Swarm services horizontally based on CPU utilisation. The autoscaler evaluates each enabled service every 15 seconds and adjusts replicas within the bounds you define.

Autoscaling Labels

Enable autoscaling by adding three labels to the deploy.labels section of your stack YAML:

LabelTypeDescription
nebula.scale.cpuString (0–100)CPU % threshold to trigger scale-up
nebula.scale.minString (≥ 1)Minimum replica count (floor)
nebula.scale.maxStringMaximum replica count (ceiling)

Labels must be strings — use quotes: "70" not 70. Labels must be in the deploy.labels section, not the top-level service labels.

yaml
version: '3.8'
services:
  web:
    image: nginx:alpine
    deploy:
      replicas: 1
      labels:
        nebula.scale.cpu: "70"   # Scale up when CPU > 70%
        nebula.scale.min: "1"    # Keep at least 1 replica
        nebula.scale.max: "10"   # Never exceed 10 replicas
      resources:
        limits:
          cpus: '0.5'
          memory: 256M

Scale Behavior

Scale Up

  • Triggers when average CPU exceeds nebula.scale.cpu
  • Adds replicas gradually (+1 or +2 at a time)
  • Never exceeds nebula.scale.max
  • Check interval: 15 seconds

Scale Down

  • Triggers when average CPU drops below 50% of the target threshold
  • Example: target = 70% → scale down kicks in below 35%
  • Removes replicas gradually to avoid disruption
  • Never goes below nebula.scale.min

CPU Limits and Scaling Speed

CPU percentage is calculated relative to the service's resources.limits.cpus value. Lower CPU limits make percentages rise faster under load, which causes the autoscaler to react more quickly.

yaml
resources:
  limits:
    cpus: '0.25'    # Lower limit = faster CPU % rise = faster scaling
    memory: 128M
  reservations:
    cpus: '0.1'
    memory: 64M

Examples

Stateless Web Application

yaml
version: '3.8'
services:
  app:
    image: nginx:alpine
    ports:
      - "8080:80"
    deploy:
      replicas: 2
      labels:
        nebula.scale.cpu: "60"
        nebula.scale.min: "2"
        nebula.scale.max: "8"
      resources:
        limits:
          cpus: '0.5'
          memory: 256M
      restart_policy:
        condition: on-failure
    networks:
      - app_net

networks:
  app_net:
    driver: overlay

High-Availability API Service

yaml
version: '3.8'
services:
  api:
    image: myapp/api:latest
    ports:
      - "3000:3000"
    deploy:
      replicas: 3
      labels:
        nebula.scale.cpu: "75"
        nebula.scale.min: "3"
        nebula.scale.max: "15"
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M
    networks:
      - backend

networks:
  backend:
    driver: overlay

Recommended Thresholds by Service Type

Service TypeCPU ThresholdNotes
Web / frontend servers60–70%Low latency sensitive
API / microservices70–75%General purpose
Databases80–85%Scale conservatively; stateful
Background workers75–80%Latency tolerant

Testing Autoscaling

bash
# Using Apache Bench (100k requests, 50 concurrent, 60 seconds)
ab -n 100000 -c 50 -t 60 http://your-service:port/

# Using hey (simpler syntax)
hey -z 60s -c 50 http://your-service:port/

# Monitor replicas in real time
watch docker service ls

Troubleshooting

Service not scaling

  1. Confirm labels are in the deploy.labels section (not top-level labels)
  2. Check that resources.limits.cpus is set on the service
  3. Verify the service is actually under load — check with docker stats
  4. Ensure nebula.scale.min < nebula.scale.max

Scaling too aggressively

  • Increase the CPU threshold
  • Reduce nebula.scale.max
  • Increase CPU resource limits so the percentage rises more slowly

Scaling too slowly

  • Lower the CPU threshold
  • Decrease CPU limits so percentage rises faster
  • Increase the initial replica count

GitOps

Nebula Enterprise's GitOps integration automatically deploys updated stack definitions when you push to a configured Git branch. You maintain your infrastructure as code and Nebula handles the deployment.

How GitOps Works

1 — Developer pushes to main branch
2 — GitHub / GitLab sends webhook to Nebula
3 — Nebula verifies signature & clones repository
4 — Stack YAML files are deployed to Swarm
5 — Services updated automatically

Requirements

  • Nebula Enterprise license
  • Git repository (GitHub, GitLab, or Bitbucket)
  • Repository accessible over HTTPS from your Nebula host
  • Nebula accessible from the internet (or from your Git provider's IP range)

Setup

Step 1 — Add Repository in Nebula

  1. Click GITOPS in the admin panel
  2. Fill in the repository details: Name, Provider, Repository URL, Branch, and Stack Path
  3. Click ADD REPOSITORY
  4. Copy the generated Webhook URL and Secret for the next step

Configuring Webhook Providers

GitHub

  1. Go to your repo → SettingsWebhooksAdd webhook
  2. Set Payload URL to the Nebula webhook URL
  3. Set Content type to application/json
  4. Paste the Nebula-generated Secret
  5. Select Just the push event and click Add webhook

GitLab

  1. Go to your repo → SettingsWebhooks
  2. Paste the webhook URL and Secret token from Nebula
  3. Check Push events and click Add webhook

Repository Structure

Organise your repository with stack files under the configured Stack Path (default: /stacks):

text
/stacks
  ├── frontend.yml
  ├── backend.yml
  ├── database.yml
  └── monitoring.yml

# Or by environment:
/stacks
  ├── production/
  │   ├── app.yml
  │   └── db.yml
  └── staging/
      ├── app.yml
      └── db.yml

Best Practices

Use environment branches

text
main      → Production deployments
staging   → Staging environment
develop   → Development testing

Configure a separate Nebula GitOps repository entry for each branch.

Pin image versions

yaml
services:
  api:
    image: myorg/api:v1.2.3    # ✅ Specific version
    # NOT: image: myorg/api:latest  # ❌ Unpredictable

Use Docker secrets for sensitive values

yaml
services:
  app:
    secrets:
      - db_password

secrets:
  db_password:
    external: true    # Created separately, never committed to Git

Validate before pushing

bash
# Validate locally before pushing
docker-compose -f stacks/app.yml config

# GitHub Actions validation workflow
# .github/workflows/validate.yml
name: Validate Stacks
on:
  push:
    paths: ['stacks/**']
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Validate all stack files
        run: |
          for f in stacks/*.yml; do
            docker-compose -f "$f" config > /dev/null && echo "✅ $f"
          done

Troubleshooting

Webhook not triggering

  1. Verify the webhook URL is correct in your Git provider
  2. Check the secret matches exactly — no trailing spaces
  3. Ensure Nebula is accessible from the internet
  4. Review webhook delivery logs in your Git provider

Deployment fails after push

  1. Check Nebula app logs: click LOGS on the nebula_app service
  2. Verify YAML syntax is valid (docker-compose config)
  3. Ensure container images are accessible from all swarm nodes
  4. Confirm the stack path in Nebula matches your repository structure

Signature verification failed

  • GitHub — Regenerate the webhook secret in Nebula and update it in GitHub
  • GitLab — Ensure the X-Gitlab-Token header matches the Nebula secret exactly

Security Considerations

  • All webhooks use cryptographic signature verification (HMAC-SHA256 for GitHub)
  • Webhook secrets are generated using cryptographically secure random bytes
  • Use HTTPS URLs for repository access; SSH is not yet supported
  • For private repositories: https://[email protected]/org/repo.git
  • Consider IP allowlisting your webhook endpoint
  • Rotate webhook secrets periodically

Node Management

Pro and Enterprise users can manage their Docker Swarm topology directly from Nebula Pulse — adding worker nodes via SSH, assigning placement labels, and draining or removing nodes from the cluster.

Swarm Topology View

The Swarm Topology table shows all nodes in your cluster with their current status, role, IP address, and assigned labels. Each node displays real-time CPU and memory usage collected by the Nebula Agent.

Labels & Placement Constraints

Docker Swarm node labels are key-value pairs that control which nodes run specific services. Nebula provides a UI for managing labels without touching the CLI.

Labels are set on nodes (via Nebula's Node Management UI), while autoscaling labels are set on services (in your stack YAML's deploy.labels section).

Example placement constraint

yaml
deploy:
  placement:
    constraints:
      - node.labels.rabbitmq == 1   # Only run on nodes with this label

Common label patterns

LabelPurpose
rabbitmq=1, rabbitmq=2Pin specific RabbitMQ nodes to specific hosts
postgres=primaryPin the primary database to a dedicated host
zone=us-east-1aAvailability zone awareness
tier=frontendGroup nodes by role
env=productionEnvironment isolation

Label format rules

  • Keys and values: alphanumeric with dots (.), dashes (-), or underscores (_)
  • Maximum 128 characters per key or value
  • Examples: rabbitmq=1, app.role=database, zone=us-east

Managing Labels via UI

Adding a label (Admin only)

  1. Go to the Swarm Topology table
  2. Find the target node and click + TAG in the Tags column
  3. Type the label in key=value format (e.g. rabbitmq=1)
  4. Press Enter or click ADD — the label is applied immediately

Removing a label (Admin only)

  1. Find the label badge on the node
  2. Click the red × on the badge and confirm

Automatic Labels

When a node joins the swarm via Nebula's SSH join feature, it automatically receives an ip_address label set to its IP address.

API Reference

EndpointMethodDescription
/api/nodes/:id/labelsPOSTAdd or update labels on a node
/api/nodes/:id/labels/:keyDELETERemove a label from a node
/api/nodes/listGETList all nodes with current labels
http
POST /api/nodes/abc123/labels
Authorization: Bearer <jwt>
Content-Type: application/json

{ "labels": { "rabbitmq": "1", "zone": "us-east" } }

→ { "success": true, "message": "Labels updated" }

Audit Logging

All label operations are recorded in the audit trail:

  • NODE_LABEL_ADD — Label added or updated (includes node ID and key/value)
  • NODE_LABEL_REMOVE — Label removed (includes node ID and key)

Docker Configs

Nebula Pulse supports Docker Swarm configs — a mechanism for injecting configuration files into containers at deploy time. Applications like RabbitMQ, HAProxy, Nginx, and Kafka often require configuration files that must exist before services start.

How It Works

Automatic config detection

When you paste YAML into the Deploy Stack modal, Nebula automatically parses it for Docker config references. If any service contains a configs: section, the UI shows input fields for each config file — one textarea per config.

Deploy flow with configs

  1. Open the Deploy Stack modal
  2. Enter a stack name and paste your YAML
  3. A purple CONFIG FILES section appears listing each detected config with its target path
  4. Paste the content for each config file into its textarea
  5. Click DEPLOY

Behind the scenes

  1. Config content is saved to disk at /opt/nebula/stacks/<stackname>/
  2. Each config is registered in Docker Swarm via docker config create
  3. The YAML is modified to add a top-level configs: block with external: true
  4. The stack is deployed with docker stack deploy

On stack destroy

  1. Nebula reads the stack YAML to identify config references
  2. Waits 3 seconds for services to drain
  3. Removes each config with docker config rm
  4. Deletes the stack directory and config files from disk

RabbitMQ Cluster Example

Stack YAML

yaml
version: "3.8"
services:
  rabbitmq1:
    image: rabbitmq:3.13-management
    hostname: rabbitmq1
    configs:
      - source: rabbitmq-config
        target: /etc/rabbitmq/rabbitmq.conf
      - source: rabbitmq-definitions
        target: /etc/rabbitmq/definitions.json
    networks:
      - rmq_net
    deploy:
      placement:
        constraints:
          - node.labels.rabbitmq == 1

networks:
  rmq_net:
    driver: overlay

rabbitmq-config content

ini
cluster_formation.peer_discovery_backend = classic_config
cluster_formation.classic_config.nodes.1 = rabbit@rabbitmq1
cluster_formation.classic_config.nodes.2 = rabbit@rabbitmq2
cluster_formation.classic_config.nodes.3 = rabbit@rabbitmq3
loopback_users.guest = false

Redeployment

Docker Swarm configs are immutable. When redeploying a stack that already has configs, Nebula removes the old configs first, then creates new ones with the updated content before redeploying the stack.

API Reference

EndpointMethodDescription
/api/stack/configs/parsePOSTParse YAML to detect config references
/api/stack/deployPOSTDeploy stack with optional configs object
/api/stack/:name/configsGETGet configs associated with a deployed stack
http
POST /api/stack/configs/parse
Content-Type: application/json

{ "yaml": "version: '3.8'\nservices:\n  rabbitmq:\n    configs:\n      - source: my-config\n        target: /etc/app/config.conf" }

→ {
  "success": true,
  "configs": [{ "name": "my-config", "target": "/etc/app/config.conf" }]
}

Secrets

Nebula Pulse integrates with Docker Swarm's native secrets engine to let you store sensitive values — passwords, API keys, TLS certificates, tokens — in an encrypted store and inject them into containers without ever writing them to environment variables, compose files, or disk.

How It Works

Secrets vs environment variables

Environment variables show up in docker inspect output, process lists (ps e), and container logs. Docker Swarm secrets avoid all of this:

PropertyEnv VarsDocker Secrets
Encrypted at restNoYes — AES-256-GCM via Raft journal
Encrypted in transitNoYes — mutual TLS between nodes
Visible in docker inspectYesNo
Visible in process listYesNo
Retrievable after creationYesNo (write-only)
Mounted as fileNoYes/run/secrets/<name>
Access-scoped to serviceNoYes — only declared services receive it

Storage model

Secret values are stored exclusively inside Docker Swarm's encrypted Raft log — Nebula Pulse never writes values to its database or disk. Only metadata (name, description, creator, timestamps) is stored in Nebula's Postgres database. Once created, a secret value cannot be read back through any Nebula API.

Container access

When a service declares a secret in its compose YAML, Docker Swarm mounts it at /run/secrets/<name> as a read-only tmpfs file (permissions 0400, owner root). Only processes inside that specific container can read it — no other service or host process has access.

Example YAML

The following stack deploys a PostgreSQL database and a Node.js API using secrets for the DB password and a third-party API key. This is the YAML you would paste into the Deploy Stack modal.

yaml
version: "3.8"

services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: appuser
      POSTGRES_DB: appdb
      # Read password from secret file, not env var
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    volumes:
      - db_data:/var/lib/postgresql/data
    networks:
      - app_net
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.role == manager

  api:
    image: myorg/myapp:latest
    environment:
      DB_HOST: db
      DB_USER: appuser
      DB_NAME: appdb
      # App reads /run/secrets/db_password and /run/secrets/stripe_key at startup
    secrets:
      - db_password
      - source: stripe_key
        target: /run/secrets/stripe_key
        mode: 0400
    networks:
      - app_net
    deploy:
      replicas: 2
      update_config:
        parallelism: 1
        delay: 10s

networks:
  app_net:
    driver: overlay

volumes:
  db_data:

secrets:
  db_password:
    external: true
  stripe_key:
    external: true

What happens during deploy

  1. Nebula detects the secrets: section in your YAML and shows an amber SECRETS panel in the Deploy modal
  2. For each secret, enter its value in the password input — or leave blank if it already exists in Swarm
  3. Nebula creates the secrets in Docker Swarm by piping values via stdin (never via shell args or temp files)
  4. The YAML is updated to mark each secret as external: true
  5. The stack is deployed with docker stack deploy
  6. Each container that declares the secret receives it mounted at /run/secrets/<name>

Secrets Manager

Admins and DevOps users can manage all secrets from the SECRETS button in the top navigation bar.

Permissions

RoleList & view metadataCreateRotateDeleteUse in deploy
AdminYesYesYesYesYes
DevOpsYesYesYesYesYes
DeveloperYes (names only)NoNoNoYes
UserNoNoNoNoNo

Status indicators

  • ACTIVE — secret exists in Docker Swarm and is available to services
  • ORPHAN — secret is recorded in Nebula's database but was removed from Docker Swarm externally (e.g. via CLI). Delete and recreate it to restore.

Creating a secret via UI

  1. Click SECRETS in the top navigation
  2. Click + NEW SECRET
  3. Enter a name (letters, numbers, _ and -, max 64 chars)
  4. Enter the secret value — use SHOW to toggle visibility
  5. Optionally add a description for team reference
  6. Click CREATE

Secret Rotation

Docker Swarm secrets are immutable — their value cannot be updated in place. Rotation requires deleting and recreating the secret under the same name. Nebula handles this automatically when you click ROTATE.

What rotation does

  1. Identifies which running services currently reference the secret
  2. Removes the existing secret from Docker Swarm
  3. Creates a new secret with the same name and the new value
  4. Returns a warning listing affected services
Important: After rotating a secret, all services that use it must be redeployed (or updated) to receive the new value. Running containers continue to use the old value in memory until restarted. Nebula will list the affected services in the rotation response.

Zero-downtime rotation pattern

  1. Rotate the secret in Nebula Secrets Manager
  2. Redeploy each affected stack from the Deploy Stack modal — leave the secret field blank to use the already-rotated value
  3. Docker Swarm performs a rolling update so at least one replica stays up during the transition

Reading Secrets in Your Application

Secrets are available as files at /run/secrets/<name> inside the container. Read them at application startup — never cache them beyond a single request cycle so rotation takes effect on redeploy.

Node.js

javascript
const fs = require('fs');

function readSecret(name) {
  const secretPath = `/run/secrets/${name}`;
  if (fs.existsSync(secretPath)) {
    return fs.readFileSync(secretPath, 'utf8').trim();
  }
  // Fallback to env var for local development
  return process.env[name.toUpperCase()];
}

const dbPassword = readSecret('db_password');
const stripeKey  = readSecret('stripe_key');

Python

python
import os

def read_secret(name: str) -> str:
    secret_path = f"/run/secrets/{name}"
    if os.path.exists(secret_path):
        with open(secret_path) as f:
            return f.read().strip()
    # Fallback to env var for local development
    return os.environ.get(name.upper(), "")

db_password = read_secret("db_password")
stripe_key  = read_secret("stripe_key")

PostgreSQL — password file support

PostgreSQL's official image supports reading the password directly from a file via the _FILE env var convention. No custom code needed:

yaml
environment:
  POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
  - db_password

MySQL, MariaDB, and several other official images support the same _FILE pattern.

API Reference

EndpointMethodAuthDescription
/api/secretsGETadmin, devops, developerList all secrets (metadata only — values never returned)
/api/secretsPOSTadmin, devopsCreate a new secret
/api/secrets/:namePUTadmin, devopsRotate (update) a secret value
/api/secrets/:nameDELETEadmin, devopsDelete a secret
/api/stack/secrets/parsePOSTanyParse YAML to detect secret references
/api/stack/deployPOSTadmin, devops, developerDeploy stack; accepts optional secrets object

Create a secret

http
POST /api/secrets
Authorization: Bearer <token>
Content-Type: application/json

{
  "name": "db_password",
  "value": "s3cur3-p@ssw0rd",
  "description": "PostgreSQL app user password"
}

→ {
  "success": true,
  "name": "db_password",
  "docker_secret_id": "abc123xyz"
}

Rotate a secret

http
PUT /api/secrets/db_password
Authorization: Bearer <token>
Content-Type: application/json

{ "value": "n3w-s3cur3-p@ssw0rd" }

→ {
  "success": true,
  "name": "db_password",
  "docker_secret_id": "def456uvw",
  "affected_services": ["myapp_api", "myapp_db"],
  "warning": "Services using this secret must be redeployed: myapp_api, myapp_db"
}

Deploy stack with inline secrets

http
POST /api/stack/deploy
Authorization: Bearer <token>
Content-Type: application/json

{
  "name": "myapp",
  "yaml": "version: '3.8'\n...",
  "secrets": {
    "db_password": "s3cur3-p@ssw0rd",
    "stripe_key": "sk_live_abc123"
  }
}

Storage

Nebula Pulse includes a built-in NFS Storage Manager that lets you register remote NFS servers and automatically provision subdirectory paths whenever a stack is deployed. This eliminates manual volume setup and ensures containers can be rescheduled to any Swarm node without losing data.

Why NFS? Docker Swarm bind mounts are node-local — if a container is rescheduled to a different node, bind-mounted data stays behind. NFS volumes are accessible from every node in the cluster, so containers can move freely without data migration.

How It Works

The Storage feature has two parts: a registry of your NFS servers and an auto-provisioning step that runs during stack deploys.

1 — Register your NFS server once in the Storage Manager
2 — Write a compose YAML using driver: local volumes with type: nfs driver options
3 — Deploy the stack — Nebula matches each NFS volume address to a registered config
4 — Nebula creates the subpath directory on the NFS share before containers start
5 — Containers start with all required directories already in place

If an NFS volume in a compose file cannot be matched to a registered server, Nebula logs a warning and skips provisioning for that volume — the deploy is not blocked.

Registering an NFS Server

Open the Storage Manager from the STORAGE button in the top navigation bar (visible to Admin and DevOps roles only).

  1. Click STORAGE in the top navigation
  2. Click + Add NFS Server
  3. Fill in the Name (internal label), Server (IP or hostname), Export Path (the root NFS export on the server), and optional Mount Options
  4. Click Save
  5. Use the TEST button to verify TCP reachability on port 2049 before deploying
FieldDescriptionExample
NameUnique internal label for this NFS servernas-01
ServerIP address or hostname of the NFS server192.168.1.50
Export PathThe root NFS export path shared by the server/mnt/pool/nebula
Mount OptionsNFS mount flags — do not include addr=, that is stored separatelyrw,nfsvers=4,soft
DescriptionOptional free-text notes for your teamMain NAS — pool storage

Do not include addr= in Mount Options. The server IP is stored in the Server field. Nebula inserts it into the NFS mount string automatically at deploy time.

Permissions

RoleView serversAdd / EditDeleteTest connection
AdminYesYesYesYes
DevOpsYesYesYesYes
DeveloperNoNoNoNo
UserNoNoNoNo

Volume YAML

Use the standard Docker Compose driver: local volume with type: nfs driver options. Nebula reads the addr from the o option string and the device path from driver_opts.device to match against your registered NFS servers.

yaml
version: "3.8"

services:
  nextcloud:
    image: nextcloud:28-apache
    volumes:
      - nc_data:/var/www/html
    networks:
      - nc_net
    deploy:
      replicas: 1

networks:
  nc_net:
    driver: overlay

volumes:
  nc_data:
    driver: local
    driver_opts:
      type: nfs
      o: "addr=192.168.1.50,rw,nfsvers=4,soft"
      device: ":/mnt/pool/nebula/nextcloud/data"

Key fields

FieldPurpose
driver: localStandard Docker volume driver — required for NFS volumes in Swarm
type: nfsTells Docker (and Nebula) this is an NFS volume
o: "addr=IP,..."NFS mount options; addr must match a registered NFS server's IP
device: ":/export/subpath"Leading colon is required. Path = export root + subpath. Nebula auto-creates the subpath.

Auto-Provisioning

When you deploy a stack containing NFS volumes, Nebula's deploy pipeline automatically creates the required subpath directories on the NFS share before any containers start.

What happens step-by-step

  1. Nebula parses your compose YAML and extracts all volumes with type: nfs
  2. For each volume, it reads the addr from the o string and the full device path
  3. It matches addr against your registered NFS servers by the Server field
  4. It computes the subpath = device path relative to the registered export root
  5. A temporary one-shot Docker service (pinned to the Swarm manager) is created; it mounts the NFS export and runs mkdir -p for each subpath
  6. The provisioner service is removed immediately after completion; the temporary volume is removed after 10 seconds
  7. The main stack is deployed — all required directories are guaranteed to exist

Root-level devices are skipped. If device equals the export root exactly (no subpath), Nebula assumes the directory already exists and skips provisioning. Only subdirectories trigger the auto-provisioner.

Deploy panel

When the YAML editor contains NFS volumes, a green STORAGE panel automatically appears in the Deploy Stack modal showing each volume and its match status:

  • ✓ nas-01 — volume matched to a registered NFS server; will be auto-provisioned
  • ✗ Not registered — no matching server found; a link to the Storage Manager is shown. The deploy will proceed, but the subpath will not be created.

API Reference

EndpointMethodAuthDescription
/api/storage/nfsGETadmin, devopsList all registered NFS server configs
/api/storage/nfsPOSTadmin, devopsRegister a new NFS server
/api/storage/nfs/:idPUTadmin, devopsUpdate server, export path, mount options, or description
/api/storage/nfs/:idDELETEadmin, devopsRemove a registered NFS server
/api/storage/nfs/:id/testPOSTadmin, devopsTCP probe on port 2049 — returns reachability status
/api/stack/nfs/parsePOSTanyParse compose YAML; returns list of NFS volumes with addr and device

Register an NFS server

http
POST /api/storage/nfs
Authorization: Bearer <token>
Content-Type: application/json

{
  "name": "nas-01",
  "server": "192.168.1.50",
  "export_path": "/mnt/pool/nebula",
  "mount_options": "rw,nfsvers=4,soft",
  "description": "Main NAS — pool storage"
}

→ {
  "id": 1,
  "name": "nas-01",
  "server": "192.168.1.50",
  "export_path": "/mnt/pool/nebula",
  "mount_options": "rw,nfsvers=4,soft"
}

Test connection

http
POST /api/storage/nfs/1/test
Authorization: Bearer <token>

→ { "reachable": true, "latency_ms": 2 }
  or
→ { "reachable": false, "error": "ECONNREFUSED" }

Parse NFS volumes from YAML

http
POST /api/stack/nfs/parse
Authorization: Bearer <token>
Content-Type: application/json

{ "yaml": "version: '3.8'
..." }

→ {
  "volumes": [
    { "volName": "nc_data", "addr": "192.168.1.50", "device": "/mnt/pool/nebula/nextcloud/data" },
    { "volName": "nc_db",   "addr": "192.168.1.50", "device": "/mnt/pool/nebula/nextcloud/db" }
  ]
}

Audit Logging

Nebula Pulse records a comprehensive audit trail of all user actions and system events. Audit logs are available to admin users across all license tiers and include automatic 90-day retention with configurable cleanup.

Event Types

Authentication Events

ActionDescription
LOGIN_SUCCESSUser successfully authenticated
LOGIN_FAILUREFailed login attempt (wrong credentials)

User Management Events

ActionDescription
USER_CREATENew user account created
USER_DELETEUser account deleted
PASSWORD_CHANGEUser changed their own password
PASSWORD_RESETAdmin reset a user's password

Stack & Service Events

ActionDescription
STACK_DEPLOYStack deployed — success or failure both logged
STACK_DESTROYStack destroyed
SERVICE_SCALEService scaled up or down
SERVICE_RESTARTService force-restarted

Node Events

ActionDescription
NODE_JOINNew node joined the swarm via SSH
NODE_REMOVENode drained and removed from swarm
NODE_LABEL_ADDLabel added or updated on a node
NODE_LABEL_REMOVELabel removed from a node

GitOps Events (Enterprise)

ActionDescription
GITOPS_REPO_ADDGit repository added for GitOps
GITOPS_REPO_DELETEGit repository removed
GITOPS_SYNCManual or webhook-triggered sync

System Events

ActionDescription
LICENSE_ACTIVATELicense key activated
BACKUP_DOWNLOADBackup ZIP downloaded
FILE_UPLOADFile uploaded to a container

Log Entry Fields

Every audit log entry contains:

FieldDescription
timestampWhen the event occurred (ISO 8601)
usernameWho performed the action
actionEvent type code (e.g. STACK_DEPLOY)
categoryEvent category (AUTH, STACK, NODE, etc.)
target_typeWhat was affected (stack, node, user, etc.)
target_nameName of the affected resource
ip_addressClient IP (forwarded through Nginx proxy)
user_agentBrowser / client user agent string
statussuccess or failure
detailsAdditional context as JSON (error messages, etc.)
session_idJWT session identifier

API & Export

Query audit logs

http
GET /api/audit/logs?page=1&limit=50&category=STACK&status=failure
Authorization: Bearer <admin-jwt>

Available query parameters: page, limit, category, action, username, status, startDate, endDate

Get audit statistics

http
GET /api/audit/stats
→ Event counts by category, status breakdown, failed logins, active users

Export logs

http
GET /api/audit/export?format=csv&startDate=2025-01-01&endDate=2025-12-31
→ Returns CSV or JSON file download

Viewing Logs in the UI

  1. Log in as an admin user
  2. Click the admin menu dropdown (top right)
  3. Select Audit Logs
  4. Use the filters to search by category, action, user, date range, or status
  5. Click Export to download as CSV or JSON

Retention Policy

  • Default retention: 90 days
  • Configurable via AUDIT_RETENTION_DAYS environment variable
  • Automatic daily cleanup of expired entries
  • Cleanup runs on a 24-hour interval

Licensing

Nebula Pulse uses JWT-based license keys to gate features across three tiers. The license file is stored at /opt/nebula/license.key and validated on startup and every hour thereafter.

License Tiers

TierLicense RequiredKey Capabilities
FreeNoneStack management, manual scaling, audit logging, up to 5 users
ProJWT license key+ Real-time metrics, autoscaling, multi-node management, RBAC, backup
EnterpriseJWT license key+ AI assistant, GitOps, full REST API

Activating a License

Via the UI

  1. In the Nebula Pulse UI, click ACTIVATE PRO (visible on the Free tier)
  2. Paste your license key into the input field
  3. Click ACTIVATE
  4. The page reloads with the new tier active

License Status API

http
GET /api/license/status

→ {
  "tier": "enterprise",
  "billing": "yearly",
  "client": "Acme Corp",
  "expired": false,
  "gracePeriod": false,
  "expiresAt": "2027-01-01T00:00:00.000Z",
  "daysRemaining": 300,
  "features": ["backup", "nodes", "scaling", "rbac", "ai", "sso", "audit", "api"]
}

Expiry & Grace Period

  • Licenses are validated on startup and every hour
  • On expiry, a 7-day grace period begins — all features remain active
  • After the grace period, the system reverts to Free tier
Days Until ExpiryWarningBilling Type
30 daysYellow bannerYearly
10 daysYellow bannerMonthly
1–10 daysOrange bannerBoth
0–7 days (expired)Red banner — grace period activeBoth

Troubleshooting

"License key has expired"

Generate a new key with a future expiration date and activate it via the UI.

Features not appearing after activation

The license is re-read on startup and every hour. If features still don't appear, restart the nebula_app service to force an immediate re-validation.

Key not working

Ensure the license was generated with the correct signing secret. Both the generator and the server must use the same secret key.

Identity Providers

Nebula Pulse Pro and Enterprise support Single Sign-On (SSO) via LDAP/LDAPS, Azure Active Directory (Entra ID), and Google Workspace. Once configured, users can authenticate with their existing corporate credentials instead of local passwords.

Pro or Enterprise license required. Identity provider configuration is gated behind the SSO feature flag. Activate your Pro or Enterprise license before proceeding.

LDAP / LDAPS

Nebula Pulse can authenticate users against any RFC 4511-compliant LDAP directory, including Active Directory, OpenLDAP, and FreeIPA. Use ldaps:// (port 636) for production deployments to encrypt the bind credential in transit.

Configuration

Add the following block to your Nebula Pulse environment (via .env or Docker Swarm secrets) and restart the service:

env
LDAP_ENABLED=true
LDAP_URL=ldaps://ldap.example.com:636
LDAP_BIND_DN=cn=svc-nebula,ou=service-accounts,dc=example,dc=com
LDAP_BIND_PASSWORD=supersecret
LDAP_SEARCH_BASE=ou=users,dc=example,dc=com
LDAP_SEARCH_FILTER=(sAMAccountName={{username}})
LDAP_TLS_CA=/run/secrets/ldap-ca.pem   # optional — custom CA for LDAPS
LDAP_ATTR_EMAIL=mail
LDAP_ATTR_DISPLAY_NAME=displayName

Configuration Reference

VariableRequiredDescription
LDAP_ENABLEDYesSet to true to enable LDAP authentication
LDAP_URLYesLDAP server URI — use ldaps:// for TLS
LDAP_BIND_DNYesDistinguished name of the service account used for directory searches
LDAP_BIND_PASSWORDYesPassword for the bind service account
LDAP_SEARCH_BASEYesBase DN under which users are searched
LDAP_SEARCH_FILTERYesLDAP filter to locate a user; {{username}} is replaced at runtime
LDAP_TLS_CANoPath to a PEM CA bundle for private/self-signed LDAPS certificates
LDAP_ATTR_EMAILNoLDAP attribute to map to the user email (default: mail)
LDAP_ATTR_DISPLAY_NAMENoLDAP attribute to map to the display name (default: displayName)

How It Works

  1. User submits their username and password at the Nebula Pulse login page.
  2. The backend binds to the LDAP server using the service account credentials.
  3. A search is performed using LDAP_SEARCH_FILTER to locate the user's DN.
  4. The backend attempts a bind with the located DN and the user-supplied password.
  5. On success, a JWT session token is issued. The user account is auto-provisioned on first login if it does not already exist locally.

Plain LDAP (port 389) transmits credentials in cleartext. Always use ldaps:// or configure StartTLS in production environments.

Troubleshooting

  • Invalid credentials — verify the bind DN and password with ldapsearch -H ldaps://host -D "<bind_dn>" -W -b "<search_base>"
  • Certificate errors — supply LDAP_TLS_CA pointing to your internal CA bundle
  • Users not found — test your search filter directly: ldapsearch -H ldaps://host -D "<bind_dn>" -W -b "<base>" "(sAMAccountName=testuser)"
  • Group-based access — extend LDAP_SEARCH_FILTER to require group membership, e.g. (&(sAMAccountName={{username}})(memberOf=cn=nebula-users,ou=groups,dc=example,dc=com))

Azure Active Directory (Entra ID)

Nebula Pulse integrates with Azure AD using OAuth 2.0 Authorization Code flow with PKCE. Users are redirected to Microsoft's login page and returned with an ID token that Nebula Pulse validates against your tenant.

Step 1 — Register an Application in Azure

  1. In the Azure portal, go to Azure Active Directory → App registrations → New registration.
  2. Set the Name (e.g. Nebula Pulse) and select the appropriate Supported account types (typically Accounts in this organizational directory only).
  3. Under Redirect URI, select Web and enter: https://<your-nebula-host>/api/auth/azure/callback
  4. Click Register. Note the Application (client) ID and Directory (tenant) ID.
  5. Go to Certificates & secrets → New client secret. Copy the generated value immediately — it is only shown once.

Step 2 — Configure Nebula Pulse

env
AZURE_AD_ENABLED=true
AZURE_AD_TENANT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
AZURE_AD_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
AZURE_AD_CLIENT_SECRET=your-client-secret-value
AZURE_AD_REDIRECT_URI=https://nebula.example.com/api/auth/azure/callback
# Optional: restrict login to specific Azure AD groups
AZURE_AD_ALLOWED_GROUPS=sg-nebula-admins,sg-nebula-users

Configuration Reference

VariableRequiredDescription
AZURE_AD_ENABLEDYesSet to true to enable Azure AD SSO
AZURE_AD_TENANT_IDYesYour Azure AD Directory (tenant) ID
AZURE_AD_CLIENT_IDYesApplication (client) ID from the app registration
AZURE_AD_CLIENT_SECRETYesClient secret value generated in Azure
AZURE_AD_REDIRECT_URIYesMust exactly match the redirect URI registered in Azure
AZURE_AD_ALLOWED_GROUPSNoComma-separated Azure AD group names or object IDs to restrict access

Required API Permissions

In your Azure app registration under API permissions, ensure the following Microsoft Graph Delegated permissions are granted:

  • openid — required for ID token issuance
  • profile — user display name and UPN
  • email — user email address
  • GroupMember.Read.All — required only if using AZURE_AD_ALLOWED_GROUPS

After adding permissions, click Grant admin consent to avoid per-user consent prompts.

Troubleshooting

  • AADSTS50011: redirect_uri mismatch — the AZURE_AD_REDIRECT_URI must exactly match (including trailing slash) what is registered in Azure.
  • AADSTS700016: application not found — verify AZURE_AD_TENANT_ID and AZURE_AD_CLIENT_ID.
  • User not in allowed group — check that the group name or object ID in AZURE_AD_ALLOWED_GROUPS matches the Azure AD group exactly.
  • Token validation failure — ensure the server clock is synchronized (NTP) — JWT validation is time-sensitive.

Google Workspace

Nebula Pulse supports Google OAuth 2.0 for authentication. Users sign in with their Google Workspace (or personal Google) account. You can optionally restrict login to a specific Workspace domain.

Step 1 — Create OAuth Credentials in Google Cloud

  1. Open the Google Cloud Console and select or create a project.
  2. Navigate to APIs & Services → OAuth consent screen. Set the User Type to Internal (Workspace org only) or External, fill in the required fields, and save.
  3. Go to APIs & Services → Credentials → Create Credentials → OAuth client ID.
  4. Choose Web application. Under Authorized redirect URIs add: https://<your-nebula-host>/api/auth/google/callback
  5. Click Create. Download or copy the Client ID and Client secret.

Step 2 — Configure Nebula Pulse

env
GOOGLE_AUTH_ENABLED=true
GOOGLE_CLIENT_ID=xxxxxxxxxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-client-secret
GOOGLE_REDIRECT_URI=https://nebula.example.com/api/auth/google/callback
# Optional: restrict login to your Workspace domain
GOOGLE_ALLOWED_DOMAIN=example.com

Configuration Reference

VariableRequiredDescription
GOOGLE_AUTH_ENABLEDYesSet to true to enable Google SSO
GOOGLE_CLIENT_IDYesOAuth 2.0 Client ID from Google Cloud Console
GOOGLE_CLIENT_SECRETYesOAuth 2.0 Client Secret
GOOGLE_REDIRECT_URIYesMust exactly match the authorized redirect URI in Google Cloud
GOOGLE_ALLOWED_DOMAINNoRestrict logins to accounts from this Google Workspace domain (e.g. example.com)

Scopes Requested

Nebula Pulse requests the following OAuth scopes during authentication:

  • openid — identity token
  • email — user's email address
  • profile — display name and avatar

If you leave GOOGLE_ALLOWED_DOMAIN unset and use an External consent screen, any Google account will be able to attempt login. Always set the domain for production deployments.

Troubleshooting

  • redirect_uri_mismatch — the GOOGLE_REDIRECT_URI value must exactly match one of the authorized redirect URIs in Google Cloud Console.
  • access_denied — if the consent screen is in Testing mode, only test users can authenticate. Publish the app or add the user as a tester.
  • Domain not allowed — the authenticated user's email domain does not match GOOGLE_ALLOWED_DOMAIN. Update the variable or use a different account.
  • invalid_client — double-check GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET; client secrets are invalidated when regenerated.