OpsNexus

Running in production

Put OpsNexus behind TLS, use your own PostgreSQL and Vault, back it up, scale the workers and harden it.

On this page

The default Docker Compose setup is built for one self-hosted server. This page covers what to change before real teams depend on it. Work through the security checklist at the end before you go live.

Overview

Area Default compose setup Recommended for production
HTTPS None. web serves plain HTTP on WEB_PORT A TLS-terminating reverse proxy or load balancer in front of web
Database db container with a local volume Managed PostgreSQL with encryption at rest, automated backups and point-in-time recovery
Secrets vault container, unsealed by vault-init with a key kept on the same host Your organization's Vault cluster with TLS and auto-unseal
Backups None scheduled Scheduled in-app backups, plus database and Vault backups

Put OpsNexus behind TLS

OpsNexus does not terminate TLS itself. Put a reverse proxy or load balancer in front of the web container and serve HTTPS only.

  1. Point your proxy at the host port WEB_PORT (default 8080).

  2. Set the public HTTPS address in .env:

    ini
    ENVIRONMENT=production
    PUBLIC_URL=https://opsnexus.example.com
    CORS_ORIGINS=https://opsnexus.example.com
  3. If build agents reach OpsNexus through the same HTTPS address, leave AGENT_URL empty. Otherwise set it to an address every agent can reach.

  4. If you use SSO, update the redirect URIs with Google or Microsoft to the new PUBLIC_URL.

  5. Recreate the backend containers: docker compose up -d --force-recreate api worker scheduler.

With ENVIRONMENT=production, the API adds a Strict-Transport-Security header to its responses.

Your proxy must:

  • Pass WebSocket upgrades for paths under /api/ws. The app uses /api/ws for live updates and /api/ws/terminal/... for the browser SSH terminal. Allow long-lived connections (the bundled Nginx allows one hour).
  • Allow large uploads. Build agents upload artifacts to /api/ci/runner/runs/<id>/artifacts, up to 100 MB per file by default. The bundled Nginx allows 110 MB on that path and 20 MB elsewhere.
  • Forward the client address with X-Forwarded-For and X-Forwarded-Proto.

An example Nginx server block for the proxy host:

server {
    listen 443 ssl;
    server_name opsnexus.example.com;
    ssl_certificate     /etc/ssl/opsnexus.crt;
    ssl_certificate_key /etc/ssl/opsnexus.key;
    client_max_body_size 110m;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $http_connection;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_read_timeout 3600s;
    }
}
Tip

If the proxy runs on the same host, publish web on the loopback address only, so nobody can bypass HTTPS: set WEB_PORT=127.0.0.1:8080 in .env.

Health checks for load balancers

Endpoint Returns
GET /healthz ok from the web container
GET /api/health/live The API process is up
GET /api/health/ready The API can reach PostgreSQL and Redis

Use an external PostgreSQL

The compose file sets DATABASE_URL for the backend containers, so a value in .env is ignored. Override it in a docker-compose.override.yml next to docker-compose.yml. Compose reads that file automatically.

yaml
services:
  api:
    environment:
      DATABASE_URL: postgresql+psycopg://opsnexus:<password>@db.example.com:5432/opsnexus
  worker:
    environment:
      DATABASE_URL: postgresql+psycopg://opsnexus:<password>@db.example.com:5432/opsnexus
  scheduler:
    environment:
      DATABASE_URL: postgresql+psycopg://opsnexus:<password>@db.example.com:5432/opsnexus
  • Use PostgreSQL 16, the version the bundled container runs.
  • Create an empty database and a user that owns it. The API creates the tables with migrations on its first start.
  • The connection string must start with postgresql+psycopg://.
Note

The backend services still depend on the bundled db container in docker-compose.yml, so it keeps starting even though OpsNexus no longer uses it. To remove it, edit the depends_on list in the compose file.

Redis works the same way: override REDIS_URL (for example redis://redis.example.com:6379/0) if you want a managed Redis. Use one with persistence.

Use your own Vault

The bundled Vault stores its unseal key in the vault-init volume on the same host, so it can restart unattended. That is fine for local use only. In production, use your organization's Vault cluster with TLS and auto-unseal (AWS KMS, Azure Key Vault or GCP KMS).

  1. Enable a KV version 2 secrets engine. OpsNexus uses the mount secret and the prefix opsnexus by default (VAULT_MOUNT, VAULT_PREFIX).

  2. Create a policy from deploy/vault/opsnexus-policy.hcl. It grants only what OpsNexus needs:

    hcl
    path "secret/data/opsnexus/*"     { capabilities = ["create", "read", "update", "delete"] }
    path "secret/metadata/opsnexus/*" { capabilities = ["read", "list", "delete"] }
    path "auth/token/renew-self"      { capabilities = ["update"] }

    Change the paths if you use another mount or prefix.

  3. Give OpsNexus credentials. Prefer AppRole over a static token. In .env:

    ini
    SECRET_BACKEND=vault
    VAULT_ROLE_ID=<role id>
    VAULT_SECRET_ID=<secret id>

    Or, with a token: VAULT_TOKEN=<token>.

  4. Point the backend at your Vault and stop it from reading the bundled token file, in docker-compose.override.yml. Repeat the block for worker and scheduler:

    yaml
    services:
      api:
        environment:
          VAULT_ADDR: https://vault.example.com:8200
          VAULT_TOKEN_FILE: ""
  5. Recreate the backend containers and check Settings → System status. The secret store should show HashiCorp Vault as OK.

OpsNexus uses VAULT_TOKEN first, then the token file, then AppRole. The scheduler renews its Vault token once a day.

Warning

Secrets already stored in the bundled Vault are not copied to the new one. Decide on your Vault before you add credentials, or add them again after you switch.

Back up OpsNexus

Use more than one kind of backup, and test restores.

In-app backups. Settings → Backup & restore (super admins only) creates a single backup file of the platform's data. It can encrypt the file with a passphrase, include secret values when encrypted, run on a schedule, and store files in the local backups volume, Amazon S3 or Azure Blob Storage. A restore only accepts backups from the same database schema version. See Backup and restore.

Database and Vault dumps. With the bundled database and Vault, run from the repository root (in Git Bash on Windows):

bash
./scripts/backup.sh

It writes backups/opsnexus-<timestamp>.sql.gz (a pg_dump of the database) and backups/vault-<timestamp>.tar.gz (the Vault data). To restore a database dump, which stops the backend while it runs:

bash
./scripts/restore.sh backups/opsnexus-<timestamp>.sql.gz
Warning

The Vault archive is useless without the unseal key in the vault-init volume. Keep a copy of that key somewhere safe and separate from the backups.

With an external database or Vault, use their own backup features instead.

The compose stack keeps its data in these Docker volumes:

Volume Holds
pgdata PostgreSQL data
redisdata Redis data (queue and cache)
vaultdata Vault data
vault-init Vault unseal key, root token and the OpsNexus token
backups In-app backup files
artifacts Files archived by pipeline runs
geoip Country database for traffic statistics
runner-data The optional Docker build agent's config and workspace

Scale the workers

Service How to scale
api Raise API_WORKERS (processes per container, default 2). The API is stateless.
worker Raise WORKER_CONCURRENCY (jobs per container, default 4), or run more containers.
scheduler Always exactly one container. More would start every scheduled job more than once.

To run three worker containers:

bash
docker compose up -d --scale worker=3

Pipeline steps do not run in these containers. They run on build agents, so add agents or executors to run more builds at once. See Build agents.

Security checklist

  • SECRET_KEY is at least 32 random bytes, kept in your secret manager, not in Git.
  • ENVIRONMENT=production, and OpsNexus is reachable only over HTTPS, with PUBLIC_URL set to the https:// URL.
  • Vault is your organization's cluster with TLS and auto-unseal, accessed with AppRole rather than a static token.
  • The Vault UI port is not reachable from the network. Compose binds it to 127.0.0.1 only (VAULT_HOST_PORT, default 18200).
  • PostgreSQL is managed, encrypted at rest, with automated backups and point-in-time recovery.
  • Require MFA for every user is on in the password policy.
  • The first admin's one-time password is changed. Admins use named accounts, and one break-glass admin is kept.
  • Integration tokens have the minimum scopes. Allow write actions is on only where needed.
  • ALLOW_PRIVATE_TARGETS stays false unless you monitor internal services.
  • Servers used with the browser SSH terminal have a dedicated, least-privilege account, preferably with SSH keys, and SSH is firewalled so only the OpsNexus host can reach it.
  • Build agents use HTTPS and are never registered with --insecure.
  • Audit logs are shipped to your SIEM, with Export CSV or a log shipper on the container output.
  • Backups run on a schedule, and you have tested a restore.
  • Base images are kept up to date. See Upgrading.

For sign-in hardening, see SSO and MFA. For who can do what, see Access control.