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.
Point your proxy at the host port
WEB_PORT(default8080).Set the public HTTPS address in
.env:iniENVIRONMENT=production PUBLIC_URL=https://opsnexus.example.com CORS_ORIGINS=https://opsnexus.example.comIf build agents reach OpsNexus through the same HTTPS address, leave
AGENT_URLempty. Otherwise set it to an address every agent can reach.If you use SSO, update the redirect URIs with Google or Microsoft to the new
PUBLIC_URL.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/wsfor 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-ForandX-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;
}
}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.
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://.
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).
Enable a KV version 2 secrets engine. OpsNexus uses the mount
secretand the prefixopsnexusby default (VAULT_MOUNT,VAULT_PREFIX).Create a policy from
deploy/vault/opsnexus-policy.hcl. It grants only what OpsNexus needs:hclpath "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.
Give OpsNexus credentials. Prefer AppRole over a static token. In
.env:iniSECRET_BACKEND=vault VAULT_ROLE_ID=<role id> VAULT_SECRET_ID=<secret id>Or, with a token:
VAULT_TOKEN=<token>.Point the backend at your Vault and stop it from reading the bundled token file, in
docker-compose.override.yml. Repeat the block forworkerandscheduler:yamlservices: api: environment: VAULT_ADDR: https://vault.example.com:8200 VAULT_TOKEN_FILE: ""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.
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):
./scripts/backup.shIt 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:
./scripts/restore.sh backups/opsnexus-<timestamp>.sql.gzThe 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:
docker compose up -d --scale worker=3Pipeline 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_KEYis at least 32 random bytes, kept in your secret manager, not in Git. -
ENVIRONMENT=production, and OpsNexus is reachable only over HTTPS, withPUBLIC_URLset to thehttps://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.1only (VAULT_HOST_PORT, default18200). - 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_TARGETSstaysfalseunless 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.